diff --git a/.phan/config.php b/.phan/config.php new file mode 100644 index 000000000..7e37bedc7 --- /dev/null +++ b/.phan/config.php @@ -0,0 +1,135 @@ + 8.0, + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in exclude_analysis_directory_list, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'include', + 'plugins', + 'bundled-libs', + '.', + ], + + // A regex used to match every file name that you want to + // exclude from parsing. Actual value will exclude every + // "test", "tests", "Test" and "Tests" folders found in + // "vendor/" directory. + 'exclude_file_regex' => '@^bundled-libs/.*/(tests?|Tests?)/@', + + // A directory list that defines files that will be excluded + // from static analysis, but whose class and method + // information should be included. + // + // Generally, you'll want to include the directories for + // third-party code (such as "vendor/") in this list. + // + // n.b.: If you'd like to parse but not analyze 3rd + // party code, directories containing that code + // should be added to both the `directory_list` + // and `exclude_analysis_directory_list` arrays. + 'exclude_analysis_directory_list' => [ + 'bundled-libs/', + 'tests/', + ], + + // Backwards Compatibility Checking. This is slow + // and expensive, but you should consider running + // it before upgrading your version of PHP to a + // new version that has backward compatibility + // breaks. (Also see target_php_version) + 'backward_compatibility_checks' => true, + + // If true, this run a quick version of checks that takes less + // time at the cost of not running as thorough + // an analysis. You should consider setting this + // to true only when you wish you had more **undiagnosed** issues + // to fix in your code base. + 'quick_mode' => true, + + // If enabled, check all methods that override a + // parent method to make sure its signature is + // compatible with the parent's. This check + // can add quite a bit of time to the analysis. + 'analyze_signature_compatibility' => false, + + // The minimum severity level to report on. This can be + // set to Issue::SEVERITY_LOW(0), Issue::SEVERITY_NORMAL(5) or + // Issue::SEVERITY_CRITICAL(10). Setting it to only + // critical issues is a good place to start on a big + // sloppy mature code base. + 'minimum_severity' => 10, + + // If true, missing properties will be created when + // they are first seen. If false, we'll report an + // error message if there is an attempt to write + // to a class property that wasn't explicitly + // defined. + 'allow_missing_properties' => true, + + // Allow null to be cast as any type and for any + // type to be cast to null. Setting this to false + // will cut down on false positives. + 'null_casts_as_any_type' => true, + + // Allow null to be cast as any array-like type. + // This is an incremental step in migrating away from null_casts_as_any_type. + // If null_casts_as_any_type is true, this has no effect. + 'null_casts_as_array' => false, + + // Allow any array-like type to be cast to null. + // This is an incremental step in migrating away from null_casts_as_any_type. + // If null_casts_as_any_type is true, this has no effect. + 'array_casts_as_null' => false, + + // If enabled, scalars (int, float, bool, true, false, string, null) + // are treated as if they can cast to each other. + 'scalar_implicit_cast' => true, + + // If this has entries, scalars (int, float, bool, true, false, string, null) + // are allowed to perform the casts listed. + // E.g. ['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']] + // allows casting null to a string, but not vice versa. + // (subset of scalar_implicit_cast) + 'scalar_implicit_partial' => [], + + // If true, seemingly undeclared variables in the global + // scope will be ignored. This is useful for projects + // with complicated cross-file globals that you have no + // hope of fixing. + 'ignore_undeclared_variables_in_global_scope' => true, + + // Add any issue types (such as 'PhanUndeclaredMethod') + // to this black-list to inhibit them from being reported. + 'suppress_issue_types' => [ + // 'PhanUndeclaredMethod', + ], + + // If empty, no filter against issues types will be applied. + // If this white-list is non-empty, only issues within the list + // will be emitted by Phan. + 'whitelist_issue_types' => [ + // 'PhanAccessMethodPrivate', + ], +]; \ No newline at end of file diff --git a/bundled-libs/bin/phan b/bundled-libs/bin/phan new file mode 120000 index 000000000..18a39f924 --- /dev/null +++ b/bundled-libs/bin/phan @@ -0,0 +1 @@ +../phan/phan/phan \ No newline at end of file diff --git a/bundled-libs/bin/phan_client b/bundled-libs/bin/phan_client new file mode 120000 index 000000000..8deb02b65 --- /dev/null +++ b/bundled-libs/bin/phan_client @@ -0,0 +1 @@ +../phan/phan/phan_client \ No newline at end of file diff --git a/bundled-libs/bin/tocheckstyle b/bundled-libs/bin/tocheckstyle new file mode 120000 index 000000000..5434201ea --- /dev/null +++ b/bundled-libs/bin/tocheckstyle @@ -0,0 +1 @@ +../phan/phan/tocheckstyle \ No newline at end of file diff --git a/bundled-libs/composer/ClassLoader.php b/bundled-libs/composer/ClassLoader.php index fce8549f0..247294d66 100644 --- a/bundled-libs/composer/ClassLoader.php +++ b/bundled-libs/composer/ClassLoader.php @@ -37,11 +37,13 @@ * * @author Fabien Potencier * @author Jordi Boggiano - * @see http://www.php-fig.org/psr/psr-0/ - * @see http://www.php-fig.org/psr/psr-4/ + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { + private $vendorDir; + // PSR-4 private $prefixLengthsPsr4 = array(); private $prefixDirsPsr4 = array(); @@ -57,10 +59,17 @@ class ClassLoader private $missingClasses = array(); private $apcuPrefix; + private static $registeredLoaders = array(); + + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } + public function getPrefixes() { if (!empty($this->prefixesPsr0)) { - return call_user_func_array('array_merge', $this->prefixesPsr0); + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); @@ -300,6 +309,17 @@ public function getApcuPrefix() public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } } /** @@ -308,6 +328,10 @@ public function register($prepend = false) public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } } /** @@ -367,6 +391,16 @@ public function findFile($class) return $file; } + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + private function findFileWithExtension($class, $ext) { // PSR-4 lookup diff --git a/bundled-libs/composer/InstalledVersions.php b/bundled-libs/composer/InstalledVersions.php new file mode 100644 index 000000000..d23ec626b --- /dev/null +++ b/bundled-libs/composer/InstalledVersions.php @@ -0,0 +1,563 @@ + + array ( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => + array ( + ), + 'reference' => '05f58f90d743fe9ade24f3fdfe9a934d0b87c6a1', + 'name' => '__root__', + ), + 'versions' => + array ( + '__root__' => + array ( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => + array ( + ), + 'reference' => '05f58f90d743fe9ade24f3fdfe9a934d0b87c6a1', + ), + 'composer/semver' => + array ( + 'pretty_version' => '3.2.4', + 'version' => '3.2.4.0', + 'aliases' => + array ( + ), + 'reference' => 'a02fdf930a3c1c3ed3a49b5f63859c0c20e10464', + ), + 'composer/xdebug-handler' => + array ( + 'pretty_version' => '1.4.6', + 'version' => '1.4.6.0', + 'aliases' => + array ( + ), + 'reference' => 'f27e06cd9675801df441b3656569b328e04aa37c', + ), + 'felixfbecker/advanced-json-rpc' => + array ( + 'pretty_version' => 'v3.2.0', + 'version' => '3.2.0.0', + 'aliases' => + array ( + ), + 'reference' => '06f0b06043c7438959dbdeed8bb3f699a19be22e', + ), + 'katzgrau/klogger' => + array ( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'aliases' => + array ( + ), + 'reference' => '46cdd92a9b4a8443120cc955bf831450cb274813', + ), + 'laminas/laminas-db' => + array ( + 'pretty_version' => '2.12.0', + 'version' => '2.12.0.0', + 'aliases' => + array ( + ), + 'reference' => '80cbba4e749f9eb7d8036172acb9ad41e8b6923f', + ), + 'laminas/laminas-stdlib' => + array ( + 'pretty_version' => '3.3.1', + 'version' => '3.3.1.0', + 'aliases' => + array ( + ), + 'reference' => 'd81c7ffe602ed0e6ecb18691019111c0f4bf1efe', + ), + 'laminas/laminas-zendframework-bridge' => + array ( + 'pretty_version' => '1.2.0', + 'version' => '1.2.0.0', + 'aliases' => + array ( + ), + 'reference' => '6cccbddfcfc742eb02158d6137ca5687d92cee32', + ), + 'microsoft/tolerant-php-parser' => + array ( + 'pretty_version' => 'v0.0.23', + 'version' => '0.0.23.0', + 'aliases' => + array ( + ), + 'reference' => '1d76657e3271754515ace52501d3e427eca42ad0', + ), + 'netresearch/jsonmapper' => + array ( + 'pretty_version' => 'v2.1.0', + 'version' => '2.1.0.0', + 'aliases' => + array ( + ), + 'reference' => 'e0f1e33a71587aca81be5cffbb9746510e1fe04e', + ), + 'phan/phan' => + array ( + 'pretty_version' => '3.2.10', + 'version' => '3.2.10.0', + 'aliases' => + array ( + ), + 'reference' => '08978125063189a3e43448c99d50afd3b216234c', + ), + 'phpdocumentor/reflection-common' => + array ( + 'pretty_version' => '2.2.0', + 'version' => '2.2.0.0', + 'aliases' => + array ( + ), + 'reference' => '1d01c49d4ed62f25aa84a747ad35d5a16924662b', + ), + 'phpdocumentor/reflection-docblock' => + array ( + 'pretty_version' => '5.2.2', + 'version' => '5.2.2.0', + 'aliases' => + array ( + ), + 'reference' => '069a785b2141f5bcf49f3e353548dc1cce6df556', + ), + 'phpdocumentor/type-resolver' => + array ( + 'pretty_version' => '1.4.0', + 'version' => '1.4.0.0', + 'aliases' => + array ( + ), + 'reference' => '6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0', + ), + 'psr/container' => + array ( + 'pretty_version' => '1.1.1', + 'version' => '1.1.1.0', + 'aliases' => + array ( + ), + 'reference' => '8622567409010282b7aeebe4bb841fe98b58dcaf', + ), + 'psr/log' => + array ( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'aliases' => + array ( + ), + 'reference' => 'fe0936ee26643249e916849d48e3a51d5f5e278b', + ), + 'psr/log-implementation' => + array ( + 'provided' => + array ( + 0 => '1.0', + ), + ), + 'psr/simple-cache' => + array ( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'aliases' => + array ( + ), + 'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b', + ), + 'psr/simple-cache-implementation' => + array ( + 'provided' => + array ( + 0 => '1.0', + ), + ), + 'sabre/event' => + array ( + 'pretty_version' => '5.1.2', + 'version' => '5.1.2.0', + 'aliases' => + array ( + ), + 'reference' => 'c120bec57c17b6251a496efc82b732418b49d50a', + ), + 'symfony/console' => + array ( + 'pretty_version' => 'v5.2.6', + 'version' => '5.2.6.0', + 'aliases' => + array ( + ), + 'reference' => '35f039df40a3b335ebf310f244cb242b3a83ac8d', + ), + 'symfony/polyfill-ctype' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => 'c6c942b1ac76c82448322025e084cadc56048b4e', + ), + 'symfony/polyfill-intl-grapheme' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => '5601e09b69f26c1828b13b6bb87cb07cddba3170', + ), + 'symfony/polyfill-intl-normalizer' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => '43a0283138253ed1d48d352ab6d0bdb3f809f248', + ), + 'symfony/polyfill-mbstring' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => '5232de97ee3b75b0360528dae24e73db49566ab1', + ), + 'symfony/polyfill-php73' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => 'a678b42e92f86eca04b7fa4c0f6f19d097fb69e2', + ), + 'symfony/polyfill-php80' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => 'dc3063ba22c2a1fd2f45ed856374d79114998f91', + ), + 'symfony/service-contracts' => + array ( + 'pretty_version' => 'v2.4.0', + 'version' => '2.4.0.0', + 'aliases' => + array ( + ), + 'reference' => 'f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb', + ), + 'symfony/string' => + array ( + 'pretty_version' => 'v5.2.6', + 'version' => '5.2.6.0', + 'aliases' => + array ( + ), + 'reference' => 'ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572', + ), + 'voku/simple-cache' => + array ( + 'pretty_version' => '4.0.5', + 'version' => '4.0.5.0', + 'aliases' => + array ( + ), + 'reference' => '416cf88902991f3bf6168b71c0683e6dabb3d5e1', + ), + 'webmozart/assert' => + array ( + 'pretty_version' => '1.10.0', + 'version' => '1.10.0.0', + 'aliases' => + array ( + ), + 'reference' => '6964c76c7804814a842473e0c8fd15bab0f18e25', + ), + 'zendframework/zend-db' => + array ( + 'replaced' => + array ( + 0 => '^2.11.0', + ), + ), + 'zendframework/zend-stdlib' => + array ( + 'replaced' => + array ( + 0 => '^3.2.1', + ), + ), + ), +); +private static $canGetVendors; +private static $installedByVendor = array(); + + + + + + + +public static function getInstalledPackages() +{ +$packages = array(); +foreach (self::getInstalled() as $installed) { +$packages[] = array_keys($installed['versions']); +} + + +if (1 === \count($packages)) { +return $packages[0]; +} + +return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); +} + + + + + + + + + +public static function isInstalled($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (isset($installed['versions'][$packageName])) { +return true; +} +} + +return false; +} + + + + + + + + + + + + + + +public static function satisfies(VersionParser $parser, $packageName, $constraint) +{ +$constraint = $parser->parseConstraints($constraint); +$provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + +return $provided->matches($constraint); +} + + + + + + + + + + +public static function getVersionRanges($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +$ranges = array(); +if (isset($installed['versions'][$packageName]['pretty_version'])) { +$ranges[] = $installed['versions'][$packageName]['pretty_version']; +} +if (array_key_exists('aliases', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); +} +if (array_key_exists('replaced', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); +} +if (array_key_exists('provided', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); +} + +return implode(' || ', $ranges); +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getVersion($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['version'])) { +return null; +} + +return $installed['versions'][$packageName]['version']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getPrettyVersion($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['pretty_version'])) { +return null; +} + +return $installed['versions'][$packageName]['pretty_version']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getReference($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['reference'])) { +return null; +} + +return $installed['versions'][$packageName]['reference']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getRootPackage() +{ +$installed = self::getInstalled(); + +return $installed[0]['root']; +} + + + + + + + +public static function getRawData() +{ +return self::$installed; +} + + + + + + + + + + + + + + + + + + + +public static function reload($data) +{ +self::$installed = $data; +self::$installedByVendor = array(); +} + + + + +private static function getInstalled() +{ +if (null === self::$canGetVendors) { +self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); +} + +$installed = array(); + +if (self::$canGetVendors) { +foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { +if (isset(self::$installedByVendor[$vendorDir])) { +$installed[] = self::$installedByVendor[$vendorDir]; +} elseif (is_file($vendorDir.'/composer/installed.php')) { +$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; +} +} +} + +$installed[] = self::$installed; + +return $installed; +} +} diff --git a/bundled-libs/composer/autoload_classmap.php b/bundled-libs/composer/autoload_classmap.php index cecd36ce1..9cb6c9b03 100644 --- a/bundled-libs/composer/autoload_classmap.php +++ b/bundled-libs/composer/autoload_classmap.php @@ -6,7 +6,861 @@ $baseDir = dirname($vendorDir); return array( + 'AdvancedJsonRpc\\Dispatcher' => $vendorDir . '/felixfbecker/advanced-json-rpc/lib/Dispatcher.php', + 'AdvancedJsonRpc\\Error' => $vendorDir . '/felixfbecker/advanced-json-rpc/lib/Error.php', + 'AdvancedJsonRpc\\ErrorCode' => $vendorDir . '/felixfbecker/advanced-json-rpc/lib/ErrorCode.php', + 'AdvancedJsonRpc\\ErrorResponse' => $vendorDir . '/felixfbecker/advanced-json-rpc/lib/ErrorResponse.php', + 'AdvancedJsonRpc\\Message' => $vendorDir . '/felixfbecker/advanced-json-rpc/lib/Message.php', + 'AdvancedJsonRpc\\Notification' => $vendorDir . '/felixfbecker/advanced-json-rpc/lib/Notification.php', + 'AdvancedJsonRpc\\Request' => $vendorDir . '/felixfbecker/advanced-json-rpc/lib/Request.php', + 'AdvancedJsonRpc\\Response' => $vendorDir . '/felixfbecker/advanced-json-rpc/lib/Response.php', + 'AdvancedJsonRpc\\SuccessResponse' => $vendorDir . '/felixfbecker/advanced-json-rpc/lib/SuccessResponse.php', + 'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'Composer\\Semver\\Comparator' => $vendorDir . '/composer/semver/src/Comparator.php', + 'Composer\\Semver\\CompilingMatcher' => $vendorDir . '/composer/semver/src/CompilingMatcher.php', + 'Composer\\Semver\\Constraint\\Bound' => $vendorDir . '/composer/semver/src/Constraint/Bound.php', + 'Composer\\Semver\\Constraint\\Constraint' => $vendorDir . '/composer/semver/src/Constraint/Constraint.php', + 'Composer\\Semver\\Constraint\\ConstraintInterface' => $vendorDir . '/composer/semver/src/Constraint/ConstraintInterface.php', + 'Composer\\Semver\\Constraint\\MatchAllConstraint' => $vendorDir . '/composer/semver/src/Constraint/MatchAllConstraint.php', + 'Composer\\Semver\\Constraint\\MatchNoneConstraint' => $vendorDir . '/composer/semver/src/Constraint/MatchNoneConstraint.php', + 'Composer\\Semver\\Constraint\\MultiConstraint' => $vendorDir . '/composer/semver/src/Constraint/MultiConstraint.php', + 'Composer\\Semver\\Interval' => $vendorDir . '/composer/semver/src/Interval.php', + 'Composer\\Semver\\Intervals' => $vendorDir . '/composer/semver/src/Intervals.php', + 'Composer\\Semver\\Semver' => $vendorDir . '/composer/semver/src/Semver.php', + 'Composer\\Semver\\VersionParser' => $vendorDir . '/composer/semver/src/VersionParser.php', + 'Composer\\XdebugHandler\\PhpConfig' => $vendorDir . '/composer/xdebug-handler/src/PhpConfig.php', + 'Composer\\XdebugHandler\\Process' => $vendorDir . '/composer/xdebug-handler/src/Process.php', + 'Composer\\XdebugHandler\\Status' => $vendorDir . '/composer/xdebug-handler/src/Status.php', + 'Composer\\XdebugHandler\\XdebugHandler' => $vendorDir . '/composer/xdebug-handler/src/XdebugHandler.php', + 'JsonException' => $vendorDir . '/symfony/polyfill-php73/Resources/stubs/JsonException.php', + 'JsonMapper' => $vendorDir . '/netresearch/jsonmapper/src/JsonMapper.php', + 'JsonMapper_Exception' => $vendorDir . '/netresearch/jsonmapper/src/JsonMapper/Exception.php', 'Katzgrau\\KLogger\\Logger' => $vendorDir . '/katzgrau/klogger/src/Logger.php', + 'Laminas\\Db\\Adapter\\Adapter' => $vendorDir . '/laminas/laminas-db/src/Adapter/Adapter.php', + 'Laminas\\Db\\Adapter\\AdapterAbstractServiceFactory' => $vendorDir . '/laminas/laminas-db/src/Adapter/AdapterAbstractServiceFactory.php', + 'Laminas\\Db\\Adapter\\AdapterAwareInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/AdapterAwareInterface.php', + 'Laminas\\Db\\Adapter\\AdapterAwareTrait' => $vendorDir . '/laminas/laminas-db/src/Adapter/AdapterAwareTrait.php', + 'Laminas\\Db\\Adapter\\AdapterInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/AdapterInterface.php', + 'Laminas\\Db\\Adapter\\AdapterServiceDelegator' => $vendorDir . '/laminas/laminas-db/src/Adapter/AdapterServiceDelegator.php', + 'Laminas\\Db\\Adapter\\AdapterServiceFactory' => $vendorDir . '/laminas/laminas-db/src/Adapter/AdapterServiceFactory.php', + 'Laminas\\Db\\Adapter\\Driver\\AbstractConnection' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/AbstractConnection.php', + 'Laminas\\Db\\Adapter\\Driver\\ConnectionInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/ConnectionInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\DriverInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/DriverInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\Feature\\AbstractFeature' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Feature/AbstractFeature.php', + 'Laminas\\Db\\Adapter\\Driver\\Feature\\DriverFeatureInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Feature/DriverFeatureInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\IbmDb2\\Connection' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\IbmDb2\\IbmDb2' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/IbmDb2/IbmDb2.php', + 'Laminas\\Db\\Adapter\\Driver\\IbmDb2\\Result' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\IbmDb2\\Statement' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\Mysqli\\Connection' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Mysqli/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Mysqli\\Mysqli' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Mysqli/Mysqli.php', + 'Laminas\\Db\\Adapter\\Driver\\Mysqli\\Result' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Mysqli/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Mysqli\\Statement' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Mysqli/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Connection' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Feature\\RowCounter' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Feature/RowCounter.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Oci8' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Oci8.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Result' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Statement' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Connection' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Feature\\OracleRowCounter' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/OracleRowCounter.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Feature\\SqliteRowCounter' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Pdo' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Pdo.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Result' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Statement' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\Pgsql\\Connection' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pgsql/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Pgsql\\Pgsql' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pgsql/Pgsql.php', + 'Laminas\\Db\\Adapter\\Driver\\Pgsql\\Result' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pgsql/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Pgsql\\Statement' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Pgsql/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\ResultInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/ResultInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Connection' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Exception\\ErrorException' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ErrorException.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Exception\\ExceptionInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Result' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Sqlsrv' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Sqlsrv.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Statement' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\StatementInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Driver/StatementInterface.php', + 'Laminas\\Db\\Adapter\\Exception\\ErrorException' => $vendorDir . '/laminas/laminas-db/src/Adapter/Exception/ErrorException.php', + 'Laminas\\Db\\Adapter\\Exception\\ExceptionInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Exception/ExceptionInterface.php', + 'Laminas\\Db\\Adapter\\Exception\\InvalidArgumentException' => $vendorDir . '/laminas/laminas-db/src/Adapter/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\Adapter\\Exception\\InvalidConnectionParametersException' => $vendorDir . '/laminas/laminas-db/src/Adapter/Exception/InvalidConnectionParametersException.php', + 'Laminas\\Db\\Adapter\\Exception\\InvalidQueryException' => $vendorDir . '/laminas/laminas-db/src/Adapter/Exception/InvalidQueryException.php', + 'Laminas\\Db\\Adapter\\Exception\\RuntimeException' => $vendorDir . '/laminas/laminas-db/src/Adapter/Exception/RuntimeException.php', + 'Laminas\\Db\\Adapter\\Exception\\UnexpectedValueException' => $vendorDir . '/laminas/laminas-db/src/Adapter/Exception/UnexpectedValueException.php', + 'Laminas\\Db\\Adapter\\ParameterContainer' => $vendorDir . '/laminas/laminas-db/src/Adapter/ParameterContainer.php', + 'Laminas\\Db\\Adapter\\Platform\\AbstractPlatform' => $vendorDir . '/laminas/laminas-db/src/Adapter/Platform/AbstractPlatform.php', + 'Laminas\\Db\\Adapter\\Platform\\IbmDb2' => $vendorDir . '/laminas/laminas-db/src/Adapter/Platform/IbmDb2.php', + 'Laminas\\Db\\Adapter\\Platform\\Mysql' => $vendorDir . '/laminas/laminas-db/src/Adapter/Platform/Mysql.php', + 'Laminas\\Db\\Adapter\\Platform\\Oracle' => $vendorDir . '/laminas/laminas-db/src/Adapter/Platform/Oracle.php', + 'Laminas\\Db\\Adapter\\Platform\\PlatformInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Platform/PlatformInterface.php', + 'Laminas\\Db\\Adapter\\Platform\\Postgresql' => $vendorDir . '/laminas/laminas-db/src/Adapter/Platform/Postgresql.php', + 'Laminas\\Db\\Adapter\\Platform\\Sql92' => $vendorDir . '/laminas/laminas-db/src/Adapter/Platform/Sql92.php', + 'Laminas\\Db\\Adapter\\Platform\\SqlServer' => $vendorDir . '/laminas/laminas-db/src/Adapter/Platform/SqlServer.php', + 'Laminas\\Db\\Adapter\\Platform\\Sqlite' => $vendorDir . '/laminas/laminas-db/src/Adapter/Platform/Sqlite.php', + 'Laminas\\Db\\Adapter\\Profiler\\Profiler' => $vendorDir . '/laminas/laminas-db/src/Adapter/Profiler/Profiler.php', + 'Laminas\\Db\\Adapter\\Profiler\\ProfilerAwareInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Profiler/ProfilerAwareInterface.php', + 'Laminas\\Db\\Adapter\\Profiler\\ProfilerInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/Profiler/ProfilerInterface.php', + 'Laminas\\Db\\Adapter\\StatementContainer' => $vendorDir . '/laminas/laminas-db/src/Adapter/StatementContainer.php', + 'Laminas\\Db\\Adapter\\StatementContainerInterface' => $vendorDir . '/laminas/laminas-db/src/Adapter/StatementContainerInterface.php', + 'Laminas\\Db\\ConfigProvider' => $vendorDir . '/laminas/laminas-db/src/ConfigProvider.php', + 'Laminas\\Db\\Exception\\ErrorException' => $vendorDir . '/laminas/laminas-db/src/Exception/ErrorException.php', + 'Laminas\\Db\\Exception\\ExceptionInterface' => $vendorDir . '/laminas/laminas-db/src/Exception/ExceptionInterface.php', + 'Laminas\\Db\\Exception\\InvalidArgumentException' => $vendorDir . '/laminas/laminas-db/src/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\Exception\\RuntimeException' => $vendorDir . '/laminas/laminas-db/src/Exception/RuntimeException.php', + 'Laminas\\Db\\Exception\\UnexpectedValueException' => $vendorDir . '/laminas/laminas-db/src/Exception/UnexpectedValueException.php', + 'Laminas\\Db\\Metadata\\Metadata' => $vendorDir . '/laminas/laminas-db/src/Metadata/Metadata.php', + 'Laminas\\Db\\Metadata\\MetadataInterface' => $vendorDir . '/laminas/laminas-db/src/Metadata/MetadataInterface.php', + 'Laminas\\Db\\Metadata\\Object\\AbstractTableObject' => $vendorDir . '/laminas/laminas-db/src/Metadata/Object/AbstractTableObject.php', + 'Laminas\\Db\\Metadata\\Object\\ColumnObject' => $vendorDir . '/laminas/laminas-db/src/Metadata/Object/ColumnObject.php', + 'Laminas\\Db\\Metadata\\Object\\ConstraintKeyObject' => $vendorDir . '/laminas/laminas-db/src/Metadata/Object/ConstraintKeyObject.php', + 'Laminas\\Db\\Metadata\\Object\\ConstraintObject' => $vendorDir . '/laminas/laminas-db/src/Metadata/Object/ConstraintObject.php', + 'Laminas\\Db\\Metadata\\Object\\TableObject' => $vendorDir . '/laminas/laminas-db/src/Metadata/Object/TableObject.php', + 'Laminas\\Db\\Metadata\\Object\\TriggerObject' => $vendorDir . '/laminas/laminas-db/src/Metadata/Object/TriggerObject.php', + 'Laminas\\Db\\Metadata\\Object\\ViewObject' => $vendorDir . '/laminas/laminas-db/src/Metadata/Object/ViewObject.php', + 'Laminas\\Db\\Metadata\\Source\\AbstractSource' => $vendorDir . '/laminas/laminas-db/src/Metadata/Source/AbstractSource.php', + 'Laminas\\Db\\Metadata\\Source\\Factory' => $vendorDir . '/laminas/laminas-db/src/Metadata/Source/Factory.php', + 'Laminas\\Db\\Metadata\\Source\\MysqlMetadata' => $vendorDir . '/laminas/laminas-db/src/Metadata/Source/MysqlMetadata.php', + 'Laminas\\Db\\Metadata\\Source\\OracleMetadata' => $vendorDir . '/laminas/laminas-db/src/Metadata/Source/OracleMetadata.php', + 'Laminas\\Db\\Metadata\\Source\\PostgresqlMetadata' => $vendorDir . '/laminas/laminas-db/src/Metadata/Source/PostgresqlMetadata.php', + 'Laminas\\Db\\Metadata\\Source\\SqlServerMetadata' => $vendorDir . '/laminas/laminas-db/src/Metadata/Source/SqlServerMetadata.php', + 'Laminas\\Db\\Metadata\\Source\\SqliteMetadata' => $vendorDir . '/laminas/laminas-db/src/Metadata/Source/SqliteMetadata.php', + 'Laminas\\Db\\Module' => $vendorDir . '/laminas/laminas-db/src/Module.php', + 'Laminas\\Db\\ResultSet\\AbstractResultSet' => $vendorDir . '/laminas/laminas-db/src/ResultSet/AbstractResultSet.php', + 'Laminas\\Db\\ResultSet\\Exception\\ExceptionInterface' => $vendorDir . '/laminas/laminas-db/src/ResultSet/Exception/ExceptionInterface.php', + 'Laminas\\Db\\ResultSet\\Exception\\InvalidArgumentException' => $vendorDir . '/laminas/laminas-db/src/ResultSet/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\ResultSet\\Exception\\RuntimeException' => $vendorDir . '/laminas/laminas-db/src/ResultSet/Exception/RuntimeException.php', + 'Laminas\\Db\\ResultSet\\HydratingResultSet' => $vendorDir . '/laminas/laminas-db/src/ResultSet/HydratingResultSet.php', + 'Laminas\\Db\\ResultSet\\ResultSet' => $vendorDir . '/laminas/laminas-db/src/ResultSet/ResultSet.php', + 'Laminas\\Db\\ResultSet\\ResultSetInterface' => $vendorDir . '/laminas/laminas-db/src/ResultSet/ResultSetInterface.php', + 'Laminas\\Db\\RowGateway\\AbstractRowGateway' => $vendorDir . '/laminas/laminas-db/src/RowGateway/AbstractRowGateway.php', + 'Laminas\\Db\\RowGateway\\Exception\\ExceptionInterface' => $vendorDir . '/laminas/laminas-db/src/RowGateway/Exception/ExceptionInterface.php', + 'Laminas\\Db\\RowGateway\\Exception\\InvalidArgumentException' => $vendorDir . '/laminas/laminas-db/src/RowGateway/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\RowGateway\\Exception\\RuntimeException' => $vendorDir . '/laminas/laminas-db/src/RowGateway/Exception/RuntimeException.php', + 'Laminas\\Db\\RowGateway\\Feature\\AbstractFeature' => $vendorDir . '/laminas/laminas-db/src/RowGateway/Feature/AbstractFeature.php', + 'Laminas\\Db\\RowGateway\\Feature\\FeatureSet' => $vendorDir . '/laminas/laminas-db/src/RowGateway/Feature/FeatureSet.php', + 'Laminas\\Db\\RowGateway\\RowGateway' => $vendorDir . '/laminas/laminas-db/src/RowGateway/RowGateway.php', + 'Laminas\\Db\\RowGateway\\RowGatewayInterface' => $vendorDir . '/laminas/laminas-db/src/RowGateway/RowGatewayInterface.php', + 'Laminas\\Db\\Sql\\AbstractExpression' => $vendorDir . '/laminas/laminas-db/src/Sql/AbstractExpression.php', + 'Laminas\\Db\\Sql\\AbstractPreparableSql' => $vendorDir . '/laminas/laminas-db/src/Sql/AbstractPreparableSql.php', + 'Laminas\\Db\\Sql\\AbstractSql' => $vendorDir . '/laminas/laminas-db/src/Sql/AbstractSql.php', + 'Laminas\\Db\\Sql\\Combine' => $vendorDir . '/laminas/laminas-db/src/Sql/Combine.php', + 'Laminas\\Db\\Sql\\Ddl\\AlterTable' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/AlterTable.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\AbstractLengthColumn' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/AbstractLengthColumn.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\AbstractPrecisionColumn' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/AbstractPrecisionColumn.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\AbstractTimestampColumn' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/AbstractTimestampColumn.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\BigInteger' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/BigInteger.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Binary' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Binary.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Blob' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Blob.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Boolean' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Boolean.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Char' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Char.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Column' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Column.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\ColumnInterface' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/ColumnInterface.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Date' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Date.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Datetime' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Datetime.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Decimal' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Decimal.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Floating' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Floating.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Integer' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Integer.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Text' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Text.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Time' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Time.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Timestamp' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Timestamp.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Varbinary' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Varbinary.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Varchar' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Column/Varchar.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\AbstractConstraint' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Constraint/AbstractConstraint.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\Check' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Constraint/Check.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\ConstraintInterface' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Constraint/ConstraintInterface.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\ForeignKey' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Constraint/ForeignKey.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\PrimaryKey' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Constraint/PrimaryKey.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\UniqueKey' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Constraint/UniqueKey.php', + 'Laminas\\Db\\Sql\\Ddl\\CreateTable' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/CreateTable.php', + 'Laminas\\Db\\Sql\\Ddl\\DropTable' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/DropTable.php', + 'Laminas\\Db\\Sql\\Ddl\\Index\\AbstractIndex' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Index/AbstractIndex.php', + 'Laminas\\Db\\Sql\\Ddl\\Index\\Index' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/Index/Index.php', + 'Laminas\\Db\\Sql\\Ddl\\SqlInterface' => $vendorDir . '/laminas/laminas-db/src/Sql/Ddl/SqlInterface.php', + 'Laminas\\Db\\Sql\\Delete' => $vendorDir . '/laminas/laminas-db/src/Sql/Delete.php', + 'Laminas\\Db\\Sql\\Exception\\ExceptionInterface' => $vendorDir . '/laminas/laminas-db/src/Sql/Exception/ExceptionInterface.php', + 'Laminas\\Db\\Sql\\Exception\\InvalidArgumentException' => $vendorDir . '/laminas/laminas-db/src/Sql/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\Sql\\Exception\\RuntimeException' => $vendorDir . '/laminas/laminas-db/src/Sql/Exception/RuntimeException.php', + 'Laminas\\Db\\Sql\\Expression' => $vendorDir . '/laminas/laminas-db/src/Sql/Expression.php', + 'Laminas\\Db\\Sql\\ExpressionInterface' => $vendorDir . '/laminas/laminas-db/src/Sql/ExpressionInterface.php', + 'Laminas\\Db\\Sql\\Having' => $vendorDir . '/laminas/laminas-db/src/Sql/Having.php', + 'Laminas\\Db\\Sql\\Insert' => $vendorDir . '/laminas/laminas-db/src/Sql/Insert.php', + 'Laminas\\Db\\Sql\\InsertIgnore' => $vendorDir . '/laminas/laminas-db/src/Sql/InsertIgnore.php', + 'Laminas\\Db\\Sql\\Join' => $vendorDir . '/laminas/laminas-db/src/Sql/Join.php', + 'Laminas\\Db\\Sql\\Literal' => $vendorDir . '/laminas/laminas-db/src/Sql/Literal.php', + 'Laminas\\Db\\Sql\\Platform\\AbstractPlatform' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/AbstractPlatform.php', + 'Laminas\\Db\\Sql\\Platform\\IbmDb2\\IbmDb2' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/IbmDb2/IbmDb2.php', + 'Laminas\\Db\\Sql\\Platform\\IbmDb2\\SelectDecorator' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/IbmDb2/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Mysql\\Ddl\\AlterTableDecorator' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Mysql\\Ddl\\CreateTableDecorator' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Mysql\\Mysql' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/Mysql/Mysql.php', + 'Laminas\\Db\\Sql\\Platform\\Mysql\\SelectDecorator' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/Mysql/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Oracle\\Oracle' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/Oracle/Oracle.php', + 'Laminas\\Db\\Sql\\Platform\\Oracle\\SelectDecorator' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/Oracle/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Platform' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/Platform.php', + 'Laminas\\Db\\Sql\\Platform\\PlatformDecoratorInterface' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/PlatformDecoratorInterface.php', + 'Laminas\\Db\\Sql\\Platform\\SqlServer\\Ddl\\CreateTableDecorator' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/SqlServer/Ddl/CreateTableDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\SqlServer\\SelectDecorator' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/SqlServer/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\SqlServer\\SqlServer' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/SqlServer/SqlServer.php', + 'Laminas\\Db\\Sql\\Platform\\Sqlite\\SelectDecorator' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/Sqlite/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Sqlite\\Sqlite' => $vendorDir . '/laminas/laminas-db/src/Sql/Platform/Sqlite/Sqlite.php', + 'Laminas\\Db\\Sql\\Predicate\\Between' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/Between.php', + 'Laminas\\Db\\Sql\\Predicate\\Expression' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/Expression.php', + 'Laminas\\Db\\Sql\\Predicate\\In' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/In.php', + 'Laminas\\Db\\Sql\\Predicate\\IsNotNull' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/IsNotNull.php', + 'Laminas\\Db\\Sql\\Predicate\\IsNull' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/IsNull.php', + 'Laminas\\Db\\Sql\\Predicate\\Like' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/Like.php', + 'Laminas\\Db\\Sql\\Predicate\\Literal' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/Literal.php', + 'Laminas\\Db\\Sql\\Predicate\\NotBetween' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/NotBetween.php', + 'Laminas\\Db\\Sql\\Predicate\\NotIn' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/NotIn.php', + 'Laminas\\Db\\Sql\\Predicate\\NotLike' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/NotLike.php', + 'Laminas\\Db\\Sql\\Predicate\\Operator' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/Operator.php', + 'Laminas\\Db\\Sql\\Predicate\\Predicate' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/Predicate.php', + 'Laminas\\Db\\Sql\\Predicate\\PredicateInterface' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/PredicateInterface.php', + 'Laminas\\Db\\Sql\\Predicate\\PredicateSet' => $vendorDir . '/laminas/laminas-db/src/Sql/Predicate/PredicateSet.php', + 'Laminas\\Db\\Sql\\PreparableSqlInterface' => $vendorDir . '/laminas/laminas-db/src/Sql/PreparableSqlInterface.php', + 'Laminas\\Db\\Sql\\Select' => $vendorDir . '/laminas/laminas-db/src/Sql/Select.php', + 'Laminas\\Db\\Sql\\Sql' => $vendorDir . '/laminas/laminas-db/src/Sql/Sql.php', + 'Laminas\\Db\\Sql\\SqlInterface' => $vendorDir . '/laminas/laminas-db/src/Sql/SqlInterface.php', + 'Laminas\\Db\\Sql\\TableIdentifier' => $vendorDir . '/laminas/laminas-db/src/Sql/TableIdentifier.php', + 'Laminas\\Db\\Sql\\Update' => $vendorDir . '/laminas/laminas-db/src/Sql/Update.php', + 'Laminas\\Db\\Sql\\Where' => $vendorDir . '/laminas/laminas-db/src/Sql/Where.php', + 'Laminas\\Db\\TableGateway\\AbstractTableGateway' => $vendorDir . '/laminas/laminas-db/src/TableGateway/AbstractTableGateway.php', + 'Laminas\\Db\\TableGateway\\Exception\\ExceptionInterface' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Exception/ExceptionInterface.php', + 'Laminas\\Db\\TableGateway\\Exception\\InvalidArgumentException' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\TableGateway\\Exception\\RuntimeException' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Exception/RuntimeException.php', + 'Laminas\\Db\\TableGateway\\Feature\\AbstractFeature' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/AbstractFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\EventFeature' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/EventFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\EventFeatureEventsInterface' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/EventFeatureEventsInterface.php', + 'Laminas\\Db\\TableGateway\\Feature\\EventFeature\\TableGatewayEvent' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php', + 'Laminas\\Db\\TableGateway\\Feature\\FeatureSet' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/FeatureSet.php', + 'Laminas\\Db\\TableGateway\\Feature\\GlobalAdapterFeature' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/GlobalAdapterFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\MasterSlaveFeature' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/MasterSlaveFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\MetadataFeature' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/MetadataFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\RowGatewayFeature' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/RowGatewayFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\SequenceFeature' => $vendorDir . '/laminas/laminas-db/src/TableGateway/Feature/SequenceFeature.php', + 'Laminas\\Db\\TableGateway\\TableGateway' => $vendorDir . '/laminas/laminas-db/src/TableGateway/TableGateway.php', + 'Laminas\\Db\\TableGateway\\TableGatewayInterface' => $vendorDir . '/laminas/laminas-db/src/TableGateway/TableGatewayInterface.php', + 'Laminas\\Stdlib\\AbstractOptions' => $vendorDir . '/laminas/laminas-stdlib/src/AbstractOptions.php', + 'Laminas\\Stdlib\\ArrayObject' => $vendorDir . '/laminas/laminas-stdlib/src/ArrayObject.php', + 'Laminas\\Stdlib\\ArraySerializableInterface' => $vendorDir . '/laminas/laminas-stdlib/src/ArraySerializableInterface.php', + 'Laminas\\Stdlib\\ArrayStack' => $vendorDir . '/laminas/laminas-stdlib/src/ArrayStack.php', + 'Laminas\\Stdlib\\ArrayUtils' => $vendorDir . '/laminas/laminas-stdlib/src/ArrayUtils.php', + 'Laminas\\Stdlib\\ArrayUtils\\MergeRemoveKey' => $vendorDir . '/laminas/laminas-stdlib/src/ArrayUtils/MergeRemoveKey.php', + 'Laminas\\Stdlib\\ArrayUtils\\MergeReplaceKey' => $vendorDir . '/laminas/laminas-stdlib/src/ArrayUtils/MergeReplaceKey.php', + 'Laminas\\Stdlib\\ArrayUtils\\MergeReplaceKeyInterface' => $vendorDir . '/laminas/laminas-stdlib/src/ArrayUtils/MergeReplaceKeyInterface.php', + 'Laminas\\Stdlib\\ConsoleHelper' => $vendorDir . '/laminas/laminas-stdlib/src/ConsoleHelper.php', + 'Laminas\\Stdlib\\DispatchableInterface' => $vendorDir . '/laminas/laminas-stdlib/src/DispatchableInterface.php', + 'Laminas\\Stdlib\\ErrorHandler' => $vendorDir . '/laminas/laminas-stdlib/src/ErrorHandler.php', + 'Laminas\\Stdlib\\Exception\\BadMethodCallException' => $vendorDir . '/laminas/laminas-stdlib/src/Exception/BadMethodCallException.php', + 'Laminas\\Stdlib\\Exception\\DomainException' => $vendorDir . '/laminas/laminas-stdlib/src/Exception/DomainException.php', + 'Laminas\\Stdlib\\Exception\\ExceptionInterface' => $vendorDir . '/laminas/laminas-stdlib/src/Exception/ExceptionInterface.php', + 'Laminas\\Stdlib\\Exception\\ExtensionNotLoadedException' => $vendorDir . '/laminas/laminas-stdlib/src/Exception/ExtensionNotLoadedException.php', + 'Laminas\\Stdlib\\Exception\\InvalidArgumentException' => $vendorDir . '/laminas/laminas-stdlib/src/Exception/InvalidArgumentException.php', + 'Laminas\\Stdlib\\Exception\\LogicException' => $vendorDir . '/laminas/laminas-stdlib/src/Exception/LogicException.php', + 'Laminas\\Stdlib\\Exception\\RuntimeException' => $vendorDir . '/laminas/laminas-stdlib/src/Exception/RuntimeException.php', + 'Laminas\\Stdlib\\FastPriorityQueue' => $vendorDir . '/laminas/laminas-stdlib/src/FastPriorityQueue.php', + 'Laminas\\Stdlib\\Glob' => $vendorDir . '/laminas/laminas-stdlib/src/Glob.php', + 'Laminas\\Stdlib\\Guard\\AllGuardsTrait' => $vendorDir . '/laminas/laminas-stdlib/src/Guard/AllGuardsTrait.php', + 'Laminas\\Stdlib\\Guard\\ArrayOrTraversableGuardTrait' => $vendorDir . '/laminas/laminas-stdlib/src/Guard/ArrayOrTraversableGuardTrait.php', + 'Laminas\\Stdlib\\Guard\\EmptyGuardTrait' => $vendorDir . '/laminas/laminas-stdlib/src/Guard/EmptyGuardTrait.php', + 'Laminas\\Stdlib\\Guard\\NullGuardTrait' => $vendorDir . '/laminas/laminas-stdlib/src/Guard/NullGuardTrait.php', + 'Laminas\\Stdlib\\InitializableInterface' => $vendorDir . '/laminas/laminas-stdlib/src/InitializableInterface.php', + 'Laminas\\Stdlib\\JsonSerializable' => $vendorDir . '/laminas/laminas-stdlib/src/JsonSerializable.php', + 'Laminas\\Stdlib\\Message' => $vendorDir . '/laminas/laminas-stdlib/src/Message.php', + 'Laminas\\Stdlib\\MessageInterface' => $vendorDir . '/laminas/laminas-stdlib/src/MessageInterface.php', + 'Laminas\\Stdlib\\ParameterObjectInterface' => $vendorDir . '/laminas/laminas-stdlib/src/ParameterObjectInterface.php', + 'Laminas\\Stdlib\\Parameters' => $vendorDir . '/laminas/laminas-stdlib/src/Parameters.php', + 'Laminas\\Stdlib\\ParametersInterface' => $vendorDir . '/laminas/laminas-stdlib/src/ParametersInterface.php', + 'Laminas\\Stdlib\\PriorityList' => $vendorDir . '/laminas/laminas-stdlib/src/PriorityList.php', + 'Laminas\\Stdlib\\PriorityQueue' => $vendorDir . '/laminas/laminas-stdlib/src/PriorityQueue.php', + 'Laminas\\Stdlib\\Request' => $vendorDir . '/laminas/laminas-stdlib/src/Request.php', + 'Laminas\\Stdlib\\RequestInterface' => $vendorDir . '/laminas/laminas-stdlib/src/RequestInterface.php', + 'Laminas\\Stdlib\\Response' => $vendorDir . '/laminas/laminas-stdlib/src/Response.php', + 'Laminas\\Stdlib\\ResponseInterface' => $vendorDir . '/laminas/laminas-stdlib/src/ResponseInterface.php', + 'Laminas\\Stdlib\\SplPriorityQueue' => $vendorDir . '/laminas/laminas-stdlib/src/SplPriorityQueue.php', + 'Laminas\\Stdlib\\SplQueue' => $vendorDir . '/laminas/laminas-stdlib/src/SplQueue.php', + 'Laminas\\Stdlib\\SplStack' => $vendorDir . '/laminas/laminas-stdlib/src/SplStack.php', + 'Laminas\\Stdlib\\StringUtils' => $vendorDir . '/laminas/laminas-stdlib/src/StringUtils.php', + 'Laminas\\Stdlib\\StringWrapper\\AbstractStringWrapper' => $vendorDir . '/laminas/laminas-stdlib/src/StringWrapper/AbstractStringWrapper.php', + 'Laminas\\Stdlib\\StringWrapper\\Iconv' => $vendorDir . '/laminas/laminas-stdlib/src/StringWrapper/Iconv.php', + 'Laminas\\Stdlib\\StringWrapper\\Intl' => $vendorDir . '/laminas/laminas-stdlib/src/StringWrapper/Intl.php', + 'Laminas\\Stdlib\\StringWrapper\\MbString' => $vendorDir . '/laminas/laminas-stdlib/src/StringWrapper/MbString.php', + 'Laminas\\Stdlib\\StringWrapper\\Native' => $vendorDir . '/laminas/laminas-stdlib/src/StringWrapper/Native.php', + 'Laminas\\Stdlib\\StringWrapper\\StringWrapperInterface' => $vendorDir . '/laminas/laminas-stdlib/src/StringWrapper/StringWrapperInterface.php', + 'Laminas\\ZendFrameworkBridge\\Autoloader' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/Autoloader.php', + 'Laminas\\ZendFrameworkBridge\\ConfigPostProcessor' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php', + 'Laminas\\ZendFrameworkBridge\\Module' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/Module.php', + 'Laminas\\ZendFrameworkBridge\\Replacements' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/Replacements.php', + 'Laminas\\ZendFrameworkBridge\\RewriteRules' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/RewriteRules.php', + 'Microsoft\\PhpParser\\CharacterCodes' => $vendorDir . '/microsoft/tolerant-php-parser/src/CharacterCodes.php', + 'Microsoft\\PhpParser\\ClassLike' => $vendorDir . '/microsoft/tolerant-php-parser/src/ClassLike.php', + 'Microsoft\\PhpParser\\Diagnostic' => $vendorDir . '/microsoft/tolerant-php-parser/src/Diagnostic.php', + 'Microsoft\\PhpParser\\DiagnosticKind' => $vendorDir . '/microsoft/tolerant-php-parser/src/DiagnosticKind.php', + 'Microsoft\\PhpParser\\DiagnosticsProvider' => $vendorDir . '/microsoft/tolerant-php-parser/src/DiagnosticsProvider.php', + 'Microsoft\\PhpParser\\FilePositionMap' => $vendorDir . '/microsoft/tolerant-php-parser/src/FilePositionMap.php', + 'Microsoft\\PhpParser\\FunctionLike' => $vendorDir . '/microsoft/tolerant-php-parser/src/FunctionLike.php', + 'Microsoft\\PhpParser\\LineCharacterPosition' => $vendorDir . '/microsoft/tolerant-php-parser/src/LineCharacterPosition.php', + 'Microsoft\\PhpParser\\MissingToken' => $vendorDir . '/microsoft/tolerant-php-parser/src/MissingToken.php', + 'Microsoft\\PhpParser\\ModifiedTypeInterface' => $vendorDir . '/microsoft/tolerant-php-parser/src/ModifiedTypeInterface.php', + 'Microsoft\\PhpParser\\ModifiedTypeTrait' => $vendorDir . '/microsoft/tolerant-php-parser/src/ModifiedTypeTrait.php', + 'Microsoft\\PhpParser\\NamespacedNameInterface' => $vendorDir . '/microsoft/tolerant-php-parser/src/NamespacedNameInterface.php', + 'Microsoft\\PhpParser\\NamespacedNameTrait' => $vendorDir . '/microsoft/tolerant-php-parser/src/NamespacedNameTrait.php', + 'Microsoft\\PhpParser\\Node' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node.php', + 'Microsoft\\PhpParser\\Node\\AnonymousFunctionUseClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/AnonymousFunctionUseClause.php', + 'Microsoft\\PhpParser\\Node\\ArrayElement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ArrayElement.php', + 'Microsoft\\PhpParser\\Node\\Attribute' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Attribute.php', + 'Microsoft\\PhpParser\\Node\\AttributeGroup' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/AttributeGroup.php', + 'Microsoft\\PhpParser\\Node\\CaseStatementNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/CaseStatementNode.php', + 'Microsoft\\PhpParser\\Node\\CatchClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/CatchClause.php', + 'Microsoft\\PhpParser\\Node\\ClassBaseClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ClassBaseClause.php', + 'Microsoft\\PhpParser\\Node\\ClassConstDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ClassConstDeclaration.php', + 'Microsoft\\PhpParser\\Node\\ClassInterfaceClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ClassInterfaceClause.php', + 'Microsoft\\PhpParser\\Node\\ClassMembersNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ClassMembersNode.php', + 'Microsoft\\PhpParser\\Node\\ConstElement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ConstElement.php', + 'Microsoft\\PhpParser\\Node\\DeclareDirective' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DeclareDirective.php', + 'Microsoft\\PhpParser\\Node\\DefaultStatementNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DefaultStatementNode.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ArgumentExpressionList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ArgumentExpressionList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ArrayElementList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ArrayElementList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\AttributeElementList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/AttributeElementList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ConstElementList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ConstElementList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\DeclareDirectiveList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/DeclareDirectiveList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ExpressionList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ExpressionList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ListExpressionList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ListExpressionList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\MatchArmConditionList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/MatchArmConditionList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\MatchExpressionArmList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/MatchExpressionArmList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\NamespaceUseClauseList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/NamespaceUseClauseList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\NamespaceUseGroupClauseList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/NamespaceUseGroupClauseList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ParameterDeclarationList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ParameterDeclarationList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\QualifiedNameList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/QualifiedNameList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\QualifiedNameParts' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/QualifiedNameParts.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\StaticVariableNameList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/StaticVariableNameList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\TraitSelectOrAliasClauseList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/TraitSelectOrAliasClauseList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\UseVariableNameList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/UseVariableNameList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\VariableNameList' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/VariableNameList.php', + 'Microsoft\\PhpParser\\Node\\ElseClauseNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ElseClauseNode.php', + 'Microsoft\\PhpParser\\Node\\ElseIfClauseNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ElseIfClauseNode.php', + 'Microsoft\\PhpParser\\Node\\Expression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\AnonymousFunctionCreationExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/AnonymousFunctionCreationExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ArgumentExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ArgumentExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ArrayCreationExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ArrayCreationExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ArrowFunctionCreationExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ArrowFunctionCreationExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\AssignmentExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/AssignmentExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\BinaryExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/BinaryExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\BracedExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/BracedExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\CallExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/CallExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\CastExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/CastExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\CloneExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/CloneExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\EchoExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/EchoExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\EmptyIntrinsicExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/EmptyIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ErrorControlExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ErrorControlExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\EvalIntrinsicExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/EvalIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ExitIntrinsicExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ExitIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\IssetIntrinsicExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/IssetIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ListIntrinsicExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ListIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\MatchExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/MatchExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\MemberAccessExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/MemberAccessExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ObjectCreationExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ObjectCreationExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ParenthesizedExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ParenthesizedExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\PostfixUpdateExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/PostfixUpdateExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\PrefixUpdateExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/PrefixUpdateExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\PrintIntrinsicExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/PrintIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ScopedPropertyAccessExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ScopedPropertyAccessExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ScriptInclusionExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ScriptInclusionExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\SubscriptExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/SubscriptExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\TernaryExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/TernaryExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ThrowExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/ThrowExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\UnaryExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/UnaryExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\UnaryOpExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/UnaryOpExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\UnsetIntrinsicExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/UnsetIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\Variable' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/Variable.php', + 'Microsoft\\PhpParser\\Node\\Expression\\YieldExpression' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Expression/YieldExpression.php', + 'Microsoft\\PhpParser\\Node\\FinallyClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/FinallyClause.php', + 'Microsoft\\PhpParser\\Node\\ForeachKey' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ForeachKey.php', + 'Microsoft\\PhpParser\\Node\\ForeachValue' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ForeachValue.php', + 'Microsoft\\PhpParser\\Node\\FunctionBody' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/FunctionBody.php', + 'Microsoft\\PhpParser\\Node\\FunctionHeader' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/FunctionHeader.php', + 'Microsoft\\PhpParser\\Node\\FunctionReturnType' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/FunctionReturnType.php', + 'Microsoft\\PhpParser\\Node\\FunctionUseClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/FunctionUseClause.php', + 'Microsoft\\PhpParser\\Node\\InterfaceBaseClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/InterfaceBaseClause.php', + 'Microsoft\\PhpParser\\Node\\InterfaceMembers' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/InterfaceMembers.php', + 'Microsoft\\PhpParser\\Node\\MatchArm' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/MatchArm.php', + 'Microsoft\\PhpParser\\Node\\MethodDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/MethodDeclaration.php', + 'Microsoft\\PhpParser\\Node\\MissingDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/MissingDeclaration.php', + 'Microsoft\\PhpParser\\Node\\MissingMemberDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/MissingMemberDeclaration.php', + 'Microsoft\\PhpParser\\Node\\NamespaceAliasingClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/NamespaceAliasingClause.php', + 'Microsoft\\PhpParser\\Node\\NamespaceUseClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/NamespaceUseClause.php', + 'Microsoft\\PhpParser\\Node\\NamespaceUseGroupClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/NamespaceUseGroupClause.php', + 'Microsoft\\PhpParser\\Node\\NumericLiteral' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/NumericLiteral.php', + 'Microsoft\\PhpParser\\Node\\Parameter' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Parameter.php', + 'Microsoft\\PhpParser\\Node\\PropertyDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/PropertyDeclaration.php', + 'Microsoft\\PhpParser\\Node\\QualifiedName' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/QualifiedName.php', + 'Microsoft\\PhpParser\\Node\\RelativeSpecifier' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/RelativeSpecifier.php', + 'Microsoft\\PhpParser\\Node\\ReservedWord' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/ReservedWord.php', + 'Microsoft\\PhpParser\\Node\\SourceFileNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/SourceFileNode.php', + 'Microsoft\\PhpParser\\Node\\StatementNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/StatementNode.php', + 'Microsoft\\PhpParser\\Node\\Statement\\BreakOrContinueStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/BreakOrContinueStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ClassDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/ClassDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\CompoundStatementNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/CompoundStatementNode.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ConstDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/ConstDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\DeclareStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/DeclareStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\DoStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/DoStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\EmptyStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/EmptyStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ExpressionStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/ExpressionStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ForStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/ForStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ForeachStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/ForeachStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\FunctionDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/FunctionDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\FunctionStaticDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/FunctionStaticDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\GlobalDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/GlobalDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\GotoStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/GotoStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\IfStatementNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/IfStatementNode.php', + 'Microsoft\\PhpParser\\Node\\Statement\\InlineHtml' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/InlineHtml.php', + 'Microsoft\\PhpParser\\Node\\Statement\\InterfaceDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/InterfaceDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\NamedLabelStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/NamedLabelStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\NamespaceDefinition' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/NamespaceDefinition.php', + 'Microsoft\\PhpParser\\Node\\Statement\\NamespaceUseDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/NamespaceUseDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ReturnStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/ReturnStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\SwitchStatementNode' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/SwitchStatementNode.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ThrowStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/ThrowStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\TraitDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/TraitDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\TryStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/TryStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\WhileStatement' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/Statement/WhileStatement.php', + 'Microsoft\\PhpParser\\Node\\StaticVariableDeclaration' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/StaticVariableDeclaration.php', + 'Microsoft\\PhpParser\\Node\\StringLiteral' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/StringLiteral.php', + 'Microsoft\\PhpParser\\Node\\TraitMembers' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/TraitMembers.php', + 'Microsoft\\PhpParser\\Node\\TraitSelectOrAliasClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/TraitSelectOrAliasClause.php', + 'Microsoft\\PhpParser\\Node\\TraitUseClause' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/TraitUseClause.php', + 'Microsoft\\PhpParser\\Node\\UseVariableName' => $vendorDir . '/microsoft/tolerant-php-parser/src/Node/UseVariableName.php', + 'Microsoft\\PhpParser\\ParseContext' => $vendorDir . '/microsoft/tolerant-php-parser/src/ParseContext.php', + 'Microsoft\\PhpParser\\Parser' => $vendorDir . '/microsoft/tolerant-php-parser/src/Parser.php', + 'Microsoft\\PhpParser\\PhpTokenizer' => $vendorDir . '/microsoft/tolerant-php-parser/src/PhpTokenizer.php', + 'Microsoft\\PhpParser\\PositionUtilities' => $vendorDir . '/microsoft/tolerant-php-parser/src/PositionUtilities.php', + 'Microsoft\\PhpParser\\Range' => $vendorDir . '/microsoft/tolerant-php-parser/src/Range.php', + 'Microsoft\\PhpParser\\ResolvedName' => $vendorDir . '/microsoft/tolerant-php-parser/src/ResolvedName.php', + 'Microsoft\\PhpParser\\SkippedToken' => $vendorDir . '/microsoft/tolerant-php-parser/src/SkippedToken.php', + 'Microsoft\\PhpParser\\TextEdit' => $vendorDir . '/microsoft/tolerant-php-parser/src/TextEdit.php', + 'Microsoft\\PhpParser\\Token' => $vendorDir . '/microsoft/tolerant-php-parser/src/Token.php', + 'Microsoft\\PhpParser\\TokenKind' => $vendorDir . '/microsoft/tolerant-php-parser/src/TokenKind.php', + 'Microsoft\\PhpParser\\TokenStreamProviderFactory' => $vendorDir . '/microsoft/tolerant-php-parser/src/TokenStreamProviderFactory.php', + 'Microsoft\\PhpParser\\TokenStreamProviderInterface' => $vendorDir . '/microsoft/tolerant-php-parser/src/TokenStreamProviderInterface.php', + 'Microsoft\\PhpParser\\TokenStringMaps' => $vendorDir . '/microsoft/tolerant-php-parser/src/TokenStringMaps.php', + 'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', + 'Phan\\AST\\ASTHasher' => $vendorDir . '/phan/phan/src/Phan/AST/ASTHasher.php', + 'Phan\\AST\\ASTReverter' => $vendorDir . '/phan/phan/src/Phan/AST/ASTReverter.php', + 'Phan\\AST\\ASTSimplifier' => $vendorDir . '/phan/phan/src/Phan/AST/ASTSimplifier.php', + 'Phan\\AST\\AnalysisVisitor' => $vendorDir . '/phan/phan/src/Phan/AST/AnalysisVisitor.php', + 'Phan\\AST\\ArrowFunc' => $vendorDir . '/phan/phan/src/Phan/AST/ArrowFunc.php', + 'Phan\\AST\\ContextNode' => $vendorDir . '/phan/phan/src/Phan/AST/ContextNode.php', + 'Phan\\AST\\FallbackUnionTypeVisitor' => $vendorDir . '/phan/phan/src/Phan/AST/FallbackUnionTypeVisitor.php', + 'Phan\\AST\\InferPureSnippetVisitor' => $vendorDir . '/phan/phan/src/Phan/AST/InferPureSnippetVisitor.php', + 'Phan\\AST\\InferPureVisitor' => $vendorDir . '/phan/phan/src/Phan/AST/InferPureVisitor.php', + 'Phan\\AST\\InferValue' => $vendorDir . '/phan/phan/src/Phan/AST/InferValue.php', + 'Phan\\AST\\Parser' => $vendorDir . '/phan/phan/src/Phan/AST/Parser.php', + 'Phan\\AST\\PhanAnnotationAdder' => $vendorDir . '/phan/phan/src/Phan/AST/PhanAnnotationAdder.php', + 'Phan\\AST\\ScopeImpactCheckingVisitor' => $vendorDir . '/phan/phan/src/Phan/AST/ScopeImpactCheckingVisitor.php', + 'Phan\\AST\\TolerantASTConverter\\CachingTolerantASTConverter' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/CachingTolerantASTConverter.php', + 'Phan\\AST\\TolerantASTConverter\\InvalidNodeException' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/InvalidNodeException.php', + 'Phan\\AST\\TolerantASTConverter\\NodeDumper' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/NodeDumper.php', + 'Phan\\AST\\TolerantASTConverter\\NodeUtils' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/NodeUtils.php', + 'Phan\\AST\\TolerantASTConverter\\ParseException' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/ParseException.php', + 'Phan\\AST\\TolerantASTConverter\\ParseResult' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/ParseResult.php', + 'Phan\\AST\\TolerantASTConverter\\PhpParserNodeEntry' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/PhpParserNodeEntry.php', + 'Phan\\AST\\TolerantASTConverter\\Shim' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/Shim.php', + 'Phan\\AST\\TolerantASTConverter\\ShimFunctions' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/ShimFunctions.php', + 'Phan\\AST\\TolerantASTConverter\\StringUtil' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/StringUtil.php', + 'Phan\\AST\\TolerantASTConverter\\TolerantASTConverter' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverter.php', + 'Phan\\AST\\TolerantASTConverter\\TolerantASTConverterPreservingOriginal' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterPreservingOriginal.php', + 'Phan\\AST\\TolerantASTConverter\\TolerantASTConverterWithNodeMapping' => $vendorDir . '/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterWithNodeMapping.php', + 'Phan\\AST\\UnionTypeVisitor' => $vendorDir . '/phan/phan/src/Phan/AST/UnionTypeVisitor.php', + 'Phan\\AST\\Visitor\\Element' => $vendorDir . '/phan/phan/src/Phan/AST/Visitor/Element.php', + 'Phan\\AST\\Visitor\\FlagVisitor' => $vendorDir . '/phan/phan/src/Phan/AST/Visitor/FlagVisitor.php', + 'Phan\\AST\\Visitor\\FlagVisitorImplementation' => $vendorDir . '/phan/phan/src/Phan/AST/Visitor/FlagVisitorImplementation.php', + 'Phan\\AST\\Visitor\\KindVisitor' => $vendorDir . '/phan/phan/src/Phan/AST/Visitor/KindVisitor.php', + 'Phan\\AST\\Visitor\\KindVisitorImplementation' => $vendorDir . '/phan/phan/src/Phan/AST/Visitor/KindVisitorImplementation.php', + 'Phan\\Analysis' => $vendorDir . '/phan/phan/src/Phan/Analysis.php', + 'Phan\\Analysis\\AbstractMethodAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/AbstractMethodAnalyzer.php', + 'Phan\\Analysis\\Analyzable' => $vendorDir . '/phan/phan/src/Phan/Analysis/Analyzable.php', + 'Phan\\Analysis\\ArgumentType' => $vendorDir . '/phan/phan/src/Phan/Analysis/ArgumentType.php', + 'Phan\\Analysis\\AssignOperatorAnalysisVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/AssignOperatorAnalysisVisitor.php', + 'Phan\\Analysis\\AssignOperatorFlagVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/AssignOperatorFlagVisitor.php', + 'Phan\\Analysis\\AssignmentVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/AssignmentVisitor.php', + 'Phan\\Analysis\\BinaryOperatorFlagVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/BinaryOperatorFlagVisitor.php', + 'Phan\\Analysis\\BlockExitStatusChecker' => $vendorDir . '/phan/phan/src/Phan/Analysis/BlockExitStatusChecker.php', + 'Phan\\Analysis\\ClassConstantTypesAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/ClassConstantTypesAnalyzer.php', + 'Phan\\Analysis\\ClassInheritanceAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/ClassInheritanceAnalyzer.php', + 'Phan\\Analysis\\CompositionAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/CompositionAnalyzer.php', + 'Phan\\Analysis\\ConditionVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitor.php', + 'Phan\\Analysis\\ConditionVisitorInterface' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitorInterface.php', + 'Phan\\Analysis\\ConditionVisitorUtil' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitorUtil.php', + 'Phan\\Analysis\\ConditionVisitor\\BinaryCondition' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitor/BinaryCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\ComparisonCondition' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitor/ComparisonCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\EqualsCondition' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitor/EqualsCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\HasTypeCondition' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitor/HasTypeCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\IdenticalCondition' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitor/IdenticalCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\NotEqualsCondition' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitor/NotEqualsCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\NotHasTypeCondition' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitor/NotHasTypeCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\NotIdenticalCondition' => $vendorDir . '/phan/phan/src/Phan/Analysis/ConditionVisitor/NotIdenticalCondition.php', + 'Phan\\Analysis\\ContextMergeVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/ContextMergeVisitor.php', + 'Phan\\Analysis\\DuplicateClassAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/DuplicateClassAnalyzer.php', + 'Phan\\Analysis\\DuplicateFunctionAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/DuplicateFunctionAnalyzer.php', + 'Phan\\Analysis\\FallbackMethodTypesVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/FallbackMethodTypesVisitor.php', + 'Phan\\Analysis\\GotoAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/GotoAnalyzer.php', + 'Phan\\Analysis\\LoopConditionVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/LoopConditionVisitor.php', + 'Phan\\Analysis\\NegatedConditionVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/NegatedConditionVisitor.php', + 'Phan\\Analysis\\ParameterTypesAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/ParameterTypesAnalyzer.php', + 'Phan\\Analysis\\ParentConstructorCalledAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/ParentConstructorCalledAnalyzer.php', + 'Phan\\Analysis\\PostOrderAnalysisVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/PostOrderAnalysisVisitor.php', + 'Phan\\Analysis\\PreOrderAnalysisVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/PreOrderAnalysisVisitor.php', + 'Phan\\Analysis\\PropertyTypesAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/PropertyTypesAnalyzer.php', + 'Phan\\Analysis\\ReachabilityChecker' => $vendorDir . '/phan/phan/src/Phan/Analysis/ReachabilityChecker.php', + 'Phan\\Analysis\\RedundantCondition' => $vendorDir . '/phan/phan/src/Phan/Analysis/RedundantCondition.php', + 'Phan\\Analysis\\ReferenceCountsAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/ReferenceCountsAnalyzer.php', + 'Phan\\Analysis\\RegexAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/RegexAnalyzer.php', + 'Phan\\Analysis\\ScopeVisitor' => $vendorDir . '/phan/phan/src/Phan/Analysis/ScopeVisitor.php', + 'Phan\\Analysis\\ThrowsTypesAnalyzer' => $vendorDir . '/phan/phan/src/Phan/Analysis/ThrowsTypesAnalyzer.php', + 'Phan\\BlockAnalysisVisitor' => $vendorDir . '/phan/phan/src/Phan/BlockAnalysisVisitor.php', + 'Phan\\CLI' => $vendorDir . '/phan/phan/src/Phan/CLI.php', + 'Phan\\CLIBuilder' => $vendorDir . '/phan/phan/src/Phan/CLIBuilder.php', + 'Phan\\CodeBase' => $vendorDir . '/phan/phan/src/Phan/CodeBase.php', + 'Phan\\CodeBase\\ClassMap' => $vendorDir . '/phan/phan/src/Phan/CodeBase/ClassMap.php', + 'Phan\\CodeBase\\UndoTracker' => $vendorDir . '/phan/phan/src/Phan/CodeBase/UndoTracker.php', + 'Phan\\Config' => $vendorDir . '/phan/phan/src/Phan/Config.php', + 'Phan\\Config\\InitializedSettings' => $vendorDir . '/phan/phan/src/Phan/Config/InitializedSettings.php', + 'Phan\\Config\\Initializer' => $vendorDir . '/phan/phan/src/Phan/Config/Initializer.php', + 'Phan\\Daemon' => $vendorDir . '/phan/phan/src/Phan/Daemon.php', + 'Phan\\Daemon\\ExitException' => $vendorDir . '/phan/phan/src/Phan/Daemon/ExitException.php', + 'Phan\\Daemon\\ParseRequest' => $vendorDir . '/phan/phan/src/Phan/Daemon/ParseRequest.php', + 'Phan\\Daemon\\Request' => $vendorDir . '/phan/phan/src/Phan/Daemon/Request.php', + 'Phan\\Daemon\\Transport\\CapturerResponder' => $vendorDir . '/phan/phan/src/Phan/Daemon/Transport/CapturerResponder.php', + 'Phan\\Daemon\\Transport\\Responder' => $vendorDir . '/phan/phan/src/Phan/Daemon/Transport/Responder.php', + 'Phan\\Daemon\\Transport\\StreamResponder' => $vendorDir . '/phan/phan/src/Phan/Daemon/Transport/StreamResponder.php', + 'Phan\\Debug' => $vendorDir . '/phan/phan/src/Phan/Debug.php', + 'Phan\\Debug\\DebugUnionType' => $vendorDir . '/phan/phan/src/Phan/Debug/DebugUnionType.php', + 'Phan\\Debug\\Frame' => $vendorDir . '/phan/phan/src/Phan/Debug/Frame.php', + 'Phan\\Debug\\SignalHandler' => $vendorDir . '/phan/phan/src/Phan/Debug/SignalHandler.php', + 'Phan\\Exception\\CodeBaseException' => $vendorDir . '/phan/phan/src/Phan/Exception/CodeBaseException.php', + 'Phan\\Exception\\EmptyFQSENException' => $vendorDir . '/phan/phan/src/Phan/Exception/EmptyFQSENException.php', + 'Phan\\Exception\\FQSENException' => $vendorDir . '/phan/phan/src/Phan/Exception/FQSENException.php', + 'Phan\\Exception\\InvalidFQSENException' => $vendorDir . '/phan/phan/src/Phan/Exception/InvalidFQSENException.php', + 'Phan\\Exception\\IssueException' => $vendorDir . '/phan/phan/src/Phan/Exception/IssueException.php', + 'Phan\\Exception\\NodeException' => $vendorDir . '/phan/phan/src/Phan/Exception/NodeException.php', + 'Phan\\Exception\\RecursionDepthException' => $vendorDir . '/phan/phan/src/Phan/Exception/RecursionDepthException.php', + 'Phan\\Exception\\UnanalyzableException' => $vendorDir . '/phan/phan/src/Phan/Exception/UnanalyzableException.php', + 'Phan\\Exception\\UnanalyzableMagicPropertyException' => $vendorDir . '/phan/phan/src/Phan/Exception/UnanalyzableMagicPropertyException.php', + 'Phan\\Exception\\UsageException' => $vendorDir . '/phan/phan/src/Phan/Exception/UsageException.php', + 'Phan\\ForkPool' => $vendorDir . '/phan/phan/src/Phan/ForkPool.php', + 'Phan\\ForkPool\\Progress' => $vendorDir . '/phan/phan/src/Phan/ForkPool/Progress.php', + 'Phan\\ForkPool\\Reader' => $vendorDir . '/phan/phan/src/Phan/ForkPool/Reader.php', + 'Phan\\ForkPool\\Writer' => $vendorDir . '/phan/phan/src/Phan/ForkPool/Writer.php', + 'Phan\\Issue' => $vendorDir . '/phan/phan/src/Phan/Issue.php', + 'Phan\\IssueFixSuggester' => $vendorDir . '/phan/phan/src/Phan/IssueFixSuggester.php', + 'Phan\\IssueInstance' => $vendorDir . '/phan/phan/src/Phan/IssueInstance.php', + 'Phan\\LanguageServer\\CachedHoverResponse' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/CachedHoverResponse.php', + 'Phan\\LanguageServer\\ClientHandler' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/ClientHandler.php', + 'Phan\\LanguageServer\\Client\\TextDocument' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Client/TextDocument.php', + 'Phan\\LanguageServer\\CompletionRequest' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/CompletionRequest.php', + 'Phan\\LanguageServer\\CompletionResolver' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/CompletionResolver.php', + 'Phan\\LanguageServer\\DefinitionResolver' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/DefinitionResolver.php', + 'Phan\\LanguageServer\\FileMapping' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/FileMapping.php', + 'Phan\\LanguageServer\\GoToDefinitionRequest' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/GoToDefinitionRequest.php', + 'Phan\\LanguageServer\\IdGenerator' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/IdGenerator.php', + 'Phan\\LanguageServer\\LanguageClient' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/LanguageClient.php', + 'Phan\\LanguageServer\\LanguageServer' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/LanguageServer.php', + 'Phan\\LanguageServer\\Logger' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Logger.php', + 'Phan\\LanguageServer\\NodeInfoRequest' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/NodeInfoRequest.php', + 'Phan\\LanguageServer\\ProtocolReader' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/ProtocolReader.php', + 'Phan\\LanguageServer\\ProtocolStreamReader' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/ProtocolStreamReader.php', + 'Phan\\LanguageServer\\ProtocolStreamWriter' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/ProtocolStreamWriter.php', + 'Phan\\LanguageServer\\ProtocolWriter' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/ProtocolWriter.php', + 'Phan\\LanguageServer\\Protocol\\ClientCapabilities' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/ClientCapabilities.php', + 'Phan\\LanguageServer\\Protocol\\CompletionContext' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionContext.php', + 'Phan\\LanguageServer\\Protocol\\CompletionItem' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionItem.php', + 'Phan\\LanguageServer\\Protocol\\CompletionItemKind' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionItemKind.php', + 'Phan\\LanguageServer\\Protocol\\CompletionList' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionList.php', + 'Phan\\LanguageServer\\Protocol\\CompletionOptions' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionOptions.php', + 'Phan\\LanguageServer\\Protocol\\CompletionTriggerKind' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionTriggerKind.php', + 'Phan\\LanguageServer\\Protocol\\Diagnostic' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/Diagnostic.php', + 'Phan\\LanguageServer\\Protocol\\DiagnosticSeverity' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/DiagnosticSeverity.php', + 'Phan\\LanguageServer\\Protocol\\FileChangeType' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/FileChangeType.php', + 'Phan\\LanguageServer\\Protocol\\FileEvent' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/FileEvent.php', + 'Phan\\LanguageServer\\Protocol\\Hover' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/Hover.php', + 'Phan\\LanguageServer\\Protocol\\InitializeResult' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/InitializeResult.php', + 'Phan\\LanguageServer\\Protocol\\Location' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/Location.php', + 'Phan\\LanguageServer\\Protocol\\MarkupContent' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/MarkupContent.php', + 'Phan\\LanguageServer\\Protocol\\Message' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/Message.php', + 'Phan\\LanguageServer\\Protocol\\Position' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/Position.php', + 'Phan\\LanguageServer\\Protocol\\Range' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/Range.php', + 'Phan\\LanguageServer\\Protocol\\SaveOptions' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/SaveOptions.php', + 'Phan\\LanguageServer\\Protocol\\ServerCapabilities' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/ServerCapabilities.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentContentChangeEvent' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentContentChangeEvent.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentIdentifier' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentIdentifier.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentItem' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentItem.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentSyncKind' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentSyncKind.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentSyncOptions' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentSyncOptions.php', + 'Phan\\LanguageServer\\Protocol\\VersionedTextDocumentIdentifier' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Protocol/VersionedTextDocumentIdentifier.php', + 'Phan\\LanguageServer\\Server\\TextDocument' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Server/TextDocument.php', + 'Phan\\LanguageServer\\Server\\Workspace' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Server/Workspace.php', + 'Phan\\LanguageServer\\Utils' => $vendorDir . '/phan/phan/src/Phan/LanguageServer/Utils.php', + 'Phan\\Language\\AnnotatedUnionType' => $vendorDir . '/phan/phan/src/Phan/Language/AnnotatedUnionType.php', + 'Phan\\Language\\Context' => $vendorDir . '/phan/phan/src/Phan/Language/Context.php', + 'Phan\\Language\\ElementContext' => $vendorDir . '/phan/phan/src/Phan/Language/ElementContext.php', + 'Phan\\Language\\Element\\AddressableElement' => $vendorDir . '/phan/phan/src/Phan/Language/Element/AddressableElement.php', + 'Phan\\Language\\Element\\AddressableElementInterface' => $vendorDir . '/phan/phan/src/Phan/Language/Element/AddressableElementInterface.php', + 'Phan\\Language\\Element\\ClassAliasRecord' => $vendorDir . '/phan/phan/src/Phan/Language/Element/ClassAliasRecord.php', + 'Phan\\Language\\Element\\ClassConstant' => $vendorDir . '/phan/phan/src/Phan/Language/Element/ClassConstant.php', + 'Phan\\Language\\Element\\ClassElement' => $vendorDir . '/phan/phan/src/Phan/Language/Element/ClassElement.php', + 'Phan\\Language\\Element\\Clazz' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Clazz.php', + 'Phan\\Language\\Element\\ClosedScopeElement' => $vendorDir . '/phan/phan/src/Phan/Language/Element/ClosedScopeElement.php', + 'Phan\\Language\\Element\\Comment' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Comment.php', + 'Phan\\Language\\Element\\Comment\\Assertion' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Comment/Assertion.php', + 'Phan\\Language\\Element\\Comment\\Builder' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Comment/Builder.php', + 'Phan\\Language\\Element\\Comment\\Method' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Comment/Method.php', + 'Phan\\Language\\Element\\Comment\\NullComment' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Comment/NullComment.php', + 'Phan\\Language\\Element\\Comment\\Parameter' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Comment/Parameter.php', + 'Phan\\Language\\Element\\Comment\\Property' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Comment/Property.php', + 'Phan\\Language\\Element\\Comment\\ReturnComment' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Comment/ReturnComment.php', + 'Phan\\Language\\Element\\ConstantInterface' => $vendorDir . '/phan/phan/src/Phan/Language/Element/ConstantInterface.php', + 'Phan\\Language\\Element\\ConstantTrait' => $vendorDir . '/phan/phan/src/Phan/Language/Element/ConstantTrait.php', + 'Phan\\Language\\Element\\ElementFutureUnionType' => $vendorDir . '/phan/phan/src/Phan/Language/Element/ElementFutureUnionType.php', + 'Phan\\Language\\Element\\Flags' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Flags.php', + 'Phan\\Language\\Element\\Func' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Func.php', + 'Phan\\Language\\Element\\FunctionFactory' => $vendorDir . '/phan/phan/src/Phan/Language/Element/FunctionFactory.php', + 'Phan\\Language\\Element\\FunctionInterface' => $vendorDir . '/phan/phan/src/Phan/Language/Element/FunctionInterface.php', + 'Phan\\Language\\Element\\FunctionTrait' => $vendorDir . '/phan/phan/src/Phan/Language/Element/FunctionTrait.php', + 'Phan\\Language\\Element\\GlobalConstant' => $vendorDir . '/phan/phan/src/Phan/Language/Element/GlobalConstant.php', + 'Phan\\Language\\Element\\MarkupDescription' => $vendorDir . '/phan/phan/src/Phan/Language/Element/MarkupDescription.php', + 'Phan\\Language\\Element\\Method' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Method.php', + 'Phan\\Language\\Element\\Parameter' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Parameter.php', + 'Phan\\Language\\Element\\PassByReferenceVariable' => $vendorDir . '/phan/phan/src/Phan/Language/Element/PassByReferenceVariable.php', + 'Phan\\Language\\Element\\Property' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Property.php', + 'Phan\\Language\\Element\\TraitAdaptations' => $vendorDir . '/phan/phan/src/Phan/Language/Element/TraitAdaptations.php', + 'Phan\\Language\\Element\\TraitAliasSource' => $vendorDir . '/phan/phan/src/Phan/Language/Element/TraitAliasSource.php', + 'Phan\\Language\\Element\\TypedElement' => $vendorDir . '/phan/phan/src/Phan/Language/Element/TypedElement.php', + 'Phan\\Language\\Element\\TypedElementInterface' => $vendorDir . '/phan/phan/src/Phan/Language/Element/TypedElementInterface.php', + 'Phan\\Language\\Element\\UnaddressableTypedElement' => $vendorDir . '/phan/phan/src/Phan/Language/Element/UnaddressableTypedElement.php', + 'Phan\\Language\\Element\\Variable' => $vendorDir . '/phan/phan/src/Phan/Language/Element/Variable.php', + 'Phan\\Language\\Element\\VariadicParameter' => $vendorDir . '/phan/phan/src/Phan/Language/Element/VariadicParameter.php', + 'Phan\\Language\\EmptyUnionType' => $vendorDir . '/phan/phan/src/Phan/Language/EmptyUnionType.php', + 'Phan\\Language\\FQSEN' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN.php', + 'Phan\\Language\\FQSEN\\AbstractFQSEN' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/AbstractFQSEN.php', + 'Phan\\Language\\FQSEN\\Alternatives' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/Alternatives.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedClassConstantName' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassConstantName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedClassElement' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassElement.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedClassName' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedConstantName' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedConstantName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedFunctionLikeName' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedFunctionLikeName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedFunctionName' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedFunctionName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedGlobalConstantName' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedGlobalConstantName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedGlobalStructuralElement' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedGlobalStructuralElement.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedMethodName' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedMethodName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedPropertyName' => $vendorDir . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedPropertyName.php', + 'Phan\\Language\\FileRef' => $vendorDir . '/phan/phan/src/Phan/Language/FileRef.php', + 'Phan\\Language\\FutureUnionType' => $vendorDir . '/phan/phan/src/Phan/Language/FutureUnionType.php', + 'Phan\\Language\\NamespaceMapEntry' => $vendorDir . '/phan/phan/src/Phan/Language/NamespaceMapEntry.php', + 'Phan\\Language\\Scope' => $vendorDir . '/phan/phan/src/Phan/Language/Scope.php', + 'Phan\\Language\\Scope\\BranchScope' => $vendorDir . '/phan/phan/src/Phan/Language/Scope/BranchScope.php', + 'Phan\\Language\\Scope\\ClassScope' => $vendorDir . '/phan/phan/src/Phan/Language/Scope/ClassScope.php', + 'Phan\\Language\\Scope\\ClosedScope' => $vendorDir . '/phan/phan/src/Phan/Language/Scope/ClosedScope.php', + 'Phan\\Language\\Scope\\ClosureScope' => $vendorDir . '/phan/phan/src/Phan/Language/Scope/ClosureScope.php', + 'Phan\\Language\\Scope\\FunctionLikeScope' => $vendorDir . '/phan/phan/src/Phan/Language/Scope/FunctionLikeScope.php', + 'Phan\\Language\\Scope\\GlobalScope' => $vendorDir . '/phan/phan/src/Phan/Language/Scope/GlobalScope.php', + 'Phan\\Language\\Scope\\PropertyScope' => $vendorDir . '/phan/phan/src/Phan/Language/Scope/PropertyScope.php', + 'Phan\\Language\\Scope\\TemplateScope' => $vendorDir . '/phan/phan/src/Phan/Language/Scope/TemplateScope.php', + 'Phan\\Language\\Type' => $vendorDir . '/phan/phan/src/Phan/Language/Type.php', + 'Phan\\Language\\Type\\ArrayShapeType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ArrayShapeType.php', + 'Phan\\Language\\Type\\ArrayType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ArrayType.php', + 'Phan\\Language\\Type\\AssociativeArrayType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/AssociativeArrayType.php', + 'Phan\\Language\\Type\\BoolType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/BoolType.php', + 'Phan\\Language\\Type\\CallableArrayType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/CallableArrayType.php', + 'Phan\\Language\\Type\\CallableDeclarationType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/CallableDeclarationType.php', + 'Phan\\Language\\Type\\CallableInterface' => $vendorDir . '/phan/phan/src/Phan/Language/Type/CallableInterface.php', + 'Phan\\Language\\Type\\CallableObjectType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/CallableObjectType.php', + 'Phan\\Language\\Type\\CallableStringType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/CallableStringType.php', + 'Phan\\Language\\Type\\CallableType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/CallableType.php', + 'Phan\\Language\\Type\\ClassStringType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ClassStringType.php', + 'Phan\\Language\\Type\\ClosureDeclarationParameter' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ClosureDeclarationParameter.php', + 'Phan\\Language\\Type\\ClosureDeclarationType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ClosureDeclarationType.php', + 'Phan\\Language\\Type\\ClosureType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ClosureType.php', + 'Phan\\Language\\Type\\FalseType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/FalseType.php', + 'Phan\\Language\\Type\\FloatType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/FloatType.php', + 'Phan\\Language\\Type\\FunctionLikeDeclarationType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/FunctionLikeDeclarationType.php', + 'Phan\\Language\\Type\\GenericArrayInterface' => $vendorDir . '/phan/phan/src/Phan/Language/Type/GenericArrayInterface.php', + 'Phan\\Language\\Type\\GenericArrayTemplateKeyType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/GenericArrayTemplateKeyType.php', + 'Phan\\Language\\Type\\GenericArrayType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/GenericArrayType.php', + 'Phan\\Language\\Type\\GenericIterableType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/GenericIterableType.php', + 'Phan\\Language\\Type\\GenericMultiArrayType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/GenericMultiArrayType.php', + 'Phan\\Language\\Type\\IntType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/IntType.php', + 'Phan\\Language\\Type\\IterableType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/IterableType.php', + 'Phan\\Language\\Type\\ListType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ListType.php', + 'Phan\\Language\\Type\\LiteralFloatType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/LiteralFloatType.php', + 'Phan\\Language\\Type\\LiteralIntType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/LiteralIntType.php', + 'Phan\\Language\\Type\\LiteralStringType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/LiteralStringType.php', + 'Phan\\Language\\Type\\LiteralTypeInterface' => $vendorDir . '/phan/phan/src/Phan/Language/Type/LiteralTypeInterface.php', + 'Phan\\Language\\Type\\MixedType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/MixedType.php', + 'Phan\\Language\\Type\\MultiType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/MultiType.php', + 'Phan\\Language\\Type\\NativeType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NativeType.php', + 'Phan\\Language\\Type\\NonEmptyArrayInterface' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NonEmptyArrayInterface.php', + 'Phan\\Language\\Type\\NonEmptyAssociativeArrayType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NonEmptyAssociativeArrayType.php', + 'Phan\\Language\\Type\\NonEmptyGenericArrayType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NonEmptyGenericArrayType.php', + 'Phan\\Language\\Type\\NonEmptyListType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NonEmptyListType.php', + 'Phan\\Language\\Type\\NonEmptyMixedType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NonEmptyMixedType.php', + 'Phan\\Language\\Type\\NonEmptyStringType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NonEmptyStringType.php', + 'Phan\\Language\\Type\\NonNullMixedType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NonNullMixedType.php', + 'Phan\\Language\\Type\\NonZeroIntType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NonZeroIntType.php', + 'Phan\\Language\\Type\\NullType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/NullType.php', + 'Phan\\Language\\Type\\ObjectType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ObjectType.php', + 'Phan\\Language\\Type\\ResourceType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ResourceType.php', + 'Phan\\Language\\Type\\ScalarRawType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ScalarRawType.php', + 'Phan\\Language\\Type\\ScalarType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/ScalarType.php', + 'Phan\\Language\\Type\\SelfType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/SelfType.php', + 'Phan\\Language\\Type\\StaticOrSelfType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/StaticOrSelfType.php', + 'Phan\\Language\\Type\\StaticType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/StaticType.php', + 'Phan\\Language\\Type\\StringType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/StringType.php', + 'Phan\\Language\\Type\\TemplateType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/TemplateType.php', + 'Phan\\Language\\Type\\TrueType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/TrueType.php', + 'Phan\\Language\\Type\\VoidType' => $vendorDir . '/phan/phan/src/Phan/Language/Type/VoidType.php', + 'Phan\\Language\\UnionType' => $vendorDir . '/phan/phan/src/Phan/Language/UnionType.php', + 'Phan\\Language\\UnionTypeBuilder' => $vendorDir . '/phan/phan/src/Phan/Language/UnionTypeBuilder.php', + 'Phan\\Library\\Cache' => $vendorDir . '/phan/phan/src/Phan/Library/Cache.php', + 'Phan\\Library\\ConversionSpec' => $vendorDir . '/phan/phan/src/Phan/Library/ConversionSpec.php', + 'Phan\\Library\\DiskCache' => $vendorDir . '/phan/phan/src/Phan/Library/DiskCache.php', + 'Phan\\Library\\FileCache' => $vendorDir . '/phan/phan/src/Phan/Library/FileCache.php', + 'Phan\\Library\\FileCacheEntry' => $vendorDir . '/phan/phan/src/Phan/Library/FileCacheEntry.php', + 'Phan\\Library\\Hasher' => $vendorDir . '/phan/phan/src/Phan/Library/Hasher.php', + 'Phan\\Library\\Hasher\\Consistent' => $vendorDir . '/phan/phan/src/Phan/Library/Hasher/Consistent.php', + 'Phan\\Library\\Hasher\\Sequential' => $vendorDir . '/phan/phan/src/Phan/Library/Hasher/Sequential.php', + 'Phan\\Library\\Map' => $vendorDir . '/phan/phan/src/Phan/Library/Map.php', + 'Phan\\Library\\None' => $vendorDir . '/phan/phan/src/Phan/Library/None.php', + 'Phan\\Library\\Option' => $vendorDir . '/phan/phan/src/Phan/Library/Option.php', + 'Phan\\Library\\Paths' => $vendorDir . '/phan/phan/src/Phan/Library/Paths.php', + 'Phan\\Library\\RAII' => $vendorDir . '/phan/phan/src/Phan/Library/RAII.php', + 'Phan\\Library\\RegexKeyExtractor' => $vendorDir . '/phan/phan/src/Phan/Library/RegexKeyExtractor.php', + 'Phan\\Library\\Restarter' => $vendorDir . '/phan/phan/src/Phan/Library/Restarter.php', + 'Phan\\Library\\Set' => $vendorDir . '/phan/phan/src/Phan/Library/Set.php', + 'Phan\\Library\\Some' => $vendorDir . '/phan/phan/src/Phan/Library/Some.php', + 'Phan\\Library\\StderrLogger' => $vendorDir . '/phan/phan/src/Phan/Library/StderrLogger.php', + 'Phan\\Library\\StringSuggester' => $vendorDir . '/phan/phan/src/Phan/Library/StringSuggester.php', + 'Phan\\Library\\StringUtil' => $vendorDir . '/phan/phan/src/Phan/Library/StringUtil.php', + 'Phan\\Library\\Tuple' => $vendorDir . '/phan/phan/src/Phan/Library/Tuple.php', + 'Phan\\Library\\Tuple1' => $vendorDir . '/phan/phan/src/Phan/Library/Tuple1.php', + 'Phan\\Library\\Tuple2' => $vendorDir . '/phan/phan/src/Phan/Library/Tuple2.php', + 'Phan\\Library\\Tuple3' => $vendorDir . '/phan/phan/src/Phan/Library/Tuple3.php', + 'Phan\\Library\\Tuple4' => $vendorDir . '/phan/phan/src/Phan/Library/Tuple4.php', + 'Phan\\Library\\Tuple5' => $vendorDir . '/phan/phan/src/Phan/Library/Tuple5.php', + 'Phan\\Memoize' => $vendorDir . '/phan/phan/src/Phan/Memoize.php', + 'Phan\\Ordering' => $vendorDir . '/phan/phan/src/Phan/Ordering.php', + 'Phan\\Output\\BufferedPrinterInterface' => $vendorDir . '/phan/phan/src/Phan/Output/BufferedPrinterInterface.php', + 'Phan\\Output\\Collector\\BufferingCollector' => $vendorDir . '/phan/phan/src/Phan/Output/Collector/BufferingCollector.php', + 'Phan\\Output\\Collector\\ParallelChildCollector' => $vendorDir . '/phan/phan/src/Phan/Output/Collector/ParallelChildCollector.php', + 'Phan\\Output\\Collector\\ParallelParentCollector' => $vendorDir . '/phan/phan/src/Phan/Output/Collector/ParallelParentCollector.php', + 'Phan\\Output\\ColorScheme\\Code' => $vendorDir . '/phan/phan/src/Phan/Output/ColorScheme/Code.php', + 'Phan\\Output\\ColorScheme\\EclipseDark' => $vendorDir . '/phan/phan/src/Phan/Output/ColorScheme/EclipseDark.php', + 'Phan\\Output\\ColorScheme\\Light' => $vendorDir . '/phan/phan/src/Phan/Output/ColorScheme/Light.php', + 'Phan\\Output\\ColorScheme\\LightHighContrast' => $vendorDir . '/phan/phan/src/Phan/Output/ColorScheme/LightHighContrast.php', + 'Phan\\Output\\ColorScheme\\Vim' => $vendorDir . '/phan/phan/src/Phan/Output/ColorScheme/Vim.php', + 'Phan\\Output\\Colorizing' => $vendorDir . '/phan/phan/src/Phan/Output/Colorizing.php', + 'Phan\\Output\\Filter\\AnyFilter' => $vendorDir . '/phan/phan/src/Phan/Output/Filter/AnyFilter.php', + 'Phan\\Output\\Filter\\CategoryIssueFilter' => $vendorDir . '/phan/phan/src/Phan/Output/Filter/CategoryIssueFilter.php', + 'Phan\\Output\\Filter\\ChainedIssueFilter' => $vendorDir . '/phan/phan/src/Phan/Output/Filter/ChainedIssueFilter.php', + 'Phan\\Output\\Filter\\FileIssueFilter' => $vendorDir . '/phan/phan/src/Phan/Output/Filter/FileIssueFilter.php', + 'Phan\\Output\\Filter\\MinimumSeverityFilter' => $vendorDir . '/phan/phan/src/Phan/Output/Filter/MinimumSeverityFilter.php', + 'Phan\\Output\\HTML' => $vendorDir . '/phan/phan/src/Phan/Output/HTML.php', + 'Phan\\Output\\IgnoredFilesFilterInterface' => $vendorDir . '/phan/phan/src/Phan/Output/IgnoredFilesFilterInterface.php', + 'Phan\\Output\\IssueCollectorInterface' => $vendorDir . '/phan/phan/src/Phan/Output/IssueCollectorInterface.php', + 'Phan\\Output\\IssueFilterInterface' => $vendorDir . '/phan/phan/src/Phan/Output/IssueFilterInterface.php', + 'Phan\\Output\\IssuePrinterInterface' => $vendorDir . '/phan/phan/src/Phan/Output/IssuePrinterInterface.php', + 'Phan\\Output\\PrinterFactory' => $vendorDir . '/phan/phan/src/Phan/Output/PrinterFactory.php', + 'Phan\\Output\\Printer\\CSVPrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/CSVPrinter.php', + 'Phan\\Output\\Printer\\CapturingJSONPrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/CapturingJSONPrinter.php', + 'Phan\\Output\\Printer\\CheckstylePrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/CheckstylePrinter.php', + 'Phan\\Output\\Printer\\CodeClimatePrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/CodeClimatePrinter.php', + 'Phan\\Output\\Printer\\FilteringPrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/FilteringPrinter.php', + 'Phan\\Output\\Printer\\HTMLPrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/HTMLPrinter.php', + 'Phan\\Output\\Printer\\JSONPrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/JSONPrinter.php', + 'Phan\\Output\\Printer\\PlainTextPrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/PlainTextPrinter.php', + 'Phan\\Output\\Printer\\PylintPrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/PylintPrinter.php', + 'Phan\\Output\\Printer\\VerbosePlainTextPrinter' => $vendorDir . '/phan/phan/src/Phan/Output/Printer/VerbosePlainTextPrinter.php', + 'Phan\\Parse\\ParseVisitor' => $vendorDir . '/phan/phan/src/Phan/Parse/ParseVisitor.php', + 'Phan\\Phan' => $vendorDir . '/phan/phan/src/Phan/Phan.php', + 'Phan\\PluginV3' => $vendorDir . '/phan/phan/src/Phan/PluginV3.php', + 'Phan\\PluginV3\\AfterAnalyzeFileCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/AfterAnalyzeFileCapability.php', + 'Phan\\PluginV3\\AnalyzeClassCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/AnalyzeClassCapability.php', + 'Phan\\PluginV3\\AnalyzeFunctionCallCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/AnalyzeFunctionCallCapability.php', + 'Phan\\PluginV3\\AnalyzeFunctionCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/AnalyzeFunctionCapability.php', + 'Phan\\PluginV3\\AnalyzeLiteralStatementCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/AnalyzeLiteralStatementCapability.php', + 'Phan\\PluginV3\\AnalyzeMethodCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/AnalyzeMethodCapability.php', + 'Phan\\PluginV3\\AnalyzePropertyCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/AnalyzePropertyCapability.php', + 'Phan\\PluginV3\\AutomaticFixCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/AutomaticFixCapability.php', + 'Phan\\PluginV3\\BeforeAnalyzeCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/BeforeAnalyzeCapability.php', + 'Phan\\PluginV3\\BeforeAnalyzeFileCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/BeforeAnalyzeFileCapability.php', + 'Phan\\PluginV3\\BeforeAnalyzePhaseCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/BeforeAnalyzePhaseCapability.php', + 'Phan\\PluginV3\\BeforeLoopBodyAnalysisCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/BeforeLoopBodyAnalysisCapability.php', + 'Phan\\PluginV3\\BeforeLoopBodyAnalysisVisitor' => $vendorDir . '/phan/phan/src/Phan/PluginV3/BeforeLoopBodyAnalysisVisitor.php', + 'Phan\\PluginV3\\FinalizeProcessCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/FinalizeProcessCapability.php', + 'Phan\\PluginV3\\HandleLazyLoadInternalFunctionCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/HandleLazyLoadInternalFunctionCapability.php', + 'Phan\\PluginV3\\IssueEmitter' => $vendorDir . '/phan/phan/src/Phan/PluginV3/IssueEmitter.php', + 'Phan\\PluginV3\\MergeVariableInfoCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/MergeVariableInfoCapability.php', + 'Phan\\PluginV3\\PluginAwareBaseAnalysisVisitor' => $vendorDir . '/phan/phan/src/Phan/PluginV3/PluginAwareBaseAnalysisVisitor.php', + 'Phan\\PluginV3\\PluginAwarePostAnalysisVisitor' => $vendorDir . '/phan/phan/src/Phan/PluginV3/PluginAwarePostAnalysisVisitor.php', + 'Phan\\PluginV3\\PluginAwarePreAnalysisVisitor' => $vendorDir . '/phan/phan/src/Phan/PluginV3/PluginAwarePreAnalysisVisitor.php', + 'Phan\\PluginV3\\PostAnalyzeNodeCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/PostAnalyzeNodeCapability.php', + 'Phan\\PluginV3\\PreAnalyzeNodeCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/PreAnalyzeNodeCapability.php', + 'Phan\\PluginV3\\ReturnTypeOverrideCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/ReturnTypeOverrideCapability.php', + 'Phan\\PluginV3\\StopParamAnalysisException' => $vendorDir . '/phan/phan/src/Phan/PluginV3/StopParamAnalysisException.php', + 'Phan\\PluginV3\\SubscribeEmitIssueCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/SubscribeEmitIssueCapability.php', + 'Phan\\PluginV3\\SuppressionCapability' => $vendorDir . '/phan/phan/src/Phan/PluginV3/SuppressionCapability.php', + 'Phan\\PluginV3\\UnloadablePluginException' => $vendorDir . '/phan/phan/src/Phan/PluginV3/UnloadablePluginException.php', + 'Phan\\Plugin\\ClosuresForKind' => $vendorDir . '/phan/phan/src/Phan/Plugin/ClosuresForKind.php', + 'Phan\\Plugin\\ConfigPluginSet' => $vendorDir . '/phan/phan/src/Phan/Plugin/ConfigPluginSet.php', + 'Phan\\Plugin\\Internal\\ArrayReturnTypeOverridePlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/ArrayReturnTypeOverridePlugin.php', + 'Phan\\Plugin\\Internal\\BaselineLoadingPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/BaselineLoadingPlugin.php', + 'Phan\\Plugin\\Internal\\BaselineSavingPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/BaselineSavingPlugin.php', + 'Phan\\Plugin\\Internal\\BuiltinSuppressionPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/BuiltinSuppressionPlugin.php', + 'Phan\\Plugin\\Internal\\CallableParamPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/CallableParamPlugin.php', + 'Phan\\Plugin\\Internal\\ClosureReturnTypeOverridePlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/ClosureReturnTypeOverridePlugin.php', + 'Phan\\Plugin\\Internal\\CompactPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/CompactPlugin.php', + 'Phan\\Plugin\\Internal\\CtagsPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/CtagsPlugin.php', + 'Phan\\Plugin\\Internal\\CtagsPlugin\\CtagsEntry' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/CtagsPlugin/CtagsEntry.php', + 'Phan\\Plugin\\Internal\\CtagsPlugin\\CtagsEntrySet' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/CtagsPlugin/CtagsEntrySet.php', + 'Phan\\Plugin\\Internal\\DependencyGraphPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/DependencyGraphPlugin.php', + 'Phan\\Plugin\\Internal\\DependentReturnTypeOverridePlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/DependentReturnTypeOverridePlugin.php', + 'Phan\\Plugin\\Internal\\DumpPHPDocPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/DumpPHPDocPlugin.php', + 'Phan\\Plugin\\Internal\\ExtendedDependentReturnTypeOverridePlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/ExtendedDependentReturnTypeOverridePlugin.php', + 'Phan\\Plugin\\Internal\\IssueFixingPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/IssueFixingPlugin.php', + 'Phan\\Plugin\\Internal\\IssueFixingPlugin\\FileEdit' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/IssueFixingPlugin/FileEdit.php', + 'Phan\\Plugin\\Internal\\IssueFixingPlugin\\FileEditSet' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/IssueFixingPlugin/FileEditSet.php', + 'Phan\\Plugin\\Internal\\IssueFixingPlugin\\IssueFixer' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/IssueFixingPlugin/IssueFixer.php', + 'Phan\\Plugin\\Internal\\LoopVariableReusePlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/LoopVariableReusePlugin.php', + 'Phan\\Plugin\\Internal\\LoopVariableReuseVisitor' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/LoopVariableReuseVisitor.php', + 'Phan\\Plugin\\Internal\\MethodSearcherPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/MethodSearcherPlugin.php', + 'Phan\\Plugin\\Internal\\MiscParamPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/MiscParamPlugin.php', + 'Phan\\Plugin\\Internal\\NodeSelectionPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/NodeSelectionPlugin.php', + 'Phan\\Plugin\\Internal\\PhantasmPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/PhantasmPlugin.php', + 'Phan\\Plugin\\Internal\\PhantasmPlugin\\PhantasmVisitor' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/PhantasmPlugin/PhantasmVisitor.php', + 'Phan\\Plugin\\Internal\\RedundantConditionCallPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/RedundantConditionCallPlugin.php', + 'Phan\\Plugin\\Internal\\RedundantConditionLoopCheck' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/RedundantConditionLoopCheck.php', + 'Phan\\Plugin\\Internal\\RedundantConditionVisitor' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/RedundantConditionVisitor.php', + 'Phan\\Plugin\\Internal\\RequireExistsPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/RequireExistsPlugin.php', + 'Phan\\Plugin\\Internal\\StringFunctionPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/StringFunctionPlugin.php', + 'Phan\\Plugin\\Internal\\ThrowAnalyzerPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/ThrowAnalyzerPlugin.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\PureMethodGraph' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/PureMethodGraph.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\PureMethodInferrer' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/PureMethodInferrer.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\RedundantReturnVisitor' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/RedundantReturnVisitor.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\StatsForFQSEN' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/StatsForFQSEN.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\UseReturnValueVisitor' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/UseReturnValueVisitor.php', + 'Phan\\Plugin\\Internal\\VariableTrackerPlugin' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/VariableTrackerPlugin.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableGraph' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableGraph.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableTrackerVisitor' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableTrackerVisitor.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableTrackingBranchScope' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableTrackingBranchScope.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableTrackingLoopScope' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableTrackingLoopScope.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableTrackingScope' => $vendorDir . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableTrackingScope.php', + 'Phan\\Prep' => $vendorDir . '/phan/phan/src/Phan/Prep.php', + 'Phan\\Profile' => $vendorDir . '/phan/phan/src/Phan/Profile.php', + 'Phan\\Suggestion' => $vendorDir . '/phan/phan/src/Phan/Suggestion.php', + 'Psr\\Container\\ContainerExceptionInterface' => $vendorDir . '/psr/container/src/ContainerExceptionInterface.php', + 'Psr\\Container\\ContainerInterface' => $vendorDir . '/psr/container/src/ContainerInterface.php', + 'Psr\\Container\\NotFoundExceptionInterface' => $vendorDir . '/psr/container/src/NotFoundExceptionInterface.php', 'Psr\\Log\\AbstractLogger' => $vendorDir . '/psr/log/Psr/Log/AbstractLogger.php', 'Psr\\Log\\InvalidArgumentException' => $vendorDir . '/psr/log/Psr/Log/InvalidArgumentException.php', 'Psr\\Log\\LogLevel' => $vendorDir . '/psr/log/Psr/Log/LogLevel.php', @@ -15,260 +869,226 @@ 'Psr\\Log\\LoggerInterface' => $vendorDir . '/psr/log/Psr/Log/LoggerInterface.php', 'Psr\\Log\\LoggerTrait' => $vendorDir . '/psr/log/Psr/Log/LoggerTrait.php', 'Psr\\Log\\NullLogger' => $vendorDir . '/psr/log/Psr/Log/NullLogger.php', - 'Psr\\Log\\Test\\DummyTest' => $vendorDir . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php', 'Psr\\Log\\Test\\LoggerInterfaceTest' => $vendorDir . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php', 'Psr\\SimpleCache\\CacheException' => $vendorDir . '/psr/simple-cache/src/CacheException.php', 'Psr\\SimpleCache\\CacheInterface' => $vendorDir . '/psr/simple-cache/src/CacheInterface.php', 'Psr\\SimpleCache\\InvalidArgumentException' => $vendorDir . '/psr/simple-cache/src/InvalidArgumentException.php', - 'Zend\\Db\\Adapter\\Adapter' => $vendorDir . '/zendframework/zend-db/src/Adapter/Adapter.php', - 'Zend\\Db\\Adapter\\AdapterAbstractServiceFactory' => $vendorDir . '/zendframework/zend-db/src/Adapter/AdapterAbstractServiceFactory.php', - 'Zend\\Db\\Adapter\\AdapterAwareInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/AdapterAwareInterface.php', - 'Zend\\Db\\Adapter\\AdapterAwareTrait' => $vendorDir . '/zendframework/zend-db/src/Adapter/AdapterAwareTrait.php', - 'Zend\\Db\\Adapter\\AdapterInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/AdapterInterface.php', - 'Zend\\Db\\Adapter\\AdapterServiceFactory' => $vendorDir . '/zendframework/zend-db/src/Adapter/AdapterServiceFactory.php', - 'Zend\\Db\\Adapter\\Driver\\AbstractConnection' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/AbstractConnection.php', - 'Zend\\Db\\Adapter\\Driver\\ConnectionInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/ConnectionInterface.php', - 'Zend\\Db\\Adapter\\Driver\\DriverInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/DriverInterface.php', - 'Zend\\Db\\Adapter\\Driver\\Feature\\AbstractFeature' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Feature/AbstractFeature.php', - 'Zend\\Db\\Adapter\\Driver\\Feature\\DriverFeatureInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Feature/DriverFeatureInterface.php', - 'Zend\\Db\\Adapter\\Driver\\IbmDb2\\Connection' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/IbmDb2/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\IbmDb2\\IbmDb2' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/IbmDb2/IbmDb2.php', - 'Zend\\Db\\Adapter\\Driver\\IbmDb2\\Result' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/IbmDb2/Result.php', - 'Zend\\Db\\Adapter\\Driver\\IbmDb2\\Statement' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/IbmDb2/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\Mysqli\\Connection' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Mysqli/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Mysqli\\Mysqli' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Mysqli/Mysqli.php', - 'Zend\\Db\\Adapter\\Driver\\Mysqli\\Result' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Mysqli/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Mysqli\\Statement' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Mysqli/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Connection' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Feature\\RowCounter' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Feature/RowCounter.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Oci8' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Oci8.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Result' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Statement' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Connection' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Feature\\OracleRowCounter' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Feature/OracleRowCounter.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Feature\\SqliteRowCounter' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Pdo' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Pdo.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Result' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Statement' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\Pgsql\\Connection' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pgsql/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Pgsql\\Pgsql' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pgsql/Pgsql.php', - 'Zend\\Db\\Adapter\\Driver\\Pgsql\\Result' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pgsql/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Pgsql\\Statement' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Pgsql/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\ResultInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/ResultInterface.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Connection' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Exception\\ErrorException' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Exception/ErrorException.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Exception\\ExceptionInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Result' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Sqlsrv' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Sqlsrv.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Statement' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\StatementInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Driver/StatementInterface.php', - 'Zend\\Db\\Adapter\\Exception\\ErrorException' => $vendorDir . '/zendframework/zend-db/src/Adapter/Exception/ErrorException.php', - 'Zend\\Db\\Adapter\\Exception\\ExceptionInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Exception/ExceptionInterface.php', - 'Zend\\Db\\Adapter\\Exception\\InvalidArgumentException' => $vendorDir . '/zendframework/zend-db/src/Adapter/Exception/InvalidArgumentException.php', - 'Zend\\Db\\Adapter\\Exception\\InvalidConnectionParametersException' => $vendorDir . '/zendframework/zend-db/src/Adapter/Exception/InvalidConnectionParametersException.php', - 'Zend\\Db\\Adapter\\Exception\\InvalidQueryException' => $vendorDir . '/zendframework/zend-db/src/Adapter/Exception/InvalidQueryException.php', - 'Zend\\Db\\Adapter\\Exception\\RuntimeException' => $vendorDir . '/zendframework/zend-db/src/Adapter/Exception/RuntimeException.php', - 'Zend\\Db\\Adapter\\Exception\\UnexpectedValueException' => $vendorDir . '/zendframework/zend-db/src/Adapter/Exception/UnexpectedValueException.php', - 'Zend\\Db\\Adapter\\ParameterContainer' => $vendorDir . '/zendframework/zend-db/src/Adapter/ParameterContainer.php', - 'Zend\\Db\\Adapter\\Platform\\AbstractPlatform' => $vendorDir . '/zendframework/zend-db/src/Adapter/Platform/AbstractPlatform.php', - 'Zend\\Db\\Adapter\\Platform\\IbmDb2' => $vendorDir . '/zendframework/zend-db/src/Adapter/Platform/IbmDb2.php', - 'Zend\\Db\\Adapter\\Platform\\Mysql' => $vendorDir . '/zendframework/zend-db/src/Adapter/Platform/Mysql.php', - 'Zend\\Db\\Adapter\\Platform\\Oracle' => $vendorDir . '/zendframework/zend-db/src/Adapter/Platform/Oracle.php', - 'Zend\\Db\\Adapter\\Platform\\PlatformInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Platform/PlatformInterface.php', - 'Zend\\Db\\Adapter\\Platform\\Postgresql' => $vendorDir . '/zendframework/zend-db/src/Adapter/Platform/Postgresql.php', - 'Zend\\Db\\Adapter\\Platform\\Sql92' => $vendorDir . '/zendframework/zend-db/src/Adapter/Platform/Sql92.php', - 'Zend\\Db\\Adapter\\Platform\\SqlServer' => $vendorDir . '/zendframework/zend-db/src/Adapter/Platform/SqlServer.php', - 'Zend\\Db\\Adapter\\Platform\\Sqlite' => $vendorDir . '/zendframework/zend-db/src/Adapter/Platform/Sqlite.php', - 'Zend\\Db\\Adapter\\Profiler\\Profiler' => $vendorDir . '/zendframework/zend-db/src/Adapter/Profiler/Profiler.php', - 'Zend\\Db\\Adapter\\Profiler\\ProfilerAwareInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Profiler/ProfilerAwareInterface.php', - 'Zend\\Db\\Adapter\\Profiler\\ProfilerInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/Profiler/ProfilerInterface.php', - 'Zend\\Db\\Adapter\\StatementContainer' => $vendorDir . '/zendframework/zend-db/src/Adapter/StatementContainer.php', - 'Zend\\Db\\Adapter\\StatementContainerInterface' => $vendorDir . '/zendframework/zend-db/src/Adapter/StatementContainerInterface.php', - 'Zend\\Db\\ConfigProvider' => $vendorDir . '/zendframework/zend-db/src/ConfigProvider.php', - 'Zend\\Db\\Exception\\ErrorException' => $vendorDir . '/zendframework/zend-db/src/Exception/ErrorException.php', - 'Zend\\Db\\Exception\\ExceptionInterface' => $vendorDir . '/zendframework/zend-db/src/Exception/ExceptionInterface.php', - 'Zend\\Db\\Exception\\InvalidArgumentException' => $vendorDir . '/zendframework/zend-db/src/Exception/InvalidArgumentException.php', - 'Zend\\Db\\Exception\\RuntimeException' => $vendorDir . '/zendframework/zend-db/src/Exception/RuntimeException.php', - 'Zend\\Db\\Exception\\UnexpectedValueException' => $vendorDir . '/zendframework/zend-db/src/Exception/UnexpectedValueException.php', - 'Zend\\Db\\Metadata\\Metadata' => $vendorDir . '/zendframework/zend-db/src/Metadata/Metadata.php', - 'Zend\\Db\\Metadata\\MetadataInterface' => $vendorDir . '/zendframework/zend-db/src/Metadata/MetadataInterface.php', - 'Zend\\Db\\Metadata\\Object\\AbstractTableObject' => $vendorDir . '/zendframework/zend-db/src/Metadata/Object/AbstractTableObject.php', - 'Zend\\Db\\Metadata\\Object\\ColumnObject' => $vendorDir . '/zendframework/zend-db/src/Metadata/Object/ColumnObject.php', - 'Zend\\Db\\Metadata\\Object\\ConstraintKeyObject' => $vendorDir . '/zendframework/zend-db/src/Metadata/Object/ConstraintKeyObject.php', - 'Zend\\Db\\Metadata\\Object\\ConstraintObject' => $vendorDir . '/zendframework/zend-db/src/Metadata/Object/ConstraintObject.php', - 'Zend\\Db\\Metadata\\Object\\TableObject' => $vendorDir . '/zendframework/zend-db/src/Metadata/Object/TableObject.php', - 'Zend\\Db\\Metadata\\Object\\TriggerObject' => $vendorDir . '/zendframework/zend-db/src/Metadata/Object/TriggerObject.php', - 'Zend\\Db\\Metadata\\Object\\ViewObject' => $vendorDir . '/zendframework/zend-db/src/Metadata/Object/ViewObject.php', - 'Zend\\Db\\Metadata\\Source\\AbstractSource' => $vendorDir . '/zendframework/zend-db/src/Metadata/Source/AbstractSource.php', - 'Zend\\Db\\Metadata\\Source\\Factory' => $vendorDir . '/zendframework/zend-db/src/Metadata/Source/Factory.php', - 'Zend\\Db\\Metadata\\Source\\MysqlMetadata' => $vendorDir . '/zendframework/zend-db/src/Metadata/Source/MysqlMetadata.php', - 'Zend\\Db\\Metadata\\Source\\OracleMetadata' => $vendorDir . '/zendframework/zend-db/src/Metadata/Source/OracleMetadata.php', - 'Zend\\Db\\Metadata\\Source\\PostgresqlMetadata' => $vendorDir . '/zendframework/zend-db/src/Metadata/Source/PostgresqlMetadata.php', - 'Zend\\Db\\Metadata\\Source\\SqlServerMetadata' => $vendorDir . '/zendframework/zend-db/src/Metadata/Source/SqlServerMetadata.php', - 'Zend\\Db\\Metadata\\Source\\SqliteMetadata' => $vendorDir . '/zendframework/zend-db/src/Metadata/Source/SqliteMetadata.php', - 'Zend\\Db\\Module' => $vendorDir . '/zendframework/zend-db/src/Module.php', - 'Zend\\Db\\ResultSet\\AbstractResultSet' => $vendorDir . '/zendframework/zend-db/src/ResultSet/AbstractResultSet.php', - 'Zend\\Db\\ResultSet\\Exception\\ExceptionInterface' => $vendorDir . '/zendframework/zend-db/src/ResultSet/Exception/ExceptionInterface.php', - 'Zend\\Db\\ResultSet\\Exception\\InvalidArgumentException' => $vendorDir . '/zendframework/zend-db/src/ResultSet/Exception/InvalidArgumentException.php', - 'Zend\\Db\\ResultSet\\Exception\\RuntimeException' => $vendorDir . '/zendframework/zend-db/src/ResultSet/Exception/RuntimeException.php', - 'Zend\\Db\\ResultSet\\HydratingResultSet' => $vendorDir . '/zendframework/zend-db/src/ResultSet/HydratingResultSet.php', - 'Zend\\Db\\ResultSet\\ResultSet' => $vendorDir . '/zendframework/zend-db/src/ResultSet/ResultSet.php', - 'Zend\\Db\\ResultSet\\ResultSetInterface' => $vendorDir . '/zendframework/zend-db/src/ResultSet/ResultSetInterface.php', - 'Zend\\Db\\RowGateway\\AbstractRowGateway' => $vendorDir . '/zendframework/zend-db/src/RowGateway/AbstractRowGateway.php', - 'Zend\\Db\\RowGateway\\Exception\\ExceptionInterface' => $vendorDir . '/zendframework/zend-db/src/RowGateway/Exception/ExceptionInterface.php', - 'Zend\\Db\\RowGateway\\Exception\\InvalidArgumentException' => $vendorDir . '/zendframework/zend-db/src/RowGateway/Exception/InvalidArgumentException.php', - 'Zend\\Db\\RowGateway\\Exception\\RuntimeException' => $vendorDir . '/zendframework/zend-db/src/RowGateway/Exception/RuntimeException.php', - 'Zend\\Db\\RowGateway\\Feature\\AbstractFeature' => $vendorDir . '/zendframework/zend-db/src/RowGateway/Feature/AbstractFeature.php', - 'Zend\\Db\\RowGateway\\Feature\\FeatureSet' => $vendorDir . '/zendframework/zend-db/src/RowGateway/Feature/FeatureSet.php', - 'Zend\\Db\\RowGateway\\RowGateway' => $vendorDir . '/zendframework/zend-db/src/RowGateway/RowGateway.php', - 'Zend\\Db\\RowGateway\\RowGatewayInterface' => $vendorDir . '/zendframework/zend-db/src/RowGateway/RowGatewayInterface.php', - 'Zend\\Db\\Sql\\AbstractExpression' => $vendorDir . '/zendframework/zend-db/src/Sql/AbstractExpression.php', - 'Zend\\Db\\Sql\\AbstractPreparableSql' => $vendorDir . '/zendframework/zend-db/src/Sql/AbstractPreparableSql.php', - 'Zend\\Db\\Sql\\AbstractSql' => $vendorDir . '/zendframework/zend-db/src/Sql/AbstractSql.php', - 'Zend\\Db\\Sql\\Combine' => $vendorDir . '/zendframework/zend-db/src/Sql/Combine.php', - 'Zend\\Db\\Sql\\Ddl\\AlterTable' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/AlterTable.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\AbstractLengthColumn' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/AbstractLengthColumn.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\AbstractPrecisionColumn' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/AbstractPrecisionColumn.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\AbstractTimestampColumn' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/AbstractTimestampColumn.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\BigInteger' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/BigInteger.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Binary' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Binary.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Blob' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Blob.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Boolean' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Boolean.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Char' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Char.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Column' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Column.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\ColumnInterface' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/ColumnInterface.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Date' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Date.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Datetime' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Datetime.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Decimal' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Decimal.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Float' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Float.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Floating' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Floating.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Integer' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Integer.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Text' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Text.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Time' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Time.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Timestamp' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Timestamp.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Varbinary' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Varbinary.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Varchar' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Column/Varchar.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\AbstractConstraint' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Constraint/AbstractConstraint.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\Check' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Constraint/Check.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\ConstraintInterface' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Constraint/ConstraintInterface.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\ForeignKey' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Constraint/ForeignKey.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\PrimaryKey' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Constraint/PrimaryKey.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\UniqueKey' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Constraint/UniqueKey.php', - 'Zend\\Db\\Sql\\Ddl\\CreateTable' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/CreateTable.php', - 'Zend\\Db\\Sql\\Ddl\\DropTable' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/DropTable.php', - 'Zend\\Db\\Sql\\Ddl\\Index\\AbstractIndex' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Index/AbstractIndex.php', - 'Zend\\Db\\Sql\\Ddl\\Index\\Index' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/Index/Index.php', - 'Zend\\Db\\Sql\\Ddl\\SqlInterface' => $vendorDir . '/zendframework/zend-db/src/Sql/Ddl/SqlInterface.php', - 'Zend\\Db\\Sql\\Delete' => $vendorDir . '/zendframework/zend-db/src/Sql/Delete.php', - 'Zend\\Db\\Sql\\Exception\\ExceptionInterface' => $vendorDir . '/zendframework/zend-db/src/Sql/Exception/ExceptionInterface.php', - 'Zend\\Db\\Sql\\Exception\\InvalidArgumentException' => $vendorDir . '/zendframework/zend-db/src/Sql/Exception/InvalidArgumentException.php', - 'Zend\\Db\\Sql\\Exception\\RuntimeException' => $vendorDir . '/zendframework/zend-db/src/Sql/Exception/RuntimeException.php', - 'Zend\\Db\\Sql\\Expression' => $vendorDir . '/zendframework/zend-db/src/Sql/Expression.php', - 'Zend\\Db\\Sql\\ExpressionInterface' => $vendorDir . '/zendframework/zend-db/src/Sql/ExpressionInterface.php', - 'Zend\\Db\\Sql\\Having' => $vendorDir . '/zendframework/zend-db/src/Sql/Having.php', - 'Zend\\Db\\Sql\\Insert' => $vendorDir . '/zendframework/zend-db/src/Sql/Insert.php', - 'Zend\\Db\\Sql\\Join' => $vendorDir . '/zendframework/zend-db/src/Sql/Join.php', - 'Zend\\Db\\Sql\\Literal' => $vendorDir . '/zendframework/zend-db/src/Sql/Literal.php', - 'Zend\\Db\\Sql\\Platform\\AbstractPlatform' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/AbstractPlatform.php', - 'Zend\\Db\\Sql\\Platform\\IbmDb2\\IbmDb2' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/IbmDb2/IbmDb2.php', - 'Zend\\Db\\Sql\\Platform\\IbmDb2\\SelectDecorator' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/IbmDb2/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Mysql\\Ddl\\AlterTableDecorator' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Mysql\\Ddl\\CreateTableDecorator' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Mysql\\Mysql' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/Mysql/Mysql.php', - 'Zend\\Db\\Sql\\Platform\\Mysql\\SelectDecorator' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/Mysql/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Oracle\\Oracle' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/Oracle/Oracle.php', - 'Zend\\Db\\Sql\\Platform\\Oracle\\SelectDecorator' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/Oracle/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Platform' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/Platform.php', - 'Zend\\Db\\Sql\\Platform\\PlatformDecoratorInterface' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/PlatformDecoratorInterface.php', - 'Zend\\Db\\Sql\\Platform\\SqlServer\\Ddl\\CreateTableDecorator' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/SqlServer/Ddl/CreateTableDecorator.php', - 'Zend\\Db\\Sql\\Platform\\SqlServer\\SelectDecorator' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/SqlServer/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\SqlServer\\SqlServer' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/SqlServer/SqlServer.php', - 'Zend\\Db\\Sql\\Platform\\Sqlite\\SelectDecorator' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/Sqlite/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Sqlite\\Sqlite' => $vendorDir . '/zendframework/zend-db/src/Sql/Platform/Sqlite/Sqlite.php', - 'Zend\\Db\\Sql\\Predicate\\Between' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/Between.php', - 'Zend\\Db\\Sql\\Predicate\\Expression' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/Expression.php', - 'Zend\\Db\\Sql\\Predicate\\In' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/In.php', - 'Zend\\Db\\Sql\\Predicate\\IsNotNull' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/IsNotNull.php', - 'Zend\\Db\\Sql\\Predicate\\IsNull' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/IsNull.php', - 'Zend\\Db\\Sql\\Predicate\\Like' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/Like.php', - 'Zend\\Db\\Sql\\Predicate\\Literal' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/Literal.php', - 'Zend\\Db\\Sql\\Predicate\\NotBetween' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/NotBetween.php', - 'Zend\\Db\\Sql\\Predicate\\NotIn' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/NotIn.php', - 'Zend\\Db\\Sql\\Predicate\\NotLike' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/NotLike.php', - 'Zend\\Db\\Sql\\Predicate\\Operator' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/Operator.php', - 'Zend\\Db\\Sql\\Predicate\\Predicate' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/Predicate.php', - 'Zend\\Db\\Sql\\Predicate\\PredicateInterface' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/PredicateInterface.php', - 'Zend\\Db\\Sql\\Predicate\\PredicateSet' => $vendorDir . '/zendframework/zend-db/src/Sql/Predicate/PredicateSet.php', - 'Zend\\Db\\Sql\\PreparableSqlInterface' => $vendorDir . '/zendframework/zend-db/src/Sql/PreparableSqlInterface.php', - 'Zend\\Db\\Sql\\Select' => $vendorDir . '/zendframework/zend-db/src/Sql/Select.php', - 'Zend\\Db\\Sql\\Sql' => $vendorDir . '/zendframework/zend-db/src/Sql/Sql.php', - 'Zend\\Db\\Sql\\SqlInterface' => $vendorDir . '/zendframework/zend-db/src/Sql/SqlInterface.php', - 'Zend\\Db\\Sql\\TableIdentifier' => $vendorDir . '/zendframework/zend-db/src/Sql/TableIdentifier.php', - 'Zend\\Db\\Sql\\Update' => $vendorDir . '/zendframework/zend-db/src/Sql/Update.php', - 'Zend\\Db\\Sql\\Where' => $vendorDir . '/zendframework/zend-db/src/Sql/Where.php', - 'Zend\\Db\\TableGateway\\AbstractTableGateway' => $vendorDir . '/zendframework/zend-db/src/TableGateway/AbstractTableGateway.php', - 'Zend\\Db\\TableGateway\\Exception\\ExceptionInterface' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Exception/ExceptionInterface.php', - 'Zend\\Db\\TableGateway\\Exception\\InvalidArgumentException' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Exception/InvalidArgumentException.php', - 'Zend\\Db\\TableGateway\\Exception\\RuntimeException' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Exception/RuntimeException.php', - 'Zend\\Db\\TableGateway\\Feature\\AbstractFeature' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/AbstractFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\EventFeature' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/EventFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\EventFeatureEventsInterface' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/EventFeatureEventsInterface.php', - 'Zend\\Db\\TableGateway\\Feature\\EventFeature\\TableGatewayEvent' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php', - 'Zend\\Db\\TableGateway\\Feature\\FeatureSet' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/FeatureSet.php', - 'Zend\\Db\\TableGateway\\Feature\\GlobalAdapterFeature' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/GlobalAdapterFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\MasterSlaveFeature' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/MasterSlaveFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\MetadataFeature' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/MetadataFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\RowGatewayFeature' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/RowGatewayFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\SequenceFeature' => $vendorDir . '/zendframework/zend-db/src/TableGateway/Feature/SequenceFeature.php', - 'Zend\\Db\\TableGateway\\TableGateway' => $vendorDir . '/zendframework/zend-db/src/TableGateway/TableGateway.php', - 'Zend\\Db\\TableGateway\\TableGatewayInterface' => $vendorDir . '/zendframework/zend-db/src/TableGateway/TableGatewayInterface.php', - 'Zend\\Stdlib\\AbstractOptions' => $vendorDir . '/zendframework/zend-stdlib/src/AbstractOptions.php', - 'Zend\\Stdlib\\ArrayObject' => $vendorDir . '/zendframework/zend-stdlib/src/ArrayObject.php', - 'Zend\\Stdlib\\ArraySerializableInterface' => $vendorDir . '/zendframework/zend-stdlib/src/ArraySerializableInterface.php', - 'Zend\\Stdlib\\ArrayStack' => $vendorDir . '/zendframework/zend-stdlib/src/ArrayStack.php', - 'Zend\\Stdlib\\ArrayUtils' => $vendorDir . '/zendframework/zend-stdlib/src/ArrayUtils.php', - 'Zend\\Stdlib\\ArrayUtils\\MergeRemoveKey' => $vendorDir . '/zendframework/zend-stdlib/src/ArrayUtils/MergeRemoveKey.php', - 'Zend\\Stdlib\\ArrayUtils\\MergeReplaceKey' => $vendorDir . '/zendframework/zend-stdlib/src/ArrayUtils/MergeReplaceKey.php', - 'Zend\\Stdlib\\ArrayUtils\\MergeReplaceKeyInterface' => $vendorDir . '/zendframework/zend-stdlib/src/ArrayUtils/MergeReplaceKeyInterface.php', - 'Zend\\Stdlib\\ConsoleHelper' => $vendorDir . '/zendframework/zend-stdlib/src/ConsoleHelper.php', - 'Zend\\Stdlib\\DispatchableInterface' => $vendorDir . '/zendframework/zend-stdlib/src/DispatchableInterface.php', - 'Zend\\Stdlib\\ErrorHandler' => $vendorDir . '/zendframework/zend-stdlib/src/ErrorHandler.php', - 'Zend\\Stdlib\\Exception\\BadMethodCallException' => $vendorDir . '/zendframework/zend-stdlib/src/Exception/BadMethodCallException.php', - 'Zend\\Stdlib\\Exception\\DomainException' => $vendorDir . '/zendframework/zend-stdlib/src/Exception/DomainException.php', - 'Zend\\Stdlib\\Exception\\ExceptionInterface' => $vendorDir . '/zendframework/zend-stdlib/src/Exception/ExceptionInterface.php', - 'Zend\\Stdlib\\Exception\\ExtensionNotLoadedException' => $vendorDir . '/zendframework/zend-stdlib/src/Exception/ExtensionNotLoadedException.php', - 'Zend\\Stdlib\\Exception\\InvalidArgumentException' => $vendorDir . '/zendframework/zend-stdlib/src/Exception/InvalidArgumentException.php', - 'Zend\\Stdlib\\Exception\\LogicException' => $vendorDir . '/zendframework/zend-stdlib/src/Exception/LogicException.php', - 'Zend\\Stdlib\\Exception\\RuntimeException' => $vendorDir . '/zendframework/zend-stdlib/src/Exception/RuntimeException.php', - 'Zend\\Stdlib\\FastPriorityQueue' => $vendorDir . '/zendframework/zend-stdlib/src/FastPriorityQueue.php', - 'Zend\\Stdlib\\Glob' => $vendorDir . '/zendframework/zend-stdlib/src/Glob.php', - 'Zend\\Stdlib\\Guard\\AllGuardsTrait' => $vendorDir . '/zendframework/zend-stdlib/src/Guard/AllGuardsTrait.php', - 'Zend\\Stdlib\\Guard\\ArrayOrTraversableGuardTrait' => $vendorDir . '/zendframework/zend-stdlib/src/Guard/ArrayOrTraversableGuardTrait.php', - 'Zend\\Stdlib\\Guard\\EmptyGuardTrait' => $vendorDir . '/zendframework/zend-stdlib/src/Guard/EmptyGuardTrait.php', - 'Zend\\Stdlib\\Guard\\NullGuardTrait' => $vendorDir . '/zendframework/zend-stdlib/src/Guard/NullGuardTrait.php', - 'Zend\\Stdlib\\InitializableInterface' => $vendorDir . '/zendframework/zend-stdlib/src/InitializableInterface.php', - 'Zend\\Stdlib\\JsonSerializable' => $vendorDir . '/zendframework/zend-stdlib/src/JsonSerializable.php', - 'Zend\\Stdlib\\Message' => $vendorDir . '/zendframework/zend-stdlib/src/Message.php', - 'Zend\\Stdlib\\MessageInterface' => $vendorDir . '/zendframework/zend-stdlib/src/MessageInterface.php', - 'Zend\\Stdlib\\ParameterObjectInterface' => $vendorDir . '/zendframework/zend-stdlib/src/ParameterObjectInterface.php', - 'Zend\\Stdlib\\Parameters' => $vendorDir . '/zendframework/zend-stdlib/src/Parameters.php', - 'Zend\\Stdlib\\ParametersInterface' => $vendorDir . '/zendframework/zend-stdlib/src/ParametersInterface.php', - 'Zend\\Stdlib\\PriorityList' => $vendorDir . '/zendframework/zend-stdlib/src/PriorityList.php', - 'Zend\\Stdlib\\PriorityQueue' => $vendorDir . '/zendframework/zend-stdlib/src/PriorityQueue.php', - 'Zend\\Stdlib\\Request' => $vendorDir . '/zendframework/zend-stdlib/src/Request.php', - 'Zend\\Stdlib\\RequestInterface' => $vendorDir . '/zendframework/zend-stdlib/src/RequestInterface.php', - 'Zend\\Stdlib\\Response' => $vendorDir . '/zendframework/zend-stdlib/src/Response.php', - 'Zend\\Stdlib\\ResponseInterface' => $vendorDir . '/zendframework/zend-stdlib/src/ResponseInterface.php', - 'Zend\\Stdlib\\SplPriorityQueue' => $vendorDir . '/zendframework/zend-stdlib/src/SplPriorityQueue.php', - 'Zend\\Stdlib\\SplQueue' => $vendorDir . '/zendframework/zend-stdlib/src/SplQueue.php', - 'Zend\\Stdlib\\SplStack' => $vendorDir . '/zendframework/zend-stdlib/src/SplStack.php', - 'Zend\\Stdlib\\StringUtils' => $vendorDir . '/zendframework/zend-stdlib/src/StringUtils.php', - 'Zend\\Stdlib\\StringWrapper\\AbstractStringWrapper' => $vendorDir . '/zendframework/zend-stdlib/src/StringWrapper/AbstractStringWrapper.php', - 'Zend\\Stdlib\\StringWrapper\\Iconv' => $vendorDir . '/zendframework/zend-stdlib/src/StringWrapper/Iconv.php', - 'Zend\\Stdlib\\StringWrapper\\Intl' => $vendorDir . '/zendframework/zend-stdlib/src/StringWrapper/Intl.php', - 'Zend\\Stdlib\\StringWrapper\\MbString' => $vendorDir . '/zendframework/zend-stdlib/src/StringWrapper/MbString.php', - 'Zend\\Stdlib\\StringWrapper\\Native' => $vendorDir . '/zendframework/zend-stdlib/src/StringWrapper/Native.php', - 'Zend\\Stdlib\\StringWrapper\\StringWrapperInterface' => $vendorDir . '/zendframework/zend-stdlib/src/StringWrapper/StringWrapperInterface.php', + 'Sabre\\Event\\Emitter' => $vendorDir . '/sabre/event/lib/Emitter.php', + 'Sabre\\Event\\EmitterInterface' => $vendorDir . '/sabre/event/lib/EmitterInterface.php', + 'Sabre\\Event\\EmitterTrait' => $vendorDir . '/sabre/event/lib/EmitterTrait.php', + 'Sabre\\Event\\EventEmitter' => $vendorDir . '/sabre/event/lib/EventEmitter.php', + 'Sabre\\Event\\Loop\\Loop' => $vendorDir . '/sabre/event/lib/Loop/Loop.php', + 'Sabre\\Event\\Promise' => $vendorDir . '/sabre/event/lib/Promise.php', + 'Sabre\\Event\\PromiseAlreadyResolvedException' => $vendorDir . '/sabre/event/lib/PromiseAlreadyResolvedException.php', + 'Sabre\\Event\\Version' => $vendorDir . '/sabre/event/lib/Version.php', + 'Sabre\\Event\\WildcardEmitter' => $vendorDir . '/sabre/event/lib/WildcardEmitter.php', + 'Sabre\\Event\\WildcardEmitterTrait' => $vendorDir . '/sabre/event/lib/WildcardEmitterTrait.php', + 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'Symfony\\Component\\Console\\Application' => $vendorDir . '/symfony/console/Application.php', + 'Symfony\\Component\\Console\\Color' => $vendorDir . '/symfony/console/Color.php', + 'Symfony\\Component\\Console\\CommandLoader\\CommandLoaderInterface' => $vendorDir . '/symfony/console/CommandLoader/CommandLoaderInterface.php', + 'Symfony\\Component\\Console\\CommandLoader\\ContainerCommandLoader' => $vendorDir . '/symfony/console/CommandLoader/ContainerCommandLoader.php', + 'Symfony\\Component\\Console\\CommandLoader\\FactoryCommandLoader' => $vendorDir . '/symfony/console/CommandLoader/FactoryCommandLoader.php', + 'Symfony\\Component\\Console\\Command\\Command' => $vendorDir . '/symfony/console/Command/Command.php', + 'Symfony\\Component\\Console\\Command\\HelpCommand' => $vendorDir . '/symfony/console/Command/HelpCommand.php', + 'Symfony\\Component\\Console\\Command\\ListCommand' => $vendorDir . '/symfony/console/Command/ListCommand.php', + 'Symfony\\Component\\Console\\Command\\LockableTrait' => $vendorDir . '/symfony/console/Command/LockableTrait.php', + 'Symfony\\Component\\Console\\Command\\SignalableCommandInterface' => $vendorDir . '/symfony/console/Command/SignalableCommandInterface.php', + 'Symfony\\Component\\Console\\ConsoleEvents' => $vendorDir . '/symfony/console/ConsoleEvents.php', + 'Symfony\\Component\\Console\\Cursor' => $vendorDir . '/symfony/console/Cursor.php', + 'Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPass' => $vendorDir . '/symfony/console/DependencyInjection/AddConsoleCommandPass.php', + 'Symfony\\Component\\Console\\Descriptor\\ApplicationDescription' => $vendorDir . '/symfony/console/Descriptor/ApplicationDescription.php', + 'Symfony\\Component\\Console\\Descriptor\\Descriptor' => $vendorDir . '/symfony/console/Descriptor/Descriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\DescriptorInterface' => $vendorDir . '/symfony/console/Descriptor/DescriptorInterface.php', + 'Symfony\\Component\\Console\\Descriptor\\JsonDescriptor' => $vendorDir . '/symfony/console/Descriptor/JsonDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\MarkdownDescriptor' => $vendorDir . '/symfony/console/Descriptor/MarkdownDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\TextDescriptor' => $vendorDir . '/symfony/console/Descriptor/TextDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\XmlDescriptor' => $vendorDir . '/symfony/console/Descriptor/XmlDescriptor.php', + 'Symfony\\Component\\Console\\EventListener\\ErrorListener' => $vendorDir . '/symfony/console/EventListener/ErrorListener.php', + 'Symfony\\Component\\Console\\Event\\ConsoleCommandEvent' => $vendorDir . '/symfony/console/Event/ConsoleCommandEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleErrorEvent' => $vendorDir . '/symfony/console/Event/ConsoleErrorEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleEvent' => $vendorDir . '/symfony/console/Event/ConsoleEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleSignalEvent' => $vendorDir . '/symfony/console/Event/ConsoleSignalEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleTerminateEvent' => $vendorDir . '/symfony/console/Event/ConsoleTerminateEvent.php', + 'Symfony\\Component\\Console\\Exception\\CommandNotFoundException' => $vendorDir . '/symfony/console/Exception/CommandNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/console/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Console\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/console/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Console\\Exception\\InvalidOptionException' => $vendorDir . '/symfony/console/Exception/InvalidOptionException.php', + 'Symfony\\Component\\Console\\Exception\\LogicException' => $vendorDir . '/symfony/console/Exception/LogicException.php', + 'Symfony\\Component\\Console\\Exception\\MissingInputException' => $vendorDir . '/symfony/console/Exception/MissingInputException.php', + 'Symfony\\Component\\Console\\Exception\\NamespaceNotFoundException' => $vendorDir . '/symfony/console/Exception/NamespaceNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\RuntimeException' => $vendorDir . '/symfony/console/Exception/RuntimeException.php', + 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatter' => $vendorDir . '/symfony/console/Formatter/NullOutputFormatter.php', + 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatterStyle' => $vendorDir . '/symfony/console/Formatter/NullOutputFormatterStyle.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatter' => $vendorDir . '/symfony/console/Formatter/OutputFormatter.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterInterface' => $vendorDir . '/symfony/console/Formatter/OutputFormatterInterface.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyle' => $vendorDir . '/symfony/console/Formatter/OutputFormatterStyle.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleInterface' => $vendorDir . '/symfony/console/Formatter/OutputFormatterStyleInterface.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleStack' => $vendorDir . '/symfony/console/Formatter/OutputFormatterStyleStack.php', + 'Symfony\\Component\\Console\\Formatter\\WrappableOutputFormatterInterface' => $vendorDir . '/symfony/console/Formatter/WrappableOutputFormatterInterface.php', + 'Symfony\\Component\\Console\\Helper\\DebugFormatterHelper' => $vendorDir . '/symfony/console/Helper/DebugFormatterHelper.php', + 'Symfony\\Component\\Console\\Helper\\DescriptorHelper' => $vendorDir . '/symfony/console/Helper/DescriptorHelper.php', + 'Symfony\\Component\\Console\\Helper\\Dumper' => $vendorDir . '/symfony/console/Helper/Dumper.php', + 'Symfony\\Component\\Console\\Helper\\FormatterHelper' => $vendorDir . '/symfony/console/Helper/FormatterHelper.php', + 'Symfony\\Component\\Console\\Helper\\Helper' => $vendorDir . '/symfony/console/Helper/Helper.php', + 'Symfony\\Component\\Console\\Helper\\HelperInterface' => $vendorDir . '/symfony/console/Helper/HelperInterface.php', + 'Symfony\\Component\\Console\\Helper\\HelperSet' => $vendorDir . '/symfony/console/Helper/HelperSet.php', + 'Symfony\\Component\\Console\\Helper\\InputAwareHelper' => $vendorDir . '/symfony/console/Helper/InputAwareHelper.php', + 'Symfony\\Component\\Console\\Helper\\ProcessHelper' => $vendorDir . '/symfony/console/Helper/ProcessHelper.php', + 'Symfony\\Component\\Console\\Helper\\ProgressBar' => $vendorDir . '/symfony/console/Helper/ProgressBar.php', + 'Symfony\\Component\\Console\\Helper\\ProgressIndicator' => $vendorDir . '/symfony/console/Helper/ProgressIndicator.php', + 'Symfony\\Component\\Console\\Helper\\QuestionHelper' => $vendorDir . '/symfony/console/Helper/QuestionHelper.php', + 'Symfony\\Component\\Console\\Helper\\SymfonyQuestionHelper' => $vendorDir . '/symfony/console/Helper/SymfonyQuestionHelper.php', + 'Symfony\\Component\\Console\\Helper\\Table' => $vendorDir . '/symfony/console/Helper/Table.php', + 'Symfony\\Component\\Console\\Helper\\TableCell' => $vendorDir . '/symfony/console/Helper/TableCell.php', + 'Symfony\\Component\\Console\\Helper\\TableCellStyle' => $vendorDir . '/symfony/console/Helper/TableCellStyle.php', + 'Symfony\\Component\\Console\\Helper\\TableRows' => $vendorDir . '/symfony/console/Helper/TableRows.php', + 'Symfony\\Component\\Console\\Helper\\TableSeparator' => $vendorDir . '/symfony/console/Helper/TableSeparator.php', + 'Symfony\\Component\\Console\\Helper\\TableStyle' => $vendorDir . '/symfony/console/Helper/TableStyle.php', + 'Symfony\\Component\\Console\\Input\\ArgvInput' => $vendorDir . '/symfony/console/Input/ArgvInput.php', + 'Symfony\\Component\\Console\\Input\\ArrayInput' => $vendorDir . '/symfony/console/Input/ArrayInput.php', + 'Symfony\\Component\\Console\\Input\\Input' => $vendorDir . '/symfony/console/Input/Input.php', + 'Symfony\\Component\\Console\\Input\\InputArgument' => $vendorDir . '/symfony/console/Input/InputArgument.php', + 'Symfony\\Component\\Console\\Input\\InputAwareInterface' => $vendorDir . '/symfony/console/Input/InputAwareInterface.php', + 'Symfony\\Component\\Console\\Input\\InputDefinition' => $vendorDir . '/symfony/console/Input/InputDefinition.php', + 'Symfony\\Component\\Console\\Input\\InputInterface' => $vendorDir . '/symfony/console/Input/InputInterface.php', + 'Symfony\\Component\\Console\\Input\\InputOption' => $vendorDir . '/symfony/console/Input/InputOption.php', + 'Symfony\\Component\\Console\\Input\\StreamableInputInterface' => $vendorDir . '/symfony/console/Input/StreamableInputInterface.php', + 'Symfony\\Component\\Console\\Input\\StringInput' => $vendorDir . '/symfony/console/Input/StringInput.php', + 'Symfony\\Component\\Console\\Logger\\ConsoleLogger' => $vendorDir . '/symfony/console/Logger/ConsoleLogger.php', + 'Symfony\\Component\\Console\\Output\\BufferedOutput' => $vendorDir . '/symfony/console/Output/BufferedOutput.php', + 'Symfony\\Component\\Console\\Output\\ConsoleOutput' => $vendorDir . '/symfony/console/Output/ConsoleOutput.php', + 'Symfony\\Component\\Console\\Output\\ConsoleOutputInterface' => $vendorDir . '/symfony/console/Output/ConsoleOutputInterface.php', + 'Symfony\\Component\\Console\\Output\\ConsoleSectionOutput' => $vendorDir . '/symfony/console/Output/ConsoleSectionOutput.php', + 'Symfony\\Component\\Console\\Output\\NullOutput' => $vendorDir . '/symfony/console/Output/NullOutput.php', + 'Symfony\\Component\\Console\\Output\\Output' => $vendorDir . '/symfony/console/Output/Output.php', + 'Symfony\\Component\\Console\\Output\\OutputInterface' => $vendorDir . '/symfony/console/Output/OutputInterface.php', + 'Symfony\\Component\\Console\\Output\\StreamOutput' => $vendorDir . '/symfony/console/Output/StreamOutput.php', + 'Symfony\\Component\\Console\\Output\\TrimmedBufferOutput' => $vendorDir . '/symfony/console/Output/TrimmedBufferOutput.php', + 'Symfony\\Component\\Console\\Question\\ChoiceQuestion' => $vendorDir . '/symfony/console/Question/ChoiceQuestion.php', + 'Symfony\\Component\\Console\\Question\\ConfirmationQuestion' => $vendorDir . '/symfony/console/Question/ConfirmationQuestion.php', + 'Symfony\\Component\\Console\\Question\\Question' => $vendorDir . '/symfony/console/Question/Question.php', + 'Symfony\\Component\\Console\\SignalRegistry\\SignalRegistry' => $vendorDir . '/symfony/console/SignalRegistry/SignalRegistry.php', + 'Symfony\\Component\\Console\\SingleCommandApplication' => $vendorDir . '/symfony/console/SingleCommandApplication.php', + 'Symfony\\Component\\Console\\Style\\OutputStyle' => $vendorDir . '/symfony/console/Style/OutputStyle.php', + 'Symfony\\Component\\Console\\Style\\StyleInterface' => $vendorDir . '/symfony/console/Style/StyleInterface.php', + 'Symfony\\Component\\Console\\Style\\SymfonyStyle' => $vendorDir . '/symfony/console/Style/SymfonyStyle.php', + 'Symfony\\Component\\Console\\Terminal' => $vendorDir . '/symfony/console/Terminal.php', + 'Symfony\\Component\\Console\\Tester\\ApplicationTester' => $vendorDir . '/symfony/console/Tester/ApplicationTester.php', + 'Symfony\\Component\\Console\\Tester\\CommandTester' => $vendorDir . '/symfony/console/Tester/CommandTester.php', + 'Symfony\\Component\\Console\\Tester\\TesterTrait' => $vendorDir . '/symfony/console/Tester/TesterTrait.php', + 'Symfony\\Component\\String\\AbstractString' => $vendorDir . '/symfony/string/AbstractString.php', + 'Symfony\\Component\\String\\AbstractUnicodeString' => $vendorDir . '/symfony/string/AbstractUnicodeString.php', + 'Symfony\\Component\\String\\ByteString' => $vendorDir . '/symfony/string/ByteString.php', + 'Symfony\\Component\\String\\CodePointString' => $vendorDir . '/symfony/string/CodePointString.php', + 'Symfony\\Component\\String\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/string/Exception/ExceptionInterface.php', + 'Symfony\\Component\\String\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/string/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\String\\Exception\\RuntimeException' => $vendorDir . '/symfony/string/Exception/RuntimeException.php', + 'Symfony\\Component\\String\\Inflector\\EnglishInflector' => $vendorDir . '/symfony/string/Inflector/EnglishInflector.php', + 'Symfony\\Component\\String\\Inflector\\FrenchInflector' => $vendorDir . '/symfony/string/Inflector/FrenchInflector.php', + 'Symfony\\Component\\String\\Inflector\\InflectorInterface' => $vendorDir . '/symfony/string/Inflector/InflectorInterface.php', + 'Symfony\\Component\\String\\LazyString' => $vendorDir . '/symfony/string/LazyString.php', + 'Symfony\\Component\\String\\Slugger\\AsciiSlugger' => $vendorDir . '/symfony/string/Slugger/AsciiSlugger.php', + 'Symfony\\Component\\String\\Slugger\\SluggerInterface' => $vendorDir . '/symfony/string/Slugger/SluggerInterface.php', + 'Symfony\\Component\\String\\UnicodeString' => $vendorDir . '/symfony/string/UnicodeString.php', + 'Symfony\\Contracts\\Service\\Attribute\\Required' => $vendorDir . '/symfony/service-contracts/Attribute/Required.php', + 'Symfony\\Contracts\\Service\\ResetInterface' => $vendorDir . '/symfony/service-contracts/ResetInterface.php', + 'Symfony\\Contracts\\Service\\ServiceLocatorTrait' => $vendorDir . '/symfony/service-contracts/ServiceLocatorTrait.php', + 'Symfony\\Contracts\\Service\\ServiceProviderInterface' => $vendorDir . '/symfony/service-contracts/ServiceProviderInterface.php', + 'Symfony\\Contracts\\Service\\ServiceSubscriberInterface' => $vendorDir . '/symfony/service-contracts/ServiceSubscriberInterface.php', + 'Symfony\\Contracts\\Service\\ServiceSubscriberTrait' => $vendorDir . '/symfony/service-contracts/ServiceSubscriberTrait.php', + 'Symfony\\Contracts\\Service\\Test\\ServiceLocatorTest' => $vendorDir . '/symfony/service-contracts/Test/ServiceLocatorTest.php', + 'Symfony\\Polyfill\\Ctype\\Ctype' => $vendorDir . '/symfony/polyfill-ctype/Ctype.php', + 'Symfony\\Polyfill\\Intl\\Grapheme\\Grapheme' => $vendorDir . '/symfony/polyfill-intl-grapheme/Grapheme.php', + 'Symfony\\Polyfill\\Intl\\Normalizer\\Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Normalizer.php', + 'Symfony\\Polyfill\\Mbstring\\Mbstring' => $vendorDir . '/symfony/polyfill-mbstring/Mbstring.php', + 'Symfony\\Polyfill\\Php73\\Php73' => $vendorDir . '/symfony/polyfill-php73/Php73.php', + 'Symfony\\Polyfill\\Php80\\Php80' => $vendorDir . '/symfony/polyfill-php80/Php80.php', + 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', + 'Webmozart\\Assert\\Assert' => $vendorDir . '/webmozart/assert/src/Assert.php', + 'Webmozart\\Assert\\InvalidArgumentException' => $vendorDir . '/webmozart/assert/src/InvalidArgumentException.php', + 'Webmozart\\Assert\\Mixin' => $vendorDir . '/webmozart/assert/src/Mixin.php', + 'phpDocumentor\\Reflection\\DocBlock' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock.php', + 'phpDocumentor\\Reflection\\DocBlockFactory' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlockFactory.php', + 'phpDocumentor\\Reflection\\DocBlockFactoryInterface' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlockFactoryInterface.php', + 'phpDocumentor\\Reflection\\DocBlock\\Description' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Description.php', + 'phpDocumentor\\Reflection\\DocBlock\\DescriptionFactory' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/DescriptionFactory.php', + 'phpDocumentor\\Reflection\\DocBlock\\ExampleFinder' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/ExampleFinder.php', + 'phpDocumentor\\Reflection\\DocBlock\\Serializer' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Serializer.php', + 'phpDocumentor\\Reflection\\DocBlock\\StandardTagFactory' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/StandardTagFactory.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tag' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tag.php', + 'phpDocumentor\\Reflection\\DocBlock\\TagFactory' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/TagFactory.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Author' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Author.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\BaseTag' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/BaseTag.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Covers' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Covers.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Deprecated' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Deprecated.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Example' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Example.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Factory\\StaticMethod' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Factory/StaticMethod.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Formatter' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Formatter.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Formatter\\AlignFormatter' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Formatter/AlignFormatter.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Formatter\\PassthroughFormatter' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Formatter/PassthroughFormatter.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Generic' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Generic.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\InvalidTag' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/InvalidTag.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Link' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Link.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Method' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Method.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Param' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Param.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Property' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Property.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\PropertyRead' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/PropertyRead.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\PropertyWrite' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/PropertyWrite.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Reference\\Fqsen' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Reference/Fqsen.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Reference\\Reference' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Reference/Reference.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Reference\\Url' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Reference/Url.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Return_' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Return_.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\See' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/See.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Since' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Since.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Source' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Source.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\TagWithType' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/TagWithType.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Throws' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Throws.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Uses' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Uses.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Var_' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Var_.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Version' => $vendorDir . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Version.php', + 'phpDocumentor\\Reflection\\Element' => $vendorDir . '/phpdocumentor/reflection-common/src/Element.php', + 'phpDocumentor\\Reflection\\Exception\\PcreException' => $vendorDir . '/phpdocumentor/reflection-docblock/src/Exception/PcreException.php', + 'phpDocumentor\\Reflection\\File' => $vendorDir . '/phpdocumentor/reflection-common/src/File.php', + 'phpDocumentor\\Reflection\\Fqsen' => $vendorDir . '/phpdocumentor/reflection-common/src/Fqsen.php', + 'phpDocumentor\\Reflection\\FqsenResolver' => $vendorDir . '/phpdocumentor/type-resolver/src/FqsenResolver.php', + 'phpDocumentor\\Reflection\\Location' => $vendorDir . '/phpdocumentor/reflection-common/src/Location.php', + 'phpDocumentor\\Reflection\\Project' => $vendorDir . '/phpdocumentor/reflection-common/src/Project.php', + 'phpDocumentor\\Reflection\\ProjectFactory' => $vendorDir . '/phpdocumentor/reflection-common/src/ProjectFactory.php', + 'phpDocumentor\\Reflection\\PseudoType' => $vendorDir . '/phpdocumentor/type-resolver/src/PseudoType.php', + 'phpDocumentor\\Reflection\\PseudoTypes\\False_' => $vendorDir . '/phpdocumentor/type-resolver/src/PseudoTypes/False_.php', + 'phpDocumentor\\Reflection\\PseudoTypes\\True_' => $vendorDir . '/phpdocumentor/type-resolver/src/PseudoTypes/True_.php', + 'phpDocumentor\\Reflection\\Type' => $vendorDir . '/phpdocumentor/type-resolver/src/Type.php', + 'phpDocumentor\\Reflection\\TypeResolver' => $vendorDir . '/phpdocumentor/type-resolver/src/TypeResolver.php', + 'phpDocumentor\\Reflection\\Types\\AbstractList' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/AbstractList.php', + 'phpDocumentor\\Reflection\\Types\\AggregatedType' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/AggregatedType.php', + 'phpDocumentor\\Reflection\\Types\\Array_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Array_.php', + 'phpDocumentor\\Reflection\\Types\\Boolean' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Boolean.php', + 'phpDocumentor\\Reflection\\Types\\Callable_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Callable_.php', + 'phpDocumentor\\Reflection\\Types\\ClassString' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/ClassString.php', + 'phpDocumentor\\Reflection\\Types\\Collection' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Collection.php', + 'phpDocumentor\\Reflection\\Types\\Compound' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Compound.php', + 'phpDocumentor\\Reflection\\Types\\Context' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Context.php', + 'phpDocumentor\\Reflection\\Types\\ContextFactory' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/ContextFactory.php', + 'phpDocumentor\\Reflection\\Types\\Expression' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Expression.php', + 'phpDocumentor\\Reflection\\Types\\Float_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Float_.php', + 'phpDocumentor\\Reflection\\Types\\Integer' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Integer.php', + 'phpDocumentor\\Reflection\\Types\\Intersection' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Intersection.php', + 'phpDocumentor\\Reflection\\Types\\Iterable_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Iterable_.php', + 'phpDocumentor\\Reflection\\Types\\Mixed_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Mixed_.php', + 'phpDocumentor\\Reflection\\Types\\Null_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Null_.php', + 'phpDocumentor\\Reflection\\Types\\Nullable' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Nullable.php', + 'phpDocumentor\\Reflection\\Types\\Object_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Object_.php', + 'phpDocumentor\\Reflection\\Types\\Parent_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Parent_.php', + 'phpDocumentor\\Reflection\\Types\\Resource_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Resource_.php', + 'phpDocumentor\\Reflection\\Types\\Scalar' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Scalar.php', + 'phpDocumentor\\Reflection\\Types\\Self_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Self_.php', + 'phpDocumentor\\Reflection\\Types\\Static_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Static_.php', + 'phpDocumentor\\Reflection\\Types\\String_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/String_.php', + 'phpDocumentor\\Reflection\\Types\\This' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/This.php', + 'phpDocumentor\\Reflection\\Types\\Void_' => $vendorDir . '/phpdocumentor/type-resolver/src/Types/Void_.php', + 'phpDocumentor\\Reflection\\Utils' => $vendorDir . '/phpdocumentor/reflection-docblock/src/Utils.php', 'voku\\cache\\AdapterApc' => $vendorDir . '/voku/simple-cache/src/voku/cache/AdapterApc.php', 'voku\\cache\\AdapterApcu' => $vendorDir . '/voku/simple-cache/src/voku/cache/AdapterApcu.php', 'voku\\cache\\AdapterArray' => $vendorDir . '/voku/simple-cache/src/voku/cache/AdapterArray.php', @@ -284,9 +1104,15 @@ 'voku\\cache\\CacheAdapterAutoManager' => $vendorDir . '/voku/simple-cache/src/voku/cache/CacheAdapterAutoManager.php', 'voku\\cache\\CacheChain' => $vendorDir . '/voku/simple-cache/src/voku/cache/CacheChain.php', 'voku\\cache\\CachePsr16' => $vendorDir . '/voku/simple-cache/src/voku/cache/CachePsr16.php', + 'voku\\cache\\Exception\\ChmodException' => $vendorDir . '/voku/simple-cache/src/voku/cache/Exception/ChmodException.php', + 'voku\\cache\\Exception\\FileErrorExceptionInterface' => $vendorDir . '/voku/simple-cache/src/voku/cache/Exception/FileErrorExceptionInterface.php', 'voku\\cache\\Exception\\InvalidArgumentException' => $vendorDir . '/voku/simple-cache/src/voku/cache/Exception/InvalidArgumentException.php', + 'voku\\cache\\Exception\\RenameException' => $vendorDir . '/voku/simple-cache/src/voku/cache/Exception/RenameException.php', + 'voku\\cache\\Exception\\RuntimeException' => $vendorDir . '/voku/simple-cache/src/voku/cache/Exception/RuntimeException.php', + 'voku\\cache\\Exception\\WriteContentException' => $vendorDir . '/voku/simple-cache/src/voku/cache/Exception/WriteContentException.php', 'voku\\cache\\SerializerDefault' => $vendorDir . '/voku/simple-cache/src/voku/cache/SerializerDefault.php', 'voku\\cache\\SerializerIgbinary' => $vendorDir . '/voku/simple-cache/src/voku/cache/SerializerIgbinary.php', + 'voku\\cache\\SerializerMsgpack' => $vendorDir . '/voku/simple-cache/src/voku/cache/SerializerMsgpack.php', 'voku\\cache\\SerializerNo' => $vendorDir . '/voku/simple-cache/src/voku/cache/SerializerNo.php', 'voku\\cache\\iAdapter' => $vendorDir . '/voku/simple-cache/src/voku/cache/iAdapter.php', 'voku\\cache\\iCache' => $vendorDir . '/voku/simple-cache/src/voku/cache/iCache.php', diff --git a/bundled-libs/composer/autoload_files.php b/bundled-libs/composer/autoload_files.php new file mode 100644 index 000000000..0eca3dc89 --- /dev/null +++ b/bundled-libs/composer/autoload_files.php @@ -0,0 +1,20 @@ + $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', + 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '7e9bd612cc444b3eed788ebbe46263a0' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/autoload.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', + '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', + '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php', + '2b9d0f43f9552984cfa82fee95491826' => $vendorDir . '/sabre/event/lib/coroutine.php', + 'd81bab31d3feb45bfe2f283ea3c8fdf7' => $vendorDir . '/sabre/event/lib/Loop/functions.php', + 'a1cce3d26cc15c00fcd0b3354bd72c88' => $vendorDir . '/sabre/event/lib/Promise/functions.php', +); diff --git a/bundled-libs/composer/autoload_namespaces.php b/bundled-libs/composer/autoload_namespaces.php index 10c9b8207..ab43af91d 100644 --- a/bundled-libs/composer/autoload_namespaces.php +++ b/bundled-libs/composer/autoload_namespaces.php @@ -7,4 +7,5 @@ return array( 'Psr\\Log\\' => array($vendorDir . '/psr/log'), + 'JsonMapper' => array($vendorDir . '/netresearch/jsonmapper/src'), ); diff --git a/bundled-libs/composer/autoload_psr4.php b/bundled-libs/composer/autoload_psr4.php index 8615aadad..e4b19bc26 100644 --- a/bundled-libs/composer/autoload_psr4.php +++ b/bundled-libs/composer/autoload_psr4.php @@ -7,8 +7,27 @@ return array( 'voku\\cache\\' => array($vendorDir . '/voku/simple-cache/src/voku/cache'), - 'Zend\\Stdlib\\' => array($vendorDir . '/zendframework/zend-stdlib/src'), - 'Zend\\Db\\' => array($vendorDir . '/zendframework/zend-db/src'), + 'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/type-resolver/src', $vendorDir . '/phpdocumentor/reflection-docblock/src'), + 'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'), + 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), + 'Symfony\\Polyfill\\Php73\\' => array($vendorDir . '/symfony/polyfill-php73'), + 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'), + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'), + 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), + 'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'), + 'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'), + 'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'), + 'Sabre\\Event\\' => array($vendorDir . '/sabre/event/lib'), 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), + 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), + 'Phan\\' => array($vendorDir . '/phan/phan/src/Phan'), + 'Microsoft\\PhpParser\\' => array($vendorDir . '/microsoft/tolerant-php-parser/src'), + 'Laminas\\ZendFrameworkBridge\\' => array($vendorDir . '/laminas/laminas-zendframework-bridge/src'), + 'Laminas\\Stdlib\\' => array($vendorDir . '/laminas/laminas-stdlib/src'), + 'Laminas\\Db\\' => array($vendorDir . '/laminas/laminas-db/src'), 'Katzgrau\\KLogger\\' => array($vendorDir . '/katzgrau/klogger/src'), + 'Composer\\XdebugHandler\\' => array($vendorDir . '/composer/xdebug-handler/src'), + 'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'), + 'AdvancedJsonRpc\\' => array($vendorDir . '/felixfbecker/advanced-json-rpc/lib'), ); diff --git a/bundled-libs/composer/autoload_real.php b/bundled-libs/composer/autoload_real.php index 907af5b15..a56b91674 100644 --- a/bundled-libs/composer/autoload_real.php +++ b/bundled-libs/composer/autoload_real.php @@ -13,19 +13,24 @@ public static function loadClassLoader($class) } } + /** + * @return \Composer\Autoload\ClassLoader + */ public static function getLoader() { if (null !== self::$loader) { return self::$loader; } + require __DIR__ . '/platform_check.php'; + spl_autoload_register(array('ComposerAutoloaderInitcbda25b16bb8365467298ce193f0f30c', 'loadClassLoader'), true, true); - self::$loader = $loader = new \Composer\Autoload\ClassLoader(); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); spl_autoload_unregister(array('ComposerAutoloaderInitcbda25b16bb8365467298ce193f0f30c', 'loadClassLoader')); $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitcbda25b16bb8365467298ce193f0f30c::getInitializer($loader)); } else { @@ -45,9 +50,27 @@ public static function getLoader() } } - $loader->setApcuPrefix('WIBdJgt9/OFG9RxODXgrL'); + $loader->setApcuPrefix('MGe62alYSEjsq/QznR33p'); $loader->register(true); + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInitcbda25b16bb8365467298ce193f0f30c::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequirecbda25b16bb8365467298ce193f0f30c($fileIdentifier, $file); + } + return $loader; } } + +function composerRequirecbda25b16bb8365467298ce193f0f30c($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/bundled-libs/composer/autoload_static.php b/bundled-libs/composer/autoload_static.php index 6bd004ad7..bad8568fd 100644 --- a/bundled-libs/composer/autoload_static.php +++ b/bundled-libs/composer/autoload_static.php @@ -6,24 +6,75 @@ class ComposerStaticInitcbda25b16bb8365467298ce193f0f30c { + public static $files = array ( + '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', + 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '7e9bd612cc444b3eed788ebbe46263a0' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/autoload.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', + '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', + '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php', + '2b9d0f43f9552984cfa82fee95491826' => __DIR__ . '/..' . '/sabre/event/lib/coroutine.php', + 'd81bab31d3feb45bfe2f283ea3c8fdf7' => __DIR__ . '/..' . '/sabre/event/lib/Loop/functions.php', + 'a1cce3d26cc15c00fcd0b3354bd72c88' => __DIR__ . '/..' . '/sabre/event/lib/Promise/functions.php', + ); + public static $prefixLengthsPsr4 = array ( 'v' => array ( 'voku\\cache\\' => 11, ), - 'Z' => + 'p' => + array ( + 'phpDocumentor\\Reflection\\' => 25, + ), + 'W' => array ( - 'Zend\\Stdlib\\' => 12, - 'Zend\\Db\\' => 8, + 'Webmozart\\Assert\\' => 17, + ), + 'S' => + array ( + 'Symfony\\Polyfill\\Php80\\' => 23, + 'Symfony\\Polyfill\\Php73\\' => 23, + 'Symfony\\Polyfill\\Mbstring\\' => 26, + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33, + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => 31, + 'Symfony\\Polyfill\\Ctype\\' => 23, + 'Symfony\\Contracts\\Service\\' => 26, + 'Symfony\\Component\\String\\' => 25, + 'Symfony\\Component\\Console\\' => 26, + 'Sabre\\Event\\' => 12, ), 'P' => array ( 'Psr\\SimpleCache\\' => 16, + 'Psr\\Container\\' => 14, + 'Phan\\' => 5, + ), + 'M' => + array ( + 'Microsoft\\PhpParser\\' => 20, + ), + 'L' => + array ( + 'Laminas\\ZendFrameworkBridge\\' => 28, + 'Laminas\\Stdlib\\' => 15, + 'Laminas\\Db\\' => 11, ), 'K' => array ( 'Katzgrau\\KLogger\\' => 17, ), + 'C' => + array ( + 'Composer\\XdebugHandler\\' => 23, + 'Composer\\Semver\\' => 16, + ), + 'A' => + array ( + 'AdvancedJsonRpc\\' => 16, + ), ); public static $prefixDirsPsr4 = array ( @@ -31,22 +82,100 @@ class ComposerStaticInitcbda25b16bb8365467298ce193f0f30c array ( 0 => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache', ), - 'Zend\\Stdlib\\' => + 'phpDocumentor\\Reflection\\' => + array ( + 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src', + 1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src', + 2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src', + ), + 'Webmozart\\Assert\\' => + array ( + 0 => __DIR__ . '/..' . '/webmozart/assert/src', + ), + 'Symfony\\Polyfill\\Php80\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', + ), + 'Symfony\\Polyfill\\Php73\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php73', + ), + 'Symfony\\Polyfill\\Mbstring\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', + ), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer', + ), + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme', + ), + 'Symfony\\Polyfill\\Ctype\\' => array ( - 0 => __DIR__ . '/..' . '/zendframework/zend-stdlib/src', + 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', ), - 'Zend\\Db\\' => + 'Symfony\\Contracts\\Service\\' => array ( - 0 => __DIR__ . '/..' . '/zendframework/zend-db/src', + 0 => __DIR__ . '/..' . '/symfony/service-contracts', + ), + 'Symfony\\Component\\String\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/string', + ), + 'Symfony\\Component\\Console\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/console', + ), + 'Sabre\\Event\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/event/lib', ), 'Psr\\SimpleCache\\' => array ( 0 => __DIR__ . '/..' . '/psr/simple-cache/src', ), + 'Psr\\Container\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/container/src', + ), + 'Phan\\' => + array ( + 0 => __DIR__ . '/..' . '/phan/phan/src/Phan', + ), + 'Microsoft\\PhpParser\\' => + array ( + 0 => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src', + ), + 'Laminas\\ZendFrameworkBridge\\' => + array ( + 0 => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src', + ), + 'Laminas\\Stdlib\\' => + array ( + 0 => __DIR__ . '/..' . '/laminas/laminas-stdlib/src', + ), + 'Laminas\\Db\\' => + array ( + 0 => __DIR__ . '/..' . '/laminas/laminas-db/src', + ), 'Katzgrau\\KLogger\\' => array ( 0 => __DIR__ . '/..' . '/katzgrau/klogger/src', ), + 'Composer\\XdebugHandler\\' => + array ( + 0 => __DIR__ . '/..' . '/composer/xdebug-handler/src', + ), + 'Composer\\Semver\\' => + array ( + 0 => __DIR__ . '/..' . '/composer/semver/src', + ), + 'AdvancedJsonRpc\\' => + array ( + 0 => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib', + ), ); public static $prefixesPsr0 = array ( @@ -57,10 +186,871 @@ class ComposerStaticInitcbda25b16bb8365467298ce193f0f30c 0 => __DIR__ . '/..' . '/psr/log', ), ), + 'J' => + array ( + 'JsonMapper' => + array ( + 0 => __DIR__ . '/..' . '/netresearch/jsonmapper/src', + ), + ), ); public static $classMap = array ( + 'AdvancedJsonRpc\\Dispatcher' => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib/Dispatcher.php', + 'AdvancedJsonRpc\\Error' => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib/Error.php', + 'AdvancedJsonRpc\\ErrorCode' => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib/ErrorCode.php', + 'AdvancedJsonRpc\\ErrorResponse' => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib/ErrorResponse.php', + 'AdvancedJsonRpc\\Message' => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib/Message.php', + 'AdvancedJsonRpc\\Notification' => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib/Notification.php', + 'AdvancedJsonRpc\\Request' => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib/Request.php', + 'AdvancedJsonRpc\\Response' => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib/Response.php', + 'AdvancedJsonRpc\\SuccessResponse' => __DIR__ . '/..' . '/felixfbecker/advanced-json-rpc/lib/SuccessResponse.php', + 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'Composer\\Semver\\Comparator' => __DIR__ . '/..' . '/composer/semver/src/Comparator.php', + 'Composer\\Semver\\CompilingMatcher' => __DIR__ . '/..' . '/composer/semver/src/CompilingMatcher.php', + 'Composer\\Semver\\Constraint\\Bound' => __DIR__ . '/..' . '/composer/semver/src/Constraint/Bound.php', + 'Composer\\Semver\\Constraint\\Constraint' => __DIR__ . '/..' . '/composer/semver/src/Constraint/Constraint.php', + 'Composer\\Semver\\Constraint\\ConstraintInterface' => __DIR__ . '/..' . '/composer/semver/src/Constraint/ConstraintInterface.php', + 'Composer\\Semver\\Constraint\\MatchAllConstraint' => __DIR__ . '/..' . '/composer/semver/src/Constraint/MatchAllConstraint.php', + 'Composer\\Semver\\Constraint\\MatchNoneConstraint' => __DIR__ . '/..' . '/composer/semver/src/Constraint/MatchNoneConstraint.php', + 'Composer\\Semver\\Constraint\\MultiConstraint' => __DIR__ . '/..' . '/composer/semver/src/Constraint/MultiConstraint.php', + 'Composer\\Semver\\Interval' => __DIR__ . '/..' . '/composer/semver/src/Interval.php', + 'Composer\\Semver\\Intervals' => __DIR__ . '/..' . '/composer/semver/src/Intervals.php', + 'Composer\\Semver\\Semver' => __DIR__ . '/..' . '/composer/semver/src/Semver.php', + 'Composer\\Semver\\VersionParser' => __DIR__ . '/..' . '/composer/semver/src/VersionParser.php', + 'Composer\\XdebugHandler\\PhpConfig' => __DIR__ . '/..' . '/composer/xdebug-handler/src/PhpConfig.php', + 'Composer\\XdebugHandler\\Process' => __DIR__ . '/..' . '/composer/xdebug-handler/src/Process.php', + 'Composer\\XdebugHandler\\Status' => __DIR__ . '/..' . '/composer/xdebug-handler/src/Status.php', + 'Composer\\XdebugHandler\\XdebugHandler' => __DIR__ . '/..' . '/composer/xdebug-handler/src/XdebugHandler.php', + 'JsonException' => __DIR__ . '/..' . '/symfony/polyfill-php73/Resources/stubs/JsonException.php', + 'JsonMapper' => __DIR__ . '/..' . '/netresearch/jsonmapper/src/JsonMapper.php', + 'JsonMapper_Exception' => __DIR__ . '/..' . '/netresearch/jsonmapper/src/JsonMapper/Exception.php', 'Katzgrau\\KLogger\\Logger' => __DIR__ . '/..' . '/katzgrau/klogger/src/Logger.php', + 'Laminas\\Db\\Adapter\\Adapter' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Adapter.php', + 'Laminas\\Db\\Adapter\\AdapterAbstractServiceFactory' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/AdapterAbstractServiceFactory.php', + 'Laminas\\Db\\Adapter\\AdapterAwareInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/AdapterAwareInterface.php', + 'Laminas\\Db\\Adapter\\AdapterAwareTrait' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/AdapterAwareTrait.php', + 'Laminas\\Db\\Adapter\\AdapterInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/AdapterInterface.php', + 'Laminas\\Db\\Adapter\\AdapterServiceDelegator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/AdapterServiceDelegator.php', + 'Laminas\\Db\\Adapter\\AdapterServiceFactory' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/AdapterServiceFactory.php', + 'Laminas\\Db\\Adapter\\Driver\\AbstractConnection' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/AbstractConnection.php', + 'Laminas\\Db\\Adapter\\Driver\\ConnectionInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/ConnectionInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\DriverInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/DriverInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\Feature\\AbstractFeature' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Feature/AbstractFeature.php', + 'Laminas\\Db\\Adapter\\Driver\\Feature\\DriverFeatureInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Feature/DriverFeatureInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\IbmDb2\\Connection' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\IbmDb2\\IbmDb2' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/IbmDb2/IbmDb2.php', + 'Laminas\\Db\\Adapter\\Driver\\IbmDb2\\Result' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\IbmDb2\\Statement' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\Mysqli\\Connection' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Mysqli/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Mysqli\\Mysqli' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Mysqli/Mysqli.php', + 'Laminas\\Db\\Adapter\\Driver\\Mysqli\\Result' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Mysqli/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Mysqli\\Statement' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Mysqli/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Connection' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Feature\\RowCounter' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Feature/RowCounter.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Oci8' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Oci8.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Result' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Oci8\\Statement' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Oci8/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Connection' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Feature\\OracleRowCounter' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/OracleRowCounter.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Feature\\SqliteRowCounter' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Pdo' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Pdo.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Result' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Pdo\\Statement' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pdo/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\Pgsql\\Connection' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pgsql/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Pgsql\\Pgsql' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pgsql/Pgsql.php', + 'Laminas\\Db\\Adapter\\Driver\\Pgsql\\Result' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pgsql/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Pgsql\\Statement' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Pgsql/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\ResultInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/ResultInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Connection' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Connection.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Exception\\ErrorException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ErrorException.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Result' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Result.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Sqlsrv' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Sqlsrv.php', + 'Laminas\\Db\\Adapter\\Driver\\Sqlsrv\\Statement' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Statement.php', + 'Laminas\\Db\\Adapter\\Driver\\StatementInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Driver/StatementInterface.php', + 'Laminas\\Db\\Adapter\\Exception\\ErrorException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Exception/ErrorException.php', + 'Laminas\\Db\\Adapter\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Exception/ExceptionInterface.php', + 'Laminas\\Db\\Adapter\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\Adapter\\Exception\\InvalidConnectionParametersException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Exception/InvalidConnectionParametersException.php', + 'Laminas\\Db\\Adapter\\Exception\\InvalidQueryException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Exception/InvalidQueryException.php', + 'Laminas\\Db\\Adapter\\Exception\\RuntimeException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Exception/RuntimeException.php', + 'Laminas\\Db\\Adapter\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Exception/UnexpectedValueException.php', + 'Laminas\\Db\\Adapter\\ParameterContainer' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/ParameterContainer.php', + 'Laminas\\Db\\Adapter\\Platform\\AbstractPlatform' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Platform/AbstractPlatform.php', + 'Laminas\\Db\\Adapter\\Platform\\IbmDb2' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Platform/IbmDb2.php', + 'Laminas\\Db\\Adapter\\Platform\\Mysql' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Platform/Mysql.php', + 'Laminas\\Db\\Adapter\\Platform\\Oracle' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Platform/Oracle.php', + 'Laminas\\Db\\Adapter\\Platform\\PlatformInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Platform/PlatformInterface.php', + 'Laminas\\Db\\Adapter\\Platform\\Postgresql' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Platform/Postgresql.php', + 'Laminas\\Db\\Adapter\\Platform\\Sql92' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Platform/Sql92.php', + 'Laminas\\Db\\Adapter\\Platform\\SqlServer' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Platform/SqlServer.php', + 'Laminas\\Db\\Adapter\\Platform\\Sqlite' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Platform/Sqlite.php', + 'Laminas\\Db\\Adapter\\Profiler\\Profiler' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Profiler/Profiler.php', + 'Laminas\\Db\\Adapter\\Profiler\\ProfilerAwareInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Profiler/ProfilerAwareInterface.php', + 'Laminas\\Db\\Adapter\\Profiler\\ProfilerInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/Profiler/ProfilerInterface.php', + 'Laminas\\Db\\Adapter\\StatementContainer' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/StatementContainer.php', + 'Laminas\\Db\\Adapter\\StatementContainerInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Adapter/StatementContainerInterface.php', + 'Laminas\\Db\\ConfigProvider' => __DIR__ . '/..' . '/laminas/laminas-db/src/ConfigProvider.php', + 'Laminas\\Db\\Exception\\ErrorException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Exception/ErrorException.php', + 'Laminas\\Db\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Exception/ExceptionInterface.php', + 'Laminas\\Db\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\Exception\\RuntimeException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Exception/RuntimeException.php', + 'Laminas\\Db\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Exception/UnexpectedValueException.php', + 'Laminas\\Db\\Metadata\\Metadata' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Metadata.php', + 'Laminas\\Db\\Metadata\\MetadataInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/MetadataInterface.php', + 'Laminas\\Db\\Metadata\\Object\\AbstractTableObject' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Object/AbstractTableObject.php', + 'Laminas\\Db\\Metadata\\Object\\ColumnObject' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Object/ColumnObject.php', + 'Laminas\\Db\\Metadata\\Object\\ConstraintKeyObject' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Object/ConstraintKeyObject.php', + 'Laminas\\Db\\Metadata\\Object\\ConstraintObject' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Object/ConstraintObject.php', + 'Laminas\\Db\\Metadata\\Object\\TableObject' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Object/TableObject.php', + 'Laminas\\Db\\Metadata\\Object\\TriggerObject' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Object/TriggerObject.php', + 'Laminas\\Db\\Metadata\\Object\\ViewObject' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Object/ViewObject.php', + 'Laminas\\Db\\Metadata\\Source\\AbstractSource' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Source/AbstractSource.php', + 'Laminas\\Db\\Metadata\\Source\\Factory' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Source/Factory.php', + 'Laminas\\Db\\Metadata\\Source\\MysqlMetadata' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Source/MysqlMetadata.php', + 'Laminas\\Db\\Metadata\\Source\\OracleMetadata' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Source/OracleMetadata.php', + 'Laminas\\Db\\Metadata\\Source\\PostgresqlMetadata' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Source/PostgresqlMetadata.php', + 'Laminas\\Db\\Metadata\\Source\\SqlServerMetadata' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Source/SqlServerMetadata.php', + 'Laminas\\Db\\Metadata\\Source\\SqliteMetadata' => __DIR__ . '/..' . '/laminas/laminas-db/src/Metadata/Source/SqliteMetadata.php', + 'Laminas\\Db\\Module' => __DIR__ . '/..' . '/laminas/laminas-db/src/Module.php', + 'Laminas\\Db\\ResultSet\\AbstractResultSet' => __DIR__ . '/..' . '/laminas/laminas-db/src/ResultSet/AbstractResultSet.php', + 'Laminas\\Db\\ResultSet\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/ResultSet/Exception/ExceptionInterface.php', + 'Laminas\\Db\\ResultSet\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/laminas/laminas-db/src/ResultSet/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\ResultSet\\Exception\\RuntimeException' => __DIR__ . '/..' . '/laminas/laminas-db/src/ResultSet/Exception/RuntimeException.php', + 'Laminas\\Db\\ResultSet\\HydratingResultSet' => __DIR__ . '/..' . '/laminas/laminas-db/src/ResultSet/HydratingResultSet.php', + 'Laminas\\Db\\ResultSet\\ResultSet' => __DIR__ . '/..' . '/laminas/laminas-db/src/ResultSet/ResultSet.php', + 'Laminas\\Db\\ResultSet\\ResultSetInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/ResultSet/ResultSetInterface.php', + 'Laminas\\Db\\RowGateway\\AbstractRowGateway' => __DIR__ . '/..' . '/laminas/laminas-db/src/RowGateway/AbstractRowGateway.php', + 'Laminas\\Db\\RowGateway\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/RowGateway/Exception/ExceptionInterface.php', + 'Laminas\\Db\\RowGateway\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/laminas/laminas-db/src/RowGateway/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\RowGateway\\Exception\\RuntimeException' => __DIR__ . '/..' . '/laminas/laminas-db/src/RowGateway/Exception/RuntimeException.php', + 'Laminas\\Db\\RowGateway\\Feature\\AbstractFeature' => __DIR__ . '/..' . '/laminas/laminas-db/src/RowGateway/Feature/AbstractFeature.php', + 'Laminas\\Db\\RowGateway\\Feature\\FeatureSet' => __DIR__ . '/..' . '/laminas/laminas-db/src/RowGateway/Feature/FeatureSet.php', + 'Laminas\\Db\\RowGateway\\RowGateway' => __DIR__ . '/..' . '/laminas/laminas-db/src/RowGateway/RowGateway.php', + 'Laminas\\Db\\RowGateway\\RowGatewayInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/RowGateway/RowGatewayInterface.php', + 'Laminas\\Db\\Sql\\AbstractExpression' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/AbstractExpression.php', + 'Laminas\\Db\\Sql\\AbstractPreparableSql' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/AbstractPreparableSql.php', + 'Laminas\\Db\\Sql\\AbstractSql' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/AbstractSql.php', + 'Laminas\\Db\\Sql\\Combine' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Combine.php', + 'Laminas\\Db\\Sql\\Ddl\\AlterTable' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/AlterTable.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\AbstractLengthColumn' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/AbstractLengthColumn.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\AbstractPrecisionColumn' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/AbstractPrecisionColumn.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\AbstractTimestampColumn' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/AbstractTimestampColumn.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\BigInteger' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/BigInteger.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Binary' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Binary.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Blob' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Blob.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Boolean' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Boolean.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Char' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Char.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Column' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Column.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\ColumnInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/ColumnInterface.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Date' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Date.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Datetime' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Datetime.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Decimal' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Decimal.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Floating' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Floating.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Integer' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Integer.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Text' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Text.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Time' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Time.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Timestamp' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Timestamp.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Varbinary' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Varbinary.php', + 'Laminas\\Db\\Sql\\Ddl\\Column\\Varchar' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Column/Varchar.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\AbstractConstraint' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Constraint/AbstractConstraint.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\Check' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Constraint/Check.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\ConstraintInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Constraint/ConstraintInterface.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\ForeignKey' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Constraint/ForeignKey.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\PrimaryKey' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Constraint/PrimaryKey.php', + 'Laminas\\Db\\Sql\\Ddl\\Constraint\\UniqueKey' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Constraint/UniqueKey.php', + 'Laminas\\Db\\Sql\\Ddl\\CreateTable' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/CreateTable.php', + 'Laminas\\Db\\Sql\\Ddl\\DropTable' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/DropTable.php', + 'Laminas\\Db\\Sql\\Ddl\\Index\\AbstractIndex' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Index/AbstractIndex.php', + 'Laminas\\Db\\Sql\\Ddl\\Index\\Index' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/Index/Index.php', + 'Laminas\\Db\\Sql\\Ddl\\SqlInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Ddl/SqlInterface.php', + 'Laminas\\Db\\Sql\\Delete' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Delete.php', + 'Laminas\\Db\\Sql\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Exception/ExceptionInterface.php', + 'Laminas\\Db\\Sql\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\Sql\\Exception\\RuntimeException' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Exception/RuntimeException.php', + 'Laminas\\Db\\Sql\\Expression' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Expression.php', + 'Laminas\\Db\\Sql\\ExpressionInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/ExpressionInterface.php', + 'Laminas\\Db\\Sql\\Having' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Having.php', + 'Laminas\\Db\\Sql\\Insert' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Insert.php', + 'Laminas\\Db\\Sql\\InsertIgnore' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/InsertIgnore.php', + 'Laminas\\Db\\Sql\\Join' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Join.php', + 'Laminas\\Db\\Sql\\Literal' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Literal.php', + 'Laminas\\Db\\Sql\\Platform\\AbstractPlatform' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/AbstractPlatform.php', + 'Laminas\\Db\\Sql\\Platform\\IbmDb2\\IbmDb2' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/IbmDb2/IbmDb2.php', + 'Laminas\\Db\\Sql\\Platform\\IbmDb2\\SelectDecorator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/IbmDb2/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Mysql\\Ddl\\AlterTableDecorator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Mysql\\Ddl\\CreateTableDecorator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Mysql\\Mysql' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/Mysql/Mysql.php', + 'Laminas\\Db\\Sql\\Platform\\Mysql\\SelectDecorator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/Mysql/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Oracle\\Oracle' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/Oracle/Oracle.php', + 'Laminas\\Db\\Sql\\Platform\\Oracle\\SelectDecorator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/Oracle/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Platform' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/Platform.php', + 'Laminas\\Db\\Sql\\Platform\\PlatformDecoratorInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/PlatformDecoratorInterface.php', + 'Laminas\\Db\\Sql\\Platform\\SqlServer\\Ddl\\CreateTableDecorator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/SqlServer/Ddl/CreateTableDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\SqlServer\\SelectDecorator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/SqlServer/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\SqlServer\\SqlServer' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/SqlServer/SqlServer.php', + 'Laminas\\Db\\Sql\\Platform\\Sqlite\\SelectDecorator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/Sqlite/SelectDecorator.php', + 'Laminas\\Db\\Sql\\Platform\\Sqlite\\Sqlite' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Platform/Sqlite/Sqlite.php', + 'Laminas\\Db\\Sql\\Predicate\\Between' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/Between.php', + 'Laminas\\Db\\Sql\\Predicate\\Expression' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/Expression.php', + 'Laminas\\Db\\Sql\\Predicate\\In' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/In.php', + 'Laminas\\Db\\Sql\\Predicate\\IsNotNull' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/IsNotNull.php', + 'Laminas\\Db\\Sql\\Predicate\\IsNull' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/IsNull.php', + 'Laminas\\Db\\Sql\\Predicate\\Like' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/Like.php', + 'Laminas\\Db\\Sql\\Predicate\\Literal' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/Literal.php', + 'Laminas\\Db\\Sql\\Predicate\\NotBetween' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/NotBetween.php', + 'Laminas\\Db\\Sql\\Predicate\\NotIn' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/NotIn.php', + 'Laminas\\Db\\Sql\\Predicate\\NotLike' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/NotLike.php', + 'Laminas\\Db\\Sql\\Predicate\\Operator' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/Operator.php', + 'Laminas\\Db\\Sql\\Predicate\\Predicate' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/Predicate.php', + 'Laminas\\Db\\Sql\\Predicate\\PredicateInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/PredicateInterface.php', + 'Laminas\\Db\\Sql\\Predicate\\PredicateSet' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Predicate/PredicateSet.php', + 'Laminas\\Db\\Sql\\PreparableSqlInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/PreparableSqlInterface.php', + 'Laminas\\Db\\Sql\\Select' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Select.php', + 'Laminas\\Db\\Sql\\Sql' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Sql.php', + 'Laminas\\Db\\Sql\\SqlInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/SqlInterface.php', + 'Laminas\\Db\\Sql\\TableIdentifier' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/TableIdentifier.php', + 'Laminas\\Db\\Sql\\Update' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Update.php', + 'Laminas\\Db\\Sql\\Where' => __DIR__ . '/..' . '/laminas/laminas-db/src/Sql/Where.php', + 'Laminas\\Db\\TableGateway\\AbstractTableGateway' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/AbstractTableGateway.php', + 'Laminas\\Db\\TableGateway\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Exception/ExceptionInterface.php', + 'Laminas\\Db\\TableGateway\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Exception/InvalidArgumentException.php', + 'Laminas\\Db\\TableGateway\\Exception\\RuntimeException' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Exception/RuntimeException.php', + 'Laminas\\Db\\TableGateway\\Feature\\AbstractFeature' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/AbstractFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\EventFeature' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/EventFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\EventFeatureEventsInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/EventFeatureEventsInterface.php', + 'Laminas\\Db\\TableGateway\\Feature\\EventFeature\\TableGatewayEvent' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php', + 'Laminas\\Db\\TableGateway\\Feature\\FeatureSet' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/FeatureSet.php', + 'Laminas\\Db\\TableGateway\\Feature\\GlobalAdapterFeature' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/GlobalAdapterFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\MasterSlaveFeature' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/MasterSlaveFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\MetadataFeature' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/MetadataFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\RowGatewayFeature' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/RowGatewayFeature.php', + 'Laminas\\Db\\TableGateway\\Feature\\SequenceFeature' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/Feature/SequenceFeature.php', + 'Laminas\\Db\\TableGateway\\TableGateway' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/TableGateway.php', + 'Laminas\\Db\\TableGateway\\TableGatewayInterface' => __DIR__ . '/..' . '/laminas/laminas-db/src/TableGateway/TableGatewayInterface.php', + 'Laminas\\Stdlib\\AbstractOptions' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/AbstractOptions.php', + 'Laminas\\Stdlib\\ArrayObject' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ArrayObject.php', + 'Laminas\\Stdlib\\ArraySerializableInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ArraySerializableInterface.php', + 'Laminas\\Stdlib\\ArrayStack' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ArrayStack.php', + 'Laminas\\Stdlib\\ArrayUtils' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ArrayUtils.php', + 'Laminas\\Stdlib\\ArrayUtils\\MergeRemoveKey' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ArrayUtils/MergeRemoveKey.php', + 'Laminas\\Stdlib\\ArrayUtils\\MergeReplaceKey' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ArrayUtils/MergeReplaceKey.php', + 'Laminas\\Stdlib\\ArrayUtils\\MergeReplaceKeyInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ArrayUtils/MergeReplaceKeyInterface.php', + 'Laminas\\Stdlib\\ConsoleHelper' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ConsoleHelper.php', + 'Laminas\\Stdlib\\DispatchableInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/DispatchableInterface.php', + 'Laminas\\Stdlib\\ErrorHandler' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ErrorHandler.php', + 'Laminas\\Stdlib\\Exception\\BadMethodCallException' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Exception/BadMethodCallException.php', + 'Laminas\\Stdlib\\Exception\\DomainException' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Exception/DomainException.php', + 'Laminas\\Stdlib\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Exception/ExceptionInterface.php', + 'Laminas\\Stdlib\\Exception\\ExtensionNotLoadedException' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Exception/ExtensionNotLoadedException.php', + 'Laminas\\Stdlib\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Exception/InvalidArgumentException.php', + 'Laminas\\Stdlib\\Exception\\LogicException' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Exception/LogicException.php', + 'Laminas\\Stdlib\\Exception\\RuntimeException' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Exception/RuntimeException.php', + 'Laminas\\Stdlib\\FastPriorityQueue' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/FastPriorityQueue.php', + 'Laminas\\Stdlib\\Glob' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Glob.php', + 'Laminas\\Stdlib\\Guard\\AllGuardsTrait' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Guard/AllGuardsTrait.php', + 'Laminas\\Stdlib\\Guard\\ArrayOrTraversableGuardTrait' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Guard/ArrayOrTraversableGuardTrait.php', + 'Laminas\\Stdlib\\Guard\\EmptyGuardTrait' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Guard/EmptyGuardTrait.php', + 'Laminas\\Stdlib\\Guard\\NullGuardTrait' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Guard/NullGuardTrait.php', + 'Laminas\\Stdlib\\InitializableInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/InitializableInterface.php', + 'Laminas\\Stdlib\\JsonSerializable' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/JsonSerializable.php', + 'Laminas\\Stdlib\\Message' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Message.php', + 'Laminas\\Stdlib\\MessageInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/MessageInterface.php', + 'Laminas\\Stdlib\\ParameterObjectInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ParameterObjectInterface.php', + 'Laminas\\Stdlib\\Parameters' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Parameters.php', + 'Laminas\\Stdlib\\ParametersInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ParametersInterface.php', + 'Laminas\\Stdlib\\PriorityList' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/PriorityList.php', + 'Laminas\\Stdlib\\PriorityQueue' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/PriorityQueue.php', + 'Laminas\\Stdlib\\Request' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Request.php', + 'Laminas\\Stdlib\\RequestInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/RequestInterface.php', + 'Laminas\\Stdlib\\Response' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/Response.php', + 'Laminas\\Stdlib\\ResponseInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/ResponseInterface.php', + 'Laminas\\Stdlib\\SplPriorityQueue' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/SplPriorityQueue.php', + 'Laminas\\Stdlib\\SplQueue' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/SplQueue.php', + 'Laminas\\Stdlib\\SplStack' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/SplStack.php', + 'Laminas\\Stdlib\\StringUtils' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/StringUtils.php', + 'Laminas\\Stdlib\\StringWrapper\\AbstractStringWrapper' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/StringWrapper/AbstractStringWrapper.php', + 'Laminas\\Stdlib\\StringWrapper\\Iconv' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/StringWrapper/Iconv.php', + 'Laminas\\Stdlib\\StringWrapper\\Intl' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/StringWrapper/Intl.php', + 'Laminas\\Stdlib\\StringWrapper\\MbString' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/StringWrapper/MbString.php', + 'Laminas\\Stdlib\\StringWrapper\\Native' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/StringWrapper/Native.php', + 'Laminas\\Stdlib\\StringWrapper\\StringWrapperInterface' => __DIR__ . '/..' . '/laminas/laminas-stdlib/src/StringWrapper/StringWrapperInterface.php', + 'Laminas\\ZendFrameworkBridge\\Autoloader' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/Autoloader.php', + 'Laminas\\ZendFrameworkBridge\\ConfigPostProcessor' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php', + 'Laminas\\ZendFrameworkBridge\\Module' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/Module.php', + 'Laminas\\ZendFrameworkBridge\\Replacements' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/Replacements.php', + 'Laminas\\ZendFrameworkBridge\\RewriteRules' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/RewriteRules.php', + 'Microsoft\\PhpParser\\CharacterCodes' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/CharacterCodes.php', + 'Microsoft\\PhpParser\\ClassLike' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/ClassLike.php', + 'Microsoft\\PhpParser\\Diagnostic' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Diagnostic.php', + 'Microsoft\\PhpParser\\DiagnosticKind' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/DiagnosticKind.php', + 'Microsoft\\PhpParser\\DiagnosticsProvider' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/DiagnosticsProvider.php', + 'Microsoft\\PhpParser\\FilePositionMap' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/FilePositionMap.php', + 'Microsoft\\PhpParser\\FunctionLike' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/FunctionLike.php', + 'Microsoft\\PhpParser\\LineCharacterPosition' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/LineCharacterPosition.php', + 'Microsoft\\PhpParser\\MissingToken' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/MissingToken.php', + 'Microsoft\\PhpParser\\ModifiedTypeInterface' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/ModifiedTypeInterface.php', + 'Microsoft\\PhpParser\\ModifiedTypeTrait' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/ModifiedTypeTrait.php', + 'Microsoft\\PhpParser\\NamespacedNameInterface' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/NamespacedNameInterface.php', + 'Microsoft\\PhpParser\\NamespacedNameTrait' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/NamespacedNameTrait.php', + 'Microsoft\\PhpParser\\Node' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node.php', + 'Microsoft\\PhpParser\\Node\\AnonymousFunctionUseClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/AnonymousFunctionUseClause.php', + 'Microsoft\\PhpParser\\Node\\ArrayElement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ArrayElement.php', + 'Microsoft\\PhpParser\\Node\\Attribute' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Attribute.php', + 'Microsoft\\PhpParser\\Node\\AttributeGroup' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/AttributeGroup.php', + 'Microsoft\\PhpParser\\Node\\CaseStatementNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/CaseStatementNode.php', + 'Microsoft\\PhpParser\\Node\\CatchClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/CatchClause.php', + 'Microsoft\\PhpParser\\Node\\ClassBaseClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ClassBaseClause.php', + 'Microsoft\\PhpParser\\Node\\ClassConstDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ClassConstDeclaration.php', + 'Microsoft\\PhpParser\\Node\\ClassInterfaceClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ClassInterfaceClause.php', + 'Microsoft\\PhpParser\\Node\\ClassMembersNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ClassMembersNode.php', + 'Microsoft\\PhpParser\\Node\\ConstElement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ConstElement.php', + 'Microsoft\\PhpParser\\Node\\DeclareDirective' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DeclareDirective.php', + 'Microsoft\\PhpParser\\Node\\DefaultStatementNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DefaultStatementNode.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ArgumentExpressionList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ArgumentExpressionList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ArrayElementList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ArrayElementList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\AttributeElementList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/AttributeElementList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ConstElementList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ConstElementList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\DeclareDirectiveList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/DeclareDirectiveList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ExpressionList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ExpressionList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ListExpressionList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ListExpressionList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\MatchArmConditionList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/MatchArmConditionList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\MatchExpressionArmList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/MatchExpressionArmList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\NamespaceUseClauseList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/NamespaceUseClauseList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\NamespaceUseGroupClauseList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/NamespaceUseGroupClauseList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\ParameterDeclarationList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/ParameterDeclarationList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\QualifiedNameList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/QualifiedNameList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\QualifiedNameParts' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/QualifiedNameParts.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\StaticVariableNameList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/StaticVariableNameList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\TraitSelectOrAliasClauseList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/TraitSelectOrAliasClauseList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\UseVariableNameList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/UseVariableNameList.php', + 'Microsoft\\PhpParser\\Node\\DelimitedList\\VariableNameList' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/DelimitedList/VariableNameList.php', + 'Microsoft\\PhpParser\\Node\\ElseClauseNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ElseClauseNode.php', + 'Microsoft\\PhpParser\\Node\\ElseIfClauseNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ElseIfClauseNode.php', + 'Microsoft\\PhpParser\\Node\\Expression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\AnonymousFunctionCreationExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/AnonymousFunctionCreationExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ArgumentExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ArgumentExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ArrayCreationExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ArrayCreationExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ArrowFunctionCreationExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ArrowFunctionCreationExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\AssignmentExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/AssignmentExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\BinaryExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/BinaryExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\BracedExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/BracedExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\CallExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/CallExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\CastExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/CastExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\CloneExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/CloneExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\EchoExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/EchoExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\EmptyIntrinsicExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/EmptyIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ErrorControlExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ErrorControlExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\EvalIntrinsicExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/EvalIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ExitIntrinsicExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ExitIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\IssetIntrinsicExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/IssetIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ListIntrinsicExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ListIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\MatchExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/MatchExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\MemberAccessExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/MemberAccessExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ObjectCreationExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ObjectCreationExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ParenthesizedExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ParenthesizedExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\PostfixUpdateExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/PostfixUpdateExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\PrefixUpdateExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/PrefixUpdateExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\PrintIntrinsicExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/PrintIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ScopedPropertyAccessExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ScopedPropertyAccessExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ScriptInclusionExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ScriptInclusionExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\SubscriptExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/SubscriptExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\TernaryExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/TernaryExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\ThrowExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/ThrowExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\UnaryExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/UnaryExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\UnaryOpExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/UnaryOpExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\UnsetIntrinsicExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/UnsetIntrinsicExpression.php', + 'Microsoft\\PhpParser\\Node\\Expression\\Variable' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/Variable.php', + 'Microsoft\\PhpParser\\Node\\Expression\\YieldExpression' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Expression/YieldExpression.php', + 'Microsoft\\PhpParser\\Node\\FinallyClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/FinallyClause.php', + 'Microsoft\\PhpParser\\Node\\ForeachKey' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ForeachKey.php', + 'Microsoft\\PhpParser\\Node\\ForeachValue' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ForeachValue.php', + 'Microsoft\\PhpParser\\Node\\FunctionBody' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/FunctionBody.php', + 'Microsoft\\PhpParser\\Node\\FunctionHeader' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/FunctionHeader.php', + 'Microsoft\\PhpParser\\Node\\FunctionReturnType' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/FunctionReturnType.php', + 'Microsoft\\PhpParser\\Node\\FunctionUseClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/FunctionUseClause.php', + 'Microsoft\\PhpParser\\Node\\InterfaceBaseClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/InterfaceBaseClause.php', + 'Microsoft\\PhpParser\\Node\\InterfaceMembers' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/InterfaceMembers.php', + 'Microsoft\\PhpParser\\Node\\MatchArm' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/MatchArm.php', + 'Microsoft\\PhpParser\\Node\\MethodDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/MethodDeclaration.php', + 'Microsoft\\PhpParser\\Node\\MissingDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/MissingDeclaration.php', + 'Microsoft\\PhpParser\\Node\\MissingMemberDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/MissingMemberDeclaration.php', + 'Microsoft\\PhpParser\\Node\\NamespaceAliasingClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/NamespaceAliasingClause.php', + 'Microsoft\\PhpParser\\Node\\NamespaceUseClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/NamespaceUseClause.php', + 'Microsoft\\PhpParser\\Node\\NamespaceUseGroupClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/NamespaceUseGroupClause.php', + 'Microsoft\\PhpParser\\Node\\NumericLiteral' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/NumericLiteral.php', + 'Microsoft\\PhpParser\\Node\\Parameter' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Parameter.php', + 'Microsoft\\PhpParser\\Node\\PropertyDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/PropertyDeclaration.php', + 'Microsoft\\PhpParser\\Node\\QualifiedName' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/QualifiedName.php', + 'Microsoft\\PhpParser\\Node\\RelativeSpecifier' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/RelativeSpecifier.php', + 'Microsoft\\PhpParser\\Node\\ReservedWord' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/ReservedWord.php', + 'Microsoft\\PhpParser\\Node\\SourceFileNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/SourceFileNode.php', + 'Microsoft\\PhpParser\\Node\\StatementNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/StatementNode.php', + 'Microsoft\\PhpParser\\Node\\Statement\\BreakOrContinueStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/BreakOrContinueStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ClassDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/ClassDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\CompoundStatementNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/CompoundStatementNode.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ConstDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/ConstDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\DeclareStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/DeclareStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\DoStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/DoStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\EmptyStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/EmptyStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ExpressionStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/ExpressionStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ForStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/ForStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ForeachStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/ForeachStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\FunctionDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/FunctionDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\FunctionStaticDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/FunctionStaticDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\GlobalDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/GlobalDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\GotoStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/GotoStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\IfStatementNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/IfStatementNode.php', + 'Microsoft\\PhpParser\\Node\\Statement\\InlineHtml' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/InlineHtml.php', + 'Microsoft\\PhpParser\\Node\\Statement\\InterfaceDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/InterfaceDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\NamedLabelStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/NamedLabelStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\NamespaceDefinition' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/NamespaceDefinition.php', + 'Microsoft\\PhpParser\\Node\\Statement\\NamespaceUseDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/NamespaceUseDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ReturnStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/ReturnStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\SwitchStatementNode' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/SwitchStatementNode.php', + 'Microsoft\\PhpParser\\Node\\Statement\\ThrowStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/ThrowStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\TraitDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/TraitDeclaration.php', + 'Microsoft\\PhpParser\\Node\\Statement\\TryStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/TryStatement.php', + 'Microsoft\\PhpParser\\Node\\Statement\\WhileStatement' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/Statement/WhileStatement.php', + 'Microsoft\\PhpParser\\Node\\StaticVariableDeclaration' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/StaticVariableDeclaration.php', + 'Microsoft\\PhpParser\\Node\\StringLiteral' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/StringLiteral.php', + 'Microsoft\\PhpParser\\Node\\TraitMembers' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/TraitMembers.php', + 'Microsoft\\PhpParser\\Node\\TraitSelectOrAliasClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/TraitSelectOrAliasClause.php', + 'Microsoft\\PhpParser\\Node\\TraitUseClause' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/TraitUseClause.php', + 'Microsoft\\PhpParser\\Node\\UseVariableName' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Node/UseVariableName.php', + 'Microsoft\\PhpParser\\ParseContext' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/ParseContext.php', + 'Microsoft\\PhpParser\\Parser' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Parser.php', + 'Microsoft\\PhpParser\\PhpTokenizer' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/PhpTokenizer.php', + 'Microsoft\\PhpParser\\PositionUtilities' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/PositionUtilities.php', + 'Microsoft\\PhpParser\\Range' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Range.php', + 'Microsoft\\PhpParser\\ResolvedName' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/ResolvedName.php', + 'Microsoft\\PhpParser\\SkippedToken' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/SkippedToken.php', + 'Microsoft\\PhpParser\\TextEdit' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/TextEdit.php', + 'Microsoft\\PhpParser\\Token' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/Token.php', + 'Microsoft\\PhpParser\\TokenKind' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/TokenKind.php', + 'Microsoft\\PhpParser\\TokenStreamProviderFactory' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/TokenStreamProviderFactory.php', + 'Microsoft\\PhpParser\\TokenStreamProviderInterface' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/TokenStreamProviderInterface.php', + 'Microsoft\\PhpParser\\TokenStringMaps' => __DIR__ . '/..' . '/microsoft/tolerant-php-parser/src/TokenStringMaps.php', + 'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', + 'Phan\\AST\\ASTHasher' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/ASTHasher.php', + 'Phan\\AST\\ASTReverter' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/ASTReverter.php', + 'Phan\\AST\\ASTSimplifier' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/ASTSimplifier.php', + 'Phan\\AST\\AnalysisVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/AnalysisVisitor.php', + 'Phan\\AST\\ArrowFunc' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/ArrowFunc.php', + 'Phan\\AST\\ContextNode' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/ContextNode.php', + 'Phan\\AST\\FallbackUnionTypeVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/FallbackUnionTypeVisitor.php', + 'Phan\\AST\\InferPureSnippetVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/InferPureSnippetVisitor.php', + 'Phan\\AST\\InferPureVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/InferPureVisitor.php', + 'Phan\\AST\\InferValue' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/InferValue.php', + 'Phan\\AST\\Parser' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/Parser.php', + 'Phan\\AST\\PhanAnnotationAdder' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/PhanAnnotationAdder.php', + 'Phan\\AST\\ScopeImpactCheckingVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/ScopeImpactCheckingVisitor.php', + 'Phan\\AST\\TolerantASTConverter\\CachingTolerantASTConverter' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/CachingTolerantASTConverter.php', + 'Phan\\AST\\TolerantASTConverter\\InvalidNodeException' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/InvalidNodeException.php', + 'Phan\\AST\\TolerantASTConverter\\NodeDumper' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/NodeDumper.php', + 'Phan\\AST\\TolerantASTConverter\\NodeUtils' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/NodeUtils.php', + 'Phan\\AST\\TolerantASTConverter\\ParseException' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/ParseException.php', + 'Phan\\AST\\TolerantASTConverter\\ParseResult' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/ParseResult.php', + 'Phan\\AST\\TolerantASTConverter\\PhpParserNodeEntry' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/PhpParserNodeEntry.php', + 'Phan\\AST\\TolerantASTConverter\\Shim' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/Shim.php', + 'Phan\\AST\\TolerantASTConverter\\ShimFunctions' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/ShimFunctions.php', + 'Phan\\AST\\TolerantASTConverter\\StringUtil' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/StringUtil.php', + 'Phan\\AST\\TolerantASTConverter\\TolerantASTConverter' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverter.php', + 'Phan\\AST\\TolerantASTConverter\\TolerantASTConverterPreservingOriginal' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterPreservingOriginal.php', + 'Phan\\AST\\TolerantASTConverter\\TolerantASTConverterWithNodeMapping' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterWithNodeMapping.php', + 'Phan\\AST\\UnionTypeVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/UnionTypeVisitor.php', + 'Phan\\AST\\Visitor\\Element' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/Visitor/Element.php', + 'Phan\\AST\\Visitor\\FlagVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/Visitor/FlagVisitor.php', + 'Phan\\AST\\Visitor\\FlagVisitorImplementation' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/Visitor/FlagVisitorImplementation.php', + 'Phan\\AST\\Visitor\\KindVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/Visitor/KindVisitor.php', + 'Phan\\AST\\Visitor\\KindVisitorImplementation' => __DIR__ . '/..' . '/phan/phan/src/Phan/AST/Visitor/KindVisitorImplementation.php', + 'Phan\\Analysis' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis.php', + 'Phan\\Analysis\\AbstractMethodAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/AbstractMethodAnalyzer.php', + 'Phan\\Analysis\\Analyzable' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/Analyzable.php', + 'Phan\\Analysis\\ArgumentType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ArgumentType.php', + 'Phan\\Analysis\\AssignOperatorAnalysisVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/AssignOperatorAnalysisVisitor.php', + 'Phan\\Analysis\\AssignOperatorFlagVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/AssignOperatorFlagVisitor.php', + 'Phan\\Analysis\\AssignmentVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/AssignmentVisitor.php', + 'Phan\\Analysis\\BinaryOperatorFlagVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/BinaryOperatorFlagVisitor.php', + 'Phan\\Analysis\\BlockExitStatusChecker' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/BlockExitStatusChecker.php', + 'Phan\\Analysis\\ClassConstantTypesAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ClassConstantTypesAnalyzer.php', + 'Phan\\Analysis\\ClassInheritanceAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ClassInheritanceAnalyzer.php', + 'Phan\\Analysis\\CompositionAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/CompositionAnalyzer.php', + 'Phan\\Analysis\\ConditionVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitor.php', + 'Phan\\Analysis\\ConditionVisitorInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitorInterface.php', + 'Phan\\Analysis\\ConditionVisitorUtil' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitorUtil.php', + 'Phan\\Analysis\\ConditionVisitor\\BinaryCondition' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitor/BinaryCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\ComparisonCondition' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitor/ComparisonCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\EqualsCondition' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitor/EqualsCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\HasTypeCondition' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitor/HasTypeCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\IdenticalCondition' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitor/IdenticalCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\NotEqualsCondition' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitor/NotEqualsCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\NotHasTypeCondition' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitor/NotHasTypeCondition.php', + 'Phan\\Analysis\\ConditionVisitor\\NotIdenticalCondition' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ConditionVisitor/NotIdenticalCondition.php', + 'Phan\\Analysis\\ContextMergeVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ContextMergeVisitor.php', + 'Phan\\Analysis\\DuplicateClassAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/DuplicateClassAnalyzer.php', + 'Phan\\Analysis\\DuplicateFunctionAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/DuplicateFunctionAnalyzer.php', + 'Phan\\Analysis\\FallbackMethodTypesVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/FallbackMethodTypesVisitor.php', + 'Phan\\Analysis\\GotoAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/GotoAnalyzer.php', + 'Phan\\Analysis\\LoopConditionVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/LoopConditionVisitor.php', + 'Phan\\Analysis\\NegatedConditionVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/NegatedConditionVisitor.php', + 'Phan\\Analysis\\ParameterTypesAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ParameterTypesAnalyzer.php', + 'Phan\\Analysis\\ParentConstructorCalledAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ParentConstructorCalledAnalyzer.php', + 'Phan\\Analysis\\PostOrderAnalysisVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/PostOrderAnalysisVisitor.php', + 'Phan\\Analysis\\PreOrderAnalysisVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/PreOrderAnalysisVisitor.php', + 'Phan\\Analysis\\PropertyTypesAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/PropertyTypesAnalyzer.php', + 'Phan\\Analysis\\ReachabilityChecker' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ReachabilityChecker.php', + 'Phan\\Analysis\\RedundantCondition' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/RedundantCondition.php', + 'Phan\\Analysis\\ReferenceCountsAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ReferenceCountsAnalyzer.php', + 'Phan\\Analysis\\RegexAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/RegexAnalyzer.php', + 'Phan\\Analysis\\ScopeVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ScopeVisitor.php', + 'Phan\\Analysis\\ThrowsTypesAnalyzer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Analysis/ThrowsTypesAnalyzer.php', + 'Phan\\BlockAnalysisVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/BlockAnalysisVisitor.php', + 'Phan\\CLI' => __DIR__ . '/..' . '/phan/phan/src/Phan/CLI.php', + 'Phan\\CLIBuilder' => __DIR__ . '/..' . '/phan/phan/src/Phan/CLIBuilder.php', + 'Phan\\CodeBase' => __DIR__ . '/..' . '/phan/phan/src/Phan/CodeBase.php', + 'Phan\\CodeBase\\ClassMap' => __DIR__ . '/..' . '/phan/phan/src/Phan/CodeBase/ClassMap.php', + 'Phan\\CodeBase\\UndoTracker' => __DIR__ . '/..' . '/phan/phan/src/Phan/CodeBase/UndoTracker.php', + 'Phan\\Config' => __DIR__ . '/..' . '/phan/phan/src/Phan/Config.php', + 'Phan\\Config\\InitializedSettings' => __DIR__ . '/..' . '/phan/phan/src/Phan/Config/InitializedSettings.php', + 'Phan\\Config\\Initializer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Config/Initializer.php', + 'Phan\\Daemon' => __DIR__ . '/..' . '/phan/phan/src/Phan/Daemon.php', + 'Phan\\Daemon\\ExitException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Daemon/ExitException.php', + 'Phan\\Daemon\\ParseRequest' => __DIR__ . '/..' . '/phan/phan/src/Phan/Daemon/ParseRequest.php', + 'Phan\\Daemon\\Request' => __DIR__ . '/..' . '/phan/phan/src/Phan/Daemon/Request.php', + 'Phan\\Daemon\\Transport\\CapturerResponder' => __DIR__ . '/..' . '/phan/phan/src/Phan/Daemon/Transport/CapturerResponder.php', + 'Phan\\Daemon\\Transport\\Responder' => __DIR__ . '/..' . '/phan/phan/src/Phan/Daemon/Transport/Responder.php', + 'Phan\\Daemon\\Transport\\StreamResponder' => __DIR__ . '/..' . '/phan/phan/src/Phan/Daemon/Transport/StreamResponder.php', + 'Phan\\Debug' => __DIR__ . '/..' . '/phan/phan/src/Phan/Debug.php', + 'Phan\\Debug\\DebugUnionType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Debug/DebugUnionType.php', + 'Phan\\Debug\\Frame' => __DIR__ . '/..' . '/phan/phan/src/Phan/Debug/Frame.php', + 'Phan\\Debug\\SignalHandler' => __DIR__ . '/..' . '/phan/phan/src/Phan/Debug/SignalHandler.php', + 'Phan\\Exception\\CodeBaseException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/CodeBaseException.php', + 'Phan\\Exception\\EmptyFQSENException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/EmptyFQSENException.php', + 'Phan\\Exception\\FQSENException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/FQSENException.php', + 'Phan\\Exception\\InvalidFQSENException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/InvalidFQSENException.php', + 'Phan\\Exception\\IssueException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/IssueException.php', + 'Phan\\Exception\\NodeException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/NodeException.php', + 'Phan\\Exception\\RecursionDepthException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/RecursionDepthException.php', + 'Phan\\Exception\\UnanalyzableException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/UnanalyzableException.php', + 'Phan\\Exception\\UnanalyzableMagicPropertyException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/UnanalyzableMagicPropertyException.php', + 'Phan\\Exception\\UsageException' => __DIR__ . '/..' . '/phan/phan/src/Phan/Exception/UsageException.php', + 'Phan\\ForkPool' => __DIR__ . '/..' . '/phan/phan/src/Phan/ForkPool.php', + 'Phan\\ForkPool\\Progress' => __DIR__ . '/..' . '/phan/phan/src/Phan/ForkPool/Progress.php', + 'Phan\\ForkPool\\Reader' => __DIR__ . '/..' . '/phan/phan/src/Phan/ForkPool/Reader.php', + 'Phan\\ForkPool\\Writer' => __DIR__ . '/..' . '/phan/phan/src/Phan/ForkPool/Writer.php', + 'Phan\\Issue' => __DIR__ . '/..' . '/phan/phan/src/Phan/Issue.php', + 'Phan\\IssueFixSuggester' => __DIR__ . '/..' . '/phan/phan/src/Phan/IssueFixSuggester.php', + 'Phan\\IssueInstance' => __DIR__ . '/..' . '/phan/phan/src/Phan/IssueInstance.php', + 'Phan\\LanguageServer\\CachedHoverResponse' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/CachedHoverResponse.php', + 'Phan\\LanguageServer\\ClientHandler' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/ClientHandler.php', + 'Phan\\LanguageServer\\Client\\TextDocument' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Client/TextDocument.php', + 'Phan\\LanguageServer\\CompletionRequest' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/CompletionRequest.php', + 'Phan\\LanguageServer\\CompletionResolver' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/CompletionResolver.php', + 'Phan\\LanguageServer\\DefinitionResolver' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/DefinitionResolver.php', + 'Phan\\LanguageServer\\FileMapping' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/FileMapping.php', + 'Phan\\LanguageServer\\GoToDefinitionRequest' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/GoToDefinitionRequest.php', + 'Phan\\LanguageServer\\IdGenerator' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/IdGenerator.php', + 'Phan\\LanguageServer\\LanguageClient' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/LanguageClient.php', + 'Phan\\LanguageServer\\LanguageServer' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/LanguageServer.php', + 'Phan\\LanguageServer\\Logger' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Logger.php', + 'Phan\\LanguageServer\\NodeInfoRequest' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/NodeInfoRequest.php', + 'Phan\\LanguageServer\\ProtocolReader' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/ProtocolReader.php', + 'Phan\\LanguageServer\\ProtocolStreamReader' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/ProtocolStreamReader.php', + 'Phan\\LanguageServer\\ProtocolStreamWriter' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/ProtocolStreamWriter.php', + 'Phan\\LanguageServer\\ProtocolWriter' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/ProtocolWriter.php', + 'Phan\\LanguageServer\\Protocol\\ClientCapabilities' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/ClientCapabilities.php', + 'Phan\\LanguageServer\\Protocol\\CompletionContext' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionContext.php', + 'Phan\\LanguageServer\\Protocol\\CompletionItem' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionItem.php', + 'Phan\\LanguageServer\\Protocol\\CompletionItemKind' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionItemKind.php', + 'Phan\\LanguageServer\\Protocol\\CompletionList' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionList.php', + 'Phan\\LanguageServer\\Protocol\\CompletionOptions' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionOptions.php', + 'Phan\\LanguageServer\\Protocol\\CompletionTriggerKind' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/CompletionTriggerKind.php', + 'Phan\\LanguageServer\\Protocol\\Diagnostic' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/Diagnostic.php', + 'Phan\\LanguageServer\\Protocol\\DiagnosticSeverity' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/DiagnosticSeverity.php', + 'Phan\\LanguageServer\\Protocol\\FileChangeType' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/FileChangeType.php', + 'Phan\\LanguageServer\\Protocol\\FileEvent' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/FileEvent.php', + 'Phan\\LanguageServer\\Protocol\\Hover' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/Hover.php', + 'Phan\\LanguageServer\\Protocol\\InitializeResult' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/InitializeResult.php', + 'Phan\\LanguageServer\\Protocol\\Location' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/Location.php', + 'Phan\\LanguageServer\\Protocol\\MarkupContent' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/MarkupContent.php', + 'Phan\\LanguageServer\\Protocol\\Message' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/Message.php', + 'Phan\\LanguageServer\\Protocol\\Position' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/Position.php', + 'Phan\\LanguageServer\\Protocol\\Range' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/Range.php', + 'Phan\\LanguageServer\\Protocol\\SaveOptions' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/SaveOptions.php', + 'Phan\\LanguageServer\\Protocol\\ServerCapabilities' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/ServerCapabilities.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentContentChangeEvent' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentContentChangeEvent.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentIdentifier' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentIdentifier.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentItem' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentItem.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentSyncKind' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentSyncKind.php', + 'Phan\\LanguageServer\\Protocol\\TextDocumentSyncOptions' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/TextDocumentSyncOptions.php', + 'Phan\\LanguageServer\\Protocol\\VersionedTextDocumentIdentifier' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Protocol/VersionedTextDocumentIdentifier.php', + 'Phan\\LanguageServer\\Server\\TextDocument' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Server/TextDocument.php', + 'Phan\\LanguageServer\\Server\\Workspace' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Server/Workspace.php', + 'Phan\\LanguageServer\\Utils' => __DIR__ . '/..' . '/phan/phan/src/Phan/LanguageServer/Utils.php', + 'Phan\\Language\\AnnotatedUnionType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/AnnotatedUnionType.php', + 'Phan\\Language\\Context' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Context.php', + 'Phan\\Language\\ElementContext' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/ElementContext.php', + 'Phan\\Language\\Element\\AddressableElement' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/AddressableElement.php', + 'Phan\\Language\\Element\\AddressableElementInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/AddressableElementInterface.php', + 'Phan\\Language\\Element\\ClassAliasRecord' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/ClassAliasRecord.php', + 'Phan\\Language\\Element\\ClassConstant' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/ClassConstant.php', + 'Phan\\Language\\Element\\ClassElement' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/ClassElement.php', + 'Phan\\Language\\Element\\Clazz' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Clazz.php', + 'Phan\\Language\\Element\\ClosedScopeElement' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/ClosedScopeElement.php', + 'Phan\\Language\\Element\\Comment' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Comment.php', + 'Phan\\Language\\Element\\Comment\\Assertion' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Comment/Assertion.php', + 'Phan\\Language\\Element\\Comment\\Builder' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Comment/Builder.php', + 'Phan\\Language\\Element\\Comment\\Method' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Comment/Method.php', + 'Phan\\Language\\Element\\Comment\\NullComment' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Comment/NullComment.php', + 'Phan\\Language\\Element\\Comment\\Parameter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Comment/Parameter.php', + 'Phan\\Language\\Element\\Comment\\Property' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Comment/Property.php', + 'Phan\\Language\\Element\\Comment\\ReturnComment' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Comment/ReturnComment.php', + 'Phan\\Language\\Element\\ConstantInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/ConstantInterface.php', + 'Phan\\Language\\Element\\ConstantTrait' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/ConstantTrait.php', + 'Phan\\Language\\Element\\ElementFutureUnionType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/ElementFutureUnionType.php', + 'Phan\\Language\\Element\\Flags' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Flags.php', + 'Phan\\Language\\Element\\Func' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Func.php', + 'Phan\\Language\\Element\\FunctionFactory' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/FunctionFactory.php', + 'Phan\\Language\\Element\\FunctionInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/FunctionInterface.php', + 'Phan\\Language\\Element\\FunctionTrait' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/FunctionTrait.php', + 'Phan\\Language\\Element\\GlobalConstant' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/GlobalConstant.php', + 'Phan\\Language\\Element\\MarkupDescription' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/MarkupDescription.php', + 'Phan\\Language\\Element\\Method' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Method.php', + 'Phan\\Language\\Element\\Parameter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Parameter.php', + 'Phan\\Language\\Element\\PassByReferenceVariable' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/PassByReferenceVariable.php', + 'Phan\\Language\\Element\\Property' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Property.php', + 'Phan\\Language\\Element\\TraitAdaptations' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/TraitAdaptations.php', + 'Phan\\Language\\Element\\TraitAliasSource' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/TraitAliasSource.php', + 'Phan\\Language\\Element\\TypedElement' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/TypedElement.php', + 'Phan\\Language\\Element\\TypedElementInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/TypedElementInterface.php', + 'Phan\\Language\\Element\\UnaddressableTypedElement' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/UnaddressableTypedElement.php', + 'Phan\\Language\\Element\\Variable' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/Variable.php', + 'Phan\\Language\\Element\\VariadicParameter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Element/VariadicParameter.php', + 'Phan\\Language\\EmptyUnionType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/EmptyUnionType.php', + 'Phan\\Language\\FQSEN' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN.php', + 'Phan\\Language\\FQSEN\\AbstractFQSEN' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/AbstractFQSEN.php', + 'Phan\\Language\\FQSEN\\Alternatives' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/Alternatives.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedClassConstantName' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassConstantName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedClassElement' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassElement.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedClassName' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedConstantName' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedConstantName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedFunctionLikeName' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedFunctionLikeName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedFunctionName' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedFunctionName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedGlobalConstantName' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedGlobalConstantName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedGlobalStructuralElement' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedGlobalStructuralElement.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedMethodName' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedMethodName.php', + 'Phan\\Language\\FQSEN\\FullyQualifiedPropertyName' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedPropertyName.php', + 'Phan\\Language\\FileRef' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FileRef.php', + 'Phan\\Language\\FutureUnionType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/FutureUnionType.php', + 'Phan\\Language\\NamespaceMapEntry' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/NamespaceMapEntry.php', + 'Phan\\Language\\Scope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Scope.php', + 'Phan\\Language\\Scope\\BranchScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Scope/BranchScope.php', + 'Phan\\Language\\Scope\\ClassScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Scope/ClassScope.php', + 'Phan\\Language\\Scope\\ClosedScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Scope/ClosedScope.php', + 'Phan\\Language\\Scope\\ClosureScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Scope/ClosureScope.php', + 'Phan\\Language\\Scope\\FunctionLikeScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Scope/FunctionLikeScope.php', + 'Phan\\Language\\Scope\\GlobalScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Scope/GlobalScope.php', + 'Phan\\Language\\Scope\\PropertyScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Scope/PropertyScope.php', + 'Phan\\Language\\Scope\\TemplateScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Scope/TemplateScope.php', + 'Phan\\Language\\Type' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type.php', + 'Phan\\Language\\Type\\ArrayShapeType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ArrayShapeType.php', + 'Phan\\Language\\Type\\ArrayType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ArrayType.php', + 'Phan\\Language\\Type\\AssociativeArrayType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/AssociativeArrayType.php', + 'Phan\\Language\\Type\\BoolType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/BoolType.php', + 'Phan\\Language\\Type\\CallableArrayType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/CallableArrayType.php', + 'Phan\\Language\\Type\\CallableDeclarationType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/CallableDeclarationType.php', + 'Phan\\Language\\Type\\CallableInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/CallableInterface.php', + 'Phan\\Language\\Type\\CallableObjectType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/CallableObjectType.php', + 'Phan\\Language\\Type\\CallableStringType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/CallableStringType.php', + 'Phan\\Language\\Type\\CallableType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/CallableType.php', + 'Phan\\Language\\Type\\ClassStringType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ClassStringType.php', + 'Phan\\Language\\Type\\ClosureDeclarationParameter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ClosureDeclarationParameter.php', + 'Phan\\Language\\Type\\ClosureDeclarationType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ClosureDeclarationType.php', + 'Phan\\Language\\Type\\ClosureType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ClosureType.php', + 'Phan\\Language\\Type\\FalseType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/FalseType.php', + 'Phan\\Language\\Type\\FloatType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/FloatType.php', + 'Phan\\Language\\Type\\FunctionLikeDeclarationType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/FunctionLikeDeclarationType.php', + 'Phan\\Language\\Type\\GenericArrayInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/GenericArrayInterface.php', + 'Phan\\Language\\Type\\GenericArrayTemplateKeyType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/GenericArrayTemplateKeyType.php', + 'Phan\\Language\\Type\\GenericArrayType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/GenericArrayType.php', + 'Phan\\Language\\Type\\GenericIterableType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/GenericIterableType.php', + 'Phan\\Language\\Type\\GenericMultiArrayType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/GenericMultiArrayType.php', + 'Phan\\Language\\Type\\IntType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/IntType.php', + 'Phan\\Language\\Type\\IterableType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/IterableType.php', + 'Phan\\Language\\Type\\ListType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ListType.php', + 'Phan\\Language\\Type\\LiteralFloatType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/LiteralFloatType.php', + 'Phan\\Language\\Type\\LiteralIntType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/LiteralIntType.php', + 'Phan\\Language\\Type\\LiteralStringType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/LiteralStringType.php', + 'Phan\\Language\\Type\\LiteralTypeInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/LiteralTypeInterface.php', + 'Phan\\Language\\Type\\MixedType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/MixedType.php', + 'Phan\\Language\\Type\\MultiType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/MultiType.php', + 'Phan\\Language\\Type\\NativeType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NativeType.php', + 'Phan\\Language\\Type\\NonEmptyArrayInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NonEmptyArrayInterface.php', + 'Phan\\Language\\Type\\NonEmptyAssociativeArrayType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NonEmptyAssociativeArrayType.php', + 'Phan\\Language\\Type\\NonEmptyGenericArrayType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NonEmptyGenericArrayType.php', + 'Phan\\Language\\Type\\NonEmptyListType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NonEmptyListType.php', + 'Phan\\Language\\Type\\NonEmptyMixedType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NonEmptyMixedType.php', + 'Phan\\Language\\Type\\NonEmptyStringType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NonEmptyStringType.php', + 'Phan\\Language\\Type\\NonNullMixedType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NonNullMixedType.php', + 'Phan\\Language\\Type\\NonZeroIntType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NonZeroIntType.php', + 'Phan\\Language\\Type\\NullType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/NullType.php', + 'Phan\\Language\\Type\\ObjectType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ObjectType.php', + 'Phan\\Language\\Type\\ResourceType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ResourceType.php', + 'Phan\\Language\\Type\\ScalarRawType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ScalarRawType.php', + 'Phan\\Language\\Type\\ScalarType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/ScalarType.php', + 'Phan\\Language\\Type\\SelfType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/SelfType.php', + 'Phan\\Language\\Type\\StaticOrSelfType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/StaticOrSelfType.php', + 'Phan\\Language\\Type\\StaticType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/StaticType.php', + 'Phan\\Language\\Type\\StringType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/StringType.php', + 'Phan\\Language\\Type\\TemplateType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/TemplateType.php', + 'Phan\\Language\\Type\\TrueType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/TrueType.php', + 'Phan\\Language\\Type\\VoidType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/Type/VoidType.php', + 'Phan\\Language\\UnionType' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/UnionType.php', + 'Phan\\Language\\UnionTypeBuilder' => __DIR__ . '/..' . '/phan/phan/src/Phan/Language/UnionTypeBuilder.php', + 'Phan\\Library\\Cache' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Cache.php', + 'Phan\\Library\\ConversionSpec' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/ConversionSpec.php', + 'Phan\\Library\\DiskCache' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/DiskCache.php', + 'Phan\\Library\\FileCache' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/FileCache.php', + 'Phan\\Library\\FileCacheEntry' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/FileCacheEntry.php', + 'Phan\\Library\\Hasher' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Hasher.php', + 'Phan\\Library\\Hasher\\Consistent' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Hasher/Consistent.php', + 'Phan\\Library\\Hasher\\Sequential' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Hasher/Sequential.php', + 'Phan\\Library\\Map' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Map.php', + 'Phan\\Library\\None' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/None.php', + 'Phan\\Library\\Option' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Option.php', + 'Phan\\Library\\Paths' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Paths.php', + 'Phan\\Library\\RAII' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/RAII.php', + 'Phan\\Library\\RegexKeyExtractor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/RegexKeyExtractor.php', + 'Phan\\Library\\Restarter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Restarter.php', + 'Phan\\Library\\Set' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Set.php', + 'Phan\\Library\\Some' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Some.php', + 'Phan\\Library\\StderrLogger' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/StderrLogger.php', + 'Phan\\Library\\StringSuggester' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/StringSuggester.php', + 'Phan\\Library\\StringUtil' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/StringUtil.php', + 'Phan\\Library\\Tuple' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Tuple.php', + 'Phan\\Library\\Tuple1' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Tuple1.php', + 'Phan\\Library\\Tuple2' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Tuple2.php', + 'Phan\\Library\\Tuple3' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Tuple3.php', + 'Phan\\Library\\Tuple4' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Tuple4.php', + 'Phan\\Library\\Tuple5' => __DIR__ . '/..' . '/phan/phan/src/Phan/Library/Tuple5.php', + 'Phan\\Memoize' => __DIR__ . '/..' . '/phan/phan/src/Phan/Memoize.php', + 'Phan\\Ordering' => __DIR__ . '/..' . '/phan/phan/src/Phan/Ordering.php', + 'Phan\\Output\\BufferedPrinterInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/BufferedPrinterInterface.php', + 'Phan\\Output\\Collector\\BufferingCollector' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Collector/BufferingCollector.php', + 'Phan\\Output\\Collector\\ParallelChildCollector' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Collector/ParallelChildCollector.php', + 'Phan\\Output\\Collector\\ParallelParentCollector' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Collector/ParallelParentCollector.php', + 'Phan\\Output\\ColorScheme\\Code' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/ColorScheme/Code.php', + 'Phan\\Output\\ColorScheme\\EclipseDark' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/ColorScheme/EclipseDark.php', + 'Phan\\Output\\ColorScheme\\Light' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/ColorScheme/Light.php', + 'Phan\\Output\\ColorScheme\\LightHighContrast' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/ColorScheme/LightHighContrast.php', + 'Phan\\Output\\ColorScheme\\Vim' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/ColorScheme/Vim.php', + 'Phan\\Output\\Colorizing' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Colorizing.php', + 'Phan\\Output\\Filter\\AnyFilter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Filter/AnyFilter.php', + 'Phan\\Output\\Filter\\CategoryIssueFilter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Filter/CategoryIssueFilter.php', + 'Phan\\Output\\Filter\\ChainedIssueFilter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Filter/ChainedIssueFilter.php', + 'Phan\\Output\\Filter\\FileIssueFilter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Filter/FileIssueFilter.php', + 'Phan\\Output\\Filter\\MinimumSeverityFilter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Filter/MinimumSeverityFilter.php', + 'Phan\\Output\\HTML' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/HTML.php', + 'Phan\\Output\\IgnoredFilesFilterInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/IgnoredFilesFilterInterface.php', + 'Phan\\Output\\IssueCollectorInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/IssueCollectorInterface.php', + 'Phan\\Output\\IssueFilterInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/IssueFilterInterface.php', + 'Phan\\Output\\IssuePrinterInterface' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/IssuePrinterInterface.php', + 'Phan\\Output\\PrinterFactory' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/PrinterFactory.php', + 'Phan\\Output\\Printer\\CSVPrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/CSVPrinter.php', + 'Phan\\Output\\Printer\\CapturingJSONPrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/CapturingJSONPrinter.php', + 'Phan\\Output\\Printer\\CheckstylePrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/CheckstylePrinter.php', + 'Phan\\Output\\Printer\\CodeClimatePrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/CodeClimatePrinter.php', + 'Phan\\Output\\Printer\\FilteringPrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/FilteringPrinter.php', + 'Phan\\Output\\Printer\\HTMLPrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/HTMLPrinter.php', + 'Phan\\Output\\Printer\\JSONPrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/JSONPrinter.php', + 'Phan\\Output\\Printer\\PlainTextPrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/PlainTextPrinter.php', + 'Phan\\Output\\Printer\\PylintPrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/PylintPrinter.php', + 'Phan\\Output\\Printer\\VerbosePlainTextPrinter' => __DIR__ . '/..' . '/phan/phan/src/Phan/Output/Printer/VerbosePlainTextPrinter.php', + 'Phan\\Parse\\ParseVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Parse/ParseVisitor.php', + 'Phan\\Phan' => __DIR__ . '/..' . '/phan/phan/src/Phan/Phan.php', + 'Phan\\PluginV3' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3.php', + 'Phan\\PluginV3\\AfterAnalyzeFileCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/AfterAnalyzeFileCapability.php', + 'Phan\\PluginV3\\AnalyzeClassCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/AnalyzeClassCapability.php', + 'Phan\\PluginV3\\AnalyzeFunctionCallCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/AnalyzeFunctionCallCapability.php', + 'Phan\\PluginV3\\AnalyzeFunctionCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/AnalyzeFunctionCapability.php', + 'Phan\\PluginV3\\AnalyzeLiteralStatementCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/AnalyzeLiteralStatementCapability.php', + 'Phan\\PluginV3\\AnalyzeMethodCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/AnalyzeMethodCapability.php', + 'Phan\\PluginV3\\AnalyzePropertyCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/AnalyzePropertyCapability.php', + 'Phan\\PluginV3\\AutomaticFixCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/AutomaticFixCapability.php', + 'Phan\\PluginV3\\BeforeAnalyzeCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/BeforeAnalyzeCapability.php', + 'Phan\\PluginV3\\BeforeAnalyzeFileCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/BeforeAnalyzeFileCapability.php', + 'Phan\\PluginV3\\BeforeAnalyzePhaseCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/BeforeAnalyzePhaseCapability.php', + 'Phan\\PluginV3\\BeforeLoopBodyAnalysisCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/BeforeLoopBodyAnalysisCapability.php', + 'Phan\\PluginV3\\BeforeLoopBodyAnalysisVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/BeforeLoopBodyAnalysisVisitor.php', + 'Phan\\PluginV3\\FinalizeProcessCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/FinalizeProcessCapability.php', + 'Phan\\PluginV3\\HandleLazyLoadInternalFunctionCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/HandleLazyLoadInternalFunctionCapability.php', + 'Phan\\PluginV3\\IssueEmitter' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/IssueEmitter.php', + 'Phan\\PluginV3\\MergeVariableInfoCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/MergeVariableInfoCapability.php', + 'Phan\\PluginV3\\PluginAwareBaseAnalysisVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/PluginAwareBaseAnalysisVisitor.php', + 'Phan\\PluginV3\\PluginAwarePostAnalysisVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/PluginAwarePostAnalysisVisitor.php', + 'Phan\\PluginV3\\PluginAwarePreAnalysisVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/PluginAwarePreAnalysisVisitor.php', + 'Phan\\PluginV3\\PostAnalyzeNodeCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/PostAnalyzeNodeCapability.php', + 'Phan\\PluginV3\\PreAnalyzeNodeCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/PreAnalyzeNodeCapability.php', + 'Phan\\PluginV3\\ReturnTypeOverrideCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/ReturnTypeOverrideCapability.php', + 'Phan\\PluginV3\\StopParamAnalysisException' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/StopParamAnalysisException.php', + 'Phan\\PluginV3\\SubscribeEmitIssueCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/SubscribeEmitIssueCapability.php', + 'Phan\\PluginV3\\SuppressionCapability' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/SuppressionCapability.php', + 'Phan\\PluginV3\\UnloadablePluginException' => __DIR__ . '/..' . '/phan/phan/src/Phan/PluginV3/UnloadablePluginException.php', + 'Phan\\Plugin\\ClosuresForKind' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/ClosuresForKind.php', + 'Phan\\Plugin\\ConfigPluginSet' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/ConfigPluginSet.php', + 'Phan\\Plugin\\Internal\\ArrayReturnTypeOverridePlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/ArrayReturnTypeOverridePlugin.php', + 'Phan\\Plugin\\Internal\\BaselineLoadingPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/BaselineLoadingPlugin.php', + 'Phan\\Plugin\\Internal\\BaselineSavingPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/BaselineSavingPlugin.php', + 'Phan\\Plugin\\Internal\\BuiltinSuppressionPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/BuiltinSuppressionPlugin.php', + 'Phan\\Plugin\\Internal\\CallableParamPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/CallableParamPlugin.php', + 'Phan\\Plugin\\Internal\\ClosureReturnTypeOverridePlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/ClosureReturnTypeOverridePlugin.php', + 'Phan\\Plugin\\Internal\\CompactPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/CompactPlugin.php', + 'Phan\\Plugin\\Internal\\CtagsPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/CtagsPlugin.php', + 'Phan\\Plugin\\Internal\\CtagsPlugin\\CtagsEntry' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/CtagsPlugin/CtagsEntry.php', + 'Phan\\Plugin\\Internal\\CtagsPlugin\\CtagsEntrySet' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/CtagsPlugin/CtagsEntrySet.php', + 'Phan\\Plugin\\Internal\\DependencyGraphPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/DependencyGraphPlugin.php', + 'Phan\\Plugin\\Internal\\DependentReturnTypeOverridePlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/DependentReturnTypeOverridePlugin.php', + 'Phan\\Plugin\\Internal\\DumpPHPDocPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/DumpPHPDocPlugin.php', + 'Phan\\Plugin\\Internal\\ExtendedDependentReturnTypeOverridePlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/ExtendedDependentReturnTypeOverridePlugin.php', + 'Phan\\Plugin\\Internal\\IssueFixingPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/IssueFixingPlugin.php', + 'Phan\\Plugin\\Internal\\IssueFixingPlugin\\FileEdit' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/IssueFixingPlugin/FileEdit.php', + 'Phan\\Plugin\\Internal\\IssueFixingPlugin\\FileEditSet' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/IssueFixingPlugin/FileEditSet.php', + 'Phan\\Plugin\\Internal\\IssueFixingPlugin\\IssueFixer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/IssueFixingPlugin/IssueFixer.php', + 'Phan\\Plugin\\Internal\\LoopVariableReusePlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/LoopVariableReusePlugin.php', + 'Phan\\Plugin\\Internal\\LoopVariableReuseVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/LoopVariableReuseVisitor.php', + 'Phan\\Plugin\\Internal\\MethodSearcherPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/MethodSearcherPlugin.php', + 'Phan\\Plugin\\Internal\\MiscParamPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/MiscParamPlugin.php', + 'Phan\\Plugin\\Internal\\NodeSelectionPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/NodeSelectionPlugin.php', + 'Phan\\Plugin\\Internal\\PhantasmPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/PhantasmPlugin.php', + 'Phan\\Plugin\\Internal\\PhantasmPlugin\\PhantasmVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/PhantasmPlugin/PhantasmVisitor.php', + 'Phan\\Plugin\\Internal\\RedundantConditionCallPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/RedundantConditionCallPlugin.php', + 'Phan\\Plugin\\Internal\\RedundantConditionLoopCheck' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/RedundantConditionLoopCheck.php', + 'Phan\\Plugin\\Internal\\RedundantConditionVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/RedundantConditionVisitor.php', + 'Phan\\Plugin\\Internal\\RequireExistsPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/RequireExistsPlugin.php', + 'Phan\\Plugin\\Internal\\StringFunctionPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/StringFunctionPlugin.php', + 'Phan\\Plugin\\Internal\\ThrowAnalyzerPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/ThrowAnalyzerPlugin.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\PureMethodGraph' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/PureMethodGraph.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\PureMethodInferrer' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/PureMethodInferrer.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\RedundantReturnVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/RedundantReturnVisitor.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\StatsForFQSEN' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/StatsForFQSEN.php', + 'Phan\\Plugin\\Internal\\UseReturnValuePlugin\\UseReturnValueVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/UseReturnValuePlugin/UseReturnValueVisitor.php', + 'Phan\\Plugin\\Internal\\VariableTrackerPlugin' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/VariableTrackerPlugin.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableGraph' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableGraph.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableTrackerVisitor' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableTrackerVisitor.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableTrackingBranchScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableTrackingBranchScope.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableTrackingLoopScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableTrackingLoopScope.php', + 'Phan\\Plugin\\Internal\\VariableTracker\\VariableTrackingScope' => __DIR__ . '/..' . '/phan/phan/src/Phan/Plugin/Internal/VariableTracker/VariableTrackingScope.php', + 'Phan\\Prep' => __DIR__ . '/..' . '/phan/phan/src/Phan/Prep.php', + 'Phan\\Profile' => __DIR__ . '/..' . '/phan/phan/src/Phan/Profile.php', + 'Phan\\Suggestion' => __DIR__ . '/..' . '/phan/phan/src/Phan/Suggestion.php', + 'Psr\\Container\\ContainerExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerExceptionInterface.php', + 'Psr\\Container\\ContainerInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerInterface.php', + 'Psr\\Container\\NotFoundExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/NotFoundExceptionInterface.php', 'Psr\\Log\\AbstractLogger' => __DIR__ . '/..' . '/psr/log/Psr/Log/AbstractLogger.php', 'Psr\\Log\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/log/Psr/Log/InvalidArgumentException.php', 'Psr\\Log\\LogLevel' => __DIR__ . '/..' . '/psr/log/Psr/Log/LogLevel.php', @@ -69,260 +1059,226 @@ class ComposerStaticInitcbda25b16bb8365467298ce193f0f30c 'Psr\\Log\\LoggerInterface' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerInterface.php', 'Psr\\Log\\LoggerTrait' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerTrait.php', 'Psr\\Log\\NullLogger' => __DIR__ . '/..' . '/psr/log/Psr/Log/NullLogger.php', - 'Psr\\Log\\Test\\DummyTest' => __DIR__ . '/..' . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php', 'Psr\\Log\\Test\\LoggerInterfaceTest' => __DIR__ . '/..' . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php', 'Psr\\SimpleCache\\CacheException' => __DIR__ . '/..' . '/psr/simple-cache/src/CacheException.php', 'Psr\\SimpleCache\\CacheInterface' => __DIR__ . '/..' . '/psr/simple-cache/src/CacheInterface.php', 'Psr\\SimpleCache\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/simple-cache/src/InvalidArgumentException.php', - 'Zend\\Db\\Adapter\\Adapter' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Adapter.php', - 'Zend\\Db\\Adapter\\AdapterAbstractServiceFactory' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/AdapterAbstractServiceFactory.php', - 'Zend\\Db\\Adapter\\AdapterAwareInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/AdapterAwareInterface.php', - 'Zend\\Db\\Adapter\\AdapterAwareTrait' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/AdapterAwareTrait.php', - 'Zend\\Db\\Adapter\\AdapterInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/AdapterInterface.php', - 'Zend\\Db\\Adapter\\AdapterServiceFactory' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/AdapterServiceFactory.php', - 'Zend\\Db\\Adapter\\Driver\\AbstractConnection' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/AbstractConnection.php', - 'Zend\\Db\\Adapter\\Driver\\ConnectionInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/ConnectionInterface.php', - 'Zend\\Db\\Adapter\\Driver\\DriverInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/DriverInterface.php', - 'Zend\\Db\\Adapter\\Driver\\Feature\\AbstractFeature' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Feature/AbstractFeature.php', - 'Zend\\Db\\Adapter\\Driver\\Feature\\DriverFeatureInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Feature/DriverFeatureInterface.php', - 'Zend\\Db\\Adapter\\Driver\\IbmDb2\\Connection' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/IbmDb2/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\IbmDb2\\IbmDb2' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/IbmDb2/IbmDb2.php', - 'Zend\\Db\\Adapter\\Driver\\IbmDb2\\Result' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/IbmDb2/Result.php', - 'Zend\\Db\\Adapter\\Driver\\IbmDb2\\Statement' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/IbmDb2/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\Mysqli\\Connection' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Mysqli/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Mysqli\\Mysqli' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Mysqli/Mysqli.php', - 'Zend\\Db\\Adapter\\Driver\\Mysqli\\Result' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Mysqli/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Mysqli\\Statement' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Mysqli/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Connection' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Feature\\RowCounter' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Feature/RowCounter.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Oci8' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Oci8.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Result' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Oci8\\Statement' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Oci8/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Connection' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Feature\\OracleRowCounter' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Feature/OracleRowCounter.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Feature\\SqliteRowCounter' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Pdo' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Pdo.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Result' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Pdo\\Statement' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pdo/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\Pgsql\\Connection' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pgsql/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Pgsql\\Pgsql' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pgsql/Pgsql.php', - 'Zend\\Db\\Adapter\\Driver\\Pgsql\\Result' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pgsql/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Pgsql\\Statement' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Pgsql/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\ResultInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/ResultInterface.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Connection' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Connection.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Exception\\ErrorException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Exception/ErrorException.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Result' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Result.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Sqlsrv' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Sqlsrv.php', - 'Zend\\Db\\Adapter\\Driver\\Sqlsrv\\Statement' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/Sqlsrv/Statement.php', - 'Zend\\Db\\Adapter\\Driver\\StatementInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Driver/StatementInterface.php', - 'Zend\\Db\\Adapter\\Exception\\ErrorException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Exception/ErrorException.php', - 'Zend\\Db\\Adapter\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Exception/ExceptionInterface.php', - 'Zend\\Db\\Adapter\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Exception/InvalidArgumentException.php', - 'Zend\\Db\\Adapter\\Exception\\InvalidConnectionParametersException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Exception/InvalidConnectionParametersException.php', - 'Zend\\Db\\Adapter\\Exception\\InvalidQueryException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Exception/InvalidQueryException.php', - 'Zend\\Db\\Adapter\\Exception\\RuntimeException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Exception/RuntimeException.php', - 'Zend\\Db\\Adapter\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Exception/UnexpectedValueException.php', - 'Zend\\Db\\Adapter\\ParameterContainer' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/ParameterContainer.php', - 'Zend\\Db\\Adapter\\Platform\\AbstractPlatform' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Platform/AbstractPlatform.php', - 'Zend\\Db\\Adapter\\Platform\\IbmDb2' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Platform/IbmDb2.php', - 'Zend\\Db\\Adapter\\Platform\\Mysql' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Platform/Mysql.php', - 'Zend\\Db\\Adapter\\Platform\\Oracle' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Platform/Oracle.php', - 'Zend\\Db\\Adapter\\Platform\\PlatformInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Platform/PlatformInterface.php', - 'Zend\\Db\\Adapter\\Platform\\Postgresql' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Platform/Postgresql.php', - 'Zend\\Db\\Adapter\\Platform\\Sql92' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Platform/Sql92.php', - 'Zend\\Db\\Adapter\\Platform\\SqlServer' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Platform/SqlServer.php', - 'Zend\\Db\\Adapter\\Platform\\Sqlite' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Platform/Sqlite.php', - 'Zend\\Db\\Adapter\\Profiler\\Profiler' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Profiler/Profiler.php', - 'Zend\\Db\\Adapter\\Profiler\\ProfilerAwareInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Profiler/ProfilerAwareInterface.php', - 'Zend\\Db\\Adapter\\Profiler\\ProfilerInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/Profiler/ProfilerInterface.php', - 'Zend\\Db\\Adapter\\StatementContainer' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/StatementContainer.php', - 'Zend\\Db\\Adapter\\StatementContainerInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Adapter/StatementContainerInterface.php', - 'Zend\\Db\\ConfigProvider' => __DIR__ . '/..' . '/zendframework/zend-db/src/ConfigProvider.php', - 'Zend\\Db\\Exception\\ErrorException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Exception/ErrorException.php', - 'Zend\\Db\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Exception/ExceptionInterface.php', - 'Zend\\Db\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Exception/InvalidArgumentException.php', - 'Zend\\Db\\Exception\\RuntimeException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Exception/RuntimeException.php', - 'Zend\\Db\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Exception/UnexpectedValueException.php', - 'Zend\\Db\\Metadata\\Metadata' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Metadata.php', - 'Zend\\Db\\Metadata\\MetadataInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/MetadataInterface.php', - 'Zend\\Db\\Metadata\\Object\\AbstractTableObject' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Object/AbstractTableObject.php', - 'Zend\\Db\\Metadata\\Object\\ColumnObject' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Object/ColumnObject.php', - 'Zend\\Db\\Metadata\\Object\\ConstraintKeyObject' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Object/ConstraintKeyObject.php', - 'Zend\\Db\\Metadata\\Object\\ConstraintObject' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Object/ConstraintObject.php', - 'Zend\\Db\\Metadata\\Object\\TableObject' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Object/TableObject.php', - 'Zend\\Db\\Metadata\\Object\\TriggerObject' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Object/TriggerObject.php', - 'Zend\\Db\\Metadata\\Object\\ViewObject' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Object/ViewObject.php', - 'Zend\\Db\\Metadata\\Source\\AbstractSource' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Source/AbstractSource.php', - 'Zend\\Db\\Metadata\\Source\\Factory' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Source/Factory.php', - 'Zend\\Db\\Metadata\\Source\\MysqlMetadata' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Source/MysqlMetadata.php', - 'Zend\\Db\\Metadata\\Source\\OracleMetadata' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Source/OracleMetadata.php', - 'Zend\\Db\\Metadata\\Source\\PostgresqlMetadata' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Source/PostgresqlMetadata.php', - 'Zend\\Db\\Metadata\\Source\\SqlServerMetadata' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Source/SqlServerMetadata.php', - 'Zend\\Db\\Metadata\\Source\\SqliteMetadata' => __DIR__ . '/..' . '/zendframework/zend-db/src/Metadata/Source/SqliteMetadata.php', - 'Zend\\Db\\Module' => __DIR__ . '/..' . '/zendframework/zend-db/src/Module.php', - 'Zend\\Db\\ResultSet\\AbstractResultSet' => __DIR__ . '/..' . '/zendframework/zend-db/src/ResultSet/AbstractResultSet.php', - 'Zend\\Db\\ResultSet\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/ResultSet/Exception/ExceptionInterface.php', - 'Zend\\Db\\ResultSet\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/zendframework/zend-db/src/ResultSet/Exception/InvalidArgumentException.php', - 'Zend\\Db\\ResultSet\\Exception\\RuntimeException' => __DIR__ . '/..' . '/zendframework/zend-db/src/ResultSet/Exception/RuntimeException.php', - 'Zend\\Db\\ResultSet\\HydratingResultSet' => __DIR__ . '/..' . '/zendframework/zend-db/src/ResultSet/HydratingResultSet.php', - 'Zend\\Db\\ResultSet\\ResultSet' => __DIR__ . '/..' . '/zendframework/zend-db/src/ResultSet/ResultSet.php', - 'Zend\\Db\\ResultSet\\ResultSetInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/ResultSet/ResultSetInterface.php', - 'Zend\\Db\\RowGateway\\AbstractRowGateway' => __DIR__ . '/..' . '/zendframework/zend-db/src/RowGateway/AbstractRowGateway.php', - 'Zend\\Db\\RowGateway\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/RowGateway/Exception/ExceptionInterface.php', - 'Zend\\Db\\RowGateway\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/zendframework/zend-db/src/RowGateway/Exception/InvalidArgumentException.php', - 'Zend\\Db\\RowGateway\\Exception\\RuntimeException' => __DIR__ . '/..' . '/zendframework/zend-db/src/RowGateway/Exception/RuntimeException.php', - 'Zend\\Db\\RowGateway\\Feature\\AbstractFeature' => __DIR__ . '/..' . '/zendframework/zend-db/src/RowGateway/Feature/AbstractFeature.php', - 'Zend\\Db\\RowGateway\\Feature\\FeatureSet' => __DIR__ . '/..' . '/zendframework/zend-db/src/RowGateway/Feature/FeatureSet.php', - 'Zend\\Db\\RowGateway\\RowGateway' => __DIR__ . '/..' . '/zendframework/zend-db/src/RowGateway/RowGateway.php', - 'Zend\\Db\\RowGateway\\RowGatewayInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/RowGateway/RowGatewayInterface.php', - 'Zend\\Db\\Sql\\AbstractExpression' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/AbstractExpression.php', - 'Zend\\Db\\Sql\\AbstractPreparableSql' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/AbstractPreparableSql.php', - 'Zend\\Db\\Sql\\AbstractSql' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/AbstractSql.php', - 'Zend\\Db\\Sql\\Combine' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Combine.php', - 'Zend\\Db\\Sql\\Ddl\\AlterTable' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/AlterTable.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\AbstractLengthColumn' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/AbstractLengthColumn.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\AbstractPrecisionColumn' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/AbstractPrecisionColumn.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\AbstractTimestampColumn' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/AbstractTimestampColumn.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\BigInteger' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/BigInteger.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Binary' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Binary.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Blob' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Blob.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Boolean' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Boolean.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Char' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Char.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Column' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Column.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\ColumnInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/ColumnInterface.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Date' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Date.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Datetime' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Datetime.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Decimal' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Decimal.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Float' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Float.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Floating' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Floating.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Integer' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Integer.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Text' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Text.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Time' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Time.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Timestamp' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Timestamp.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Varbinary' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Varbinary.php', - 'Zend\\Db\\Sql\\Ddl\\Column\\Varchar' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Column/Varchar.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\AbstractConstraint' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Constraint/AbstractConstraint.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\Check' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Constraint/Check.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\ConstraintInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Constraint/ConstraintInterface.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\ForeignKey' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Constraint/ForeignKey.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\PrimaryKey' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Constraint/PrimaryKey.php', - 'Zend\\Db\\Sql\\Ddl\\Constraint\\UniqueKey' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Constraint/UniqueKey.php', - 'Zend\\Db\\Sql\\Ddl\\CreateTable' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/CreateTable.php', - 'Zend\\Db\\Sql\\Ddl\\DropTable' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/DropTable.php', - 'Zend\\Db\\Sql\\Ddl\\Index\\AbstractIndex' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Index/AbstractIndex.php', - 'Zend\\Db\\Sql\\Ddl\\Index\\Index' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/Index/Index.php', - 'Zend\\Db\\Sql\\Ddl\\SqlInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Ddl/SqlInterface.php', - 'Zend\\Db\\Sql\\Delete' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Delete.php', - 'Zend\\Db\\Sql\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Exception/ExceptionInterface.php', - 'Zend\\Db\\Sql\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Exception/InvalidArgumentException.php', - 'Zend\\Db\\Sql\\Exception\\RuntimeException' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Exception/RuntimeException.php', - 'Zend\\Db\\Sql\\Expression' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Expression.php', - 'Zend\\Db\\Sql\\ExpressionInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/ExpressionInterface.php', - 'Zend\\Db\\Sql\\Having' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Having.php', - 'Zend\\Db\\Sql\\Insert' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Insert.php', - 'Zend\\Db\\Sql\\Join' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Join.php', - 'Zend\\Db\\Sql\\Literal' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Literal.php', - 'Zend\\Db\\Sql\\Platform\\AbstractPlatform' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/AbstractPlatform.php', - 'Zend\\Db\\Sql\\Platform\\IbmDb2\\IbmDb2' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/IbmDb2/IbmDb2.php', - 'Zend\\Db\\Sql\\Platform\\IbmDb2\\SelectDecorator' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/IbmDb2/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Mysql\\Ddl\\AlterTableDecorator' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Mysql\\Ddl\\CreateTableDecorator' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Mysql\\Mysql' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/Mysql/Mysql.php', - 'Zend\\Db\\Sql\\Platform\\Mysql\\SelectDecorator' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/Mysql/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Oracle\\Oracle' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/Oracle/Oracle.php', - 'Zend\\Db\\Sql\\Platform\\Oracle\\SelectDecorator' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/Oracle/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Platform' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/Platform.php', - 'Zend\\Db\\Sql\\Platform\\PlatformDecoratorInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/PlatformDecoratorInterface.php', - 'Zend\\Db\\Sql\\Platform\\SqlServer\\Ddl\\CreateTableDecorator' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/SqlServer/Ddl/CreateTableDecorator.php', - 'Zend\\Db\\Sql\\Platform\\SqlServer\\SelectDecorator' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/SqlServer/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\SqlServer\\SqlServer' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/SqlServer/SqlServer.php', - 'Zend\\Db\\Sql\\Platform\\Sqlite\\SelectDecorator' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/Sqlite/SelectDecorator.php', - 'Zend\\Db\\Sql\\Platform\\Sqlite\\Sqlite' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Platform/Sqlite/Sqlite.php', - 'Zend\\Db\\Sql\\Predicate\\Between' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/Between.php', - 'Zend\\Db\\Sql\\Predicate\\Expression' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/Expression.php', - 'Zend\\Db\\Sql\\Predicate\\In' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/In.php', - 'Zend\\Db\\Sql\\Predicate\\IsNotNull' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/IsNotNull.php', - 'Zend\\Db\\Sql\\Predicate\\IsNull' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/IsNull.php', - 'Zend\\Db\\Sql\\Predicate\\Like' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/Like.php', - 'Zend\\Db\\Sql\\Predicate\\Literal' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/Literal.php', - 'Zend\\Db\\Sql\\Predicate\\NotBetween' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/NotBetween.php', - 'Zend\\Db\\Sql\\Predicate\\NotIn' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/NotIn.php', - 'Zend\\Db\\Sql\\Predicate\\NotLike' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/NotLike.php', - 'Zend\\Db\\Sql\\Predicate\\Operator' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/Operator.php', - 'Zend\\Db\\Sql\\Predicate\\Predicate' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/Predicate.php', - 'Zend\\Db\\Sql\\Predicate\\PredicateInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/PredicateInterface.php', - 'Zend\\Db\\Sql\\Predicate\\PredicateSet' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Predicate/PredicateSet.php', - 'Zend\\Db\\Sql\\PreparableSqlInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/PreparableSqlInterface.php', - 'Zend\\Db\\Sql\\Select' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Select.php', - 'Zend\\Db\\Sql\\Sql' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Sql.php', - 'Zend\\Db\\Sql\\SqlInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/SqlInterface.php', - 'Zend\\Db\\Sql\\TableIdentifier' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/TableIdentifier.php', - 'Zend\\Db\\Sql\\Update' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Update.php', - 'Zend\\Db\\Sql\\Where' => __DIR__ . '/..' . '/zendframework/zend-db/src/Sql/Where.php', - 'Zend\\Db\\TableGateway\\AbstractTableGateway' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/AbstractTableGateway.php', - 'Zend\\Db\\TableGateway\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Exception/ExceptionInterface.php', - 'Zend\\Db\\TableGateway\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Exception/InvalidArgumentException.php', - 'Zend\\Db\\TableGateway\\Exception\\RuntimeException' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Exception/RuntimeException.php', - 'Zend\\Db\\TableGateway\\Feature\\AbstractFeature' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/AbstractFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\EventFeature' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/EventFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\EventFeatureEventsInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/EventFeatureEventsInterface.php', - 'Zend\\Db\\TableGateway\\Feature\\EventFeature\\TableGatewayEvent' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php', - 'Zend\\Db\\TableGateway\\Feature\\FeatureSet' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/FeatureSet.php', - 'Zend\\Db\\TableGateway\\Feature\\GlobalAdapterFeature' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/GlobalAdapterFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\MasterSlaveFeature' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/MasterSlaveFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\MetadataFeature' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/MetadataFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\RowGatewayFeature' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/RowGatewayFeature.php', - 'Zend\\Db\\TableGateway\\Feature\\SequenceFeature' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/Feature/SequenceFeature.php', - 'Zend\\Db\\TableGateway\\TableGateway' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/TableGateway.php', - 'Zend\\Db\\TableGateway\\TableGatewayInterface' => __DIR__ . '/..' . '/zendframework/zend-db/src/TableGateway/TableGatewayInterface.php', - 'Zend\\Stdlib\\AbstractOptions' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/AbstractOptions.php', - 'Zend\\Stdlib\\ArrayObject' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ArrayObject.php', - 'Zend\\Stdlib\\ArraySerializableInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ArraySerializableInterface.php', - 'Zend\\Stdlib\\ArrayStack' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ArrayStack.php', - 'Zend\\Stdlib\\ArrayUtils' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ArrayUtils.php', - 'Zend\\Stdlib\\ArrayUtils\\MergeRemoveKey' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ArrayUtils/MergeRemoveKey.php', - 'Zend\\Stdlib\\ArrayUtils\\MergeReplaceKey' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ArrayUtils/MergeReplaceKey.php', - 'Zend\\Stdlib\\ArrayUtils\\MergeReplaceKeyInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ArrayUtils/MergeReplaceKeyInterface.php', - 'Zend\\Stdlib\\ConsoleHelper' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ConsoleHelper.php', - 'Zend\\Stdlib\\DispatchableInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/DispatchableInterface.php', - 'Zend\\Stdlib\\ErrorHandler' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ErrorHandler.php', - 'Zend\\Stdlib\\Exception\\BadMethodCallException' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Exception/BadMethodCallException.php', - 'Zend\\Stdlib\\Exception\\DomainException' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Exception/DomainException.php', - 'Zend\\Stdlib\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Exception/ExceptionInterface.php', - 'Zend\\Stdlib\\Exception\\ExtensionNotLoadedException' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Exception/ExtensionNotLoadedException.php', - 'Zend\\Stdlib\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Exception/InvalidArgumentException.php', - 'Zend\\Stdlib\\Exception\\LogicException' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Exception/LogicException.php', - 'Zend\\Stdlib\\Exception\\RuntimeException' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Exception/RuntimeException.php', - 'Zend\\Stdlib\\FastPriorityQueue' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/FastPriorityQueue.php', - 'Zend\\Stdlib\\Glob' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Glob.php', - 'Zend\\Stdlib\\Guard\\AllGuardsTrait' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Guard/AllGuardsTrait.php', - 'Zend\\Stdlib\\Guard\\ArrayOrTraversableGuardTrait' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Guard/ArrayOrTraversableGuardTrait.php', - 'Zend\\Stdlib\\Guard\\EmptyGuardTrait' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Guard/EmptyGuardTrait.php', - 'Zend\\Stdlib\\Guard\\NullGuardTrait' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Guard/NullGuardTrait.php', - 'Zend\\Stdlib\\InitializableInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/InitializableInterface.php', - 'Zend\\Stdlib\\JsonSerializable' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/JsonSerializable.php', - 'Zend\\Stdlib\\Message' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Message.php', - 'Zend\\Stdlib\\MessageInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/MessageInterface.php', - 'Zend\\Stdlib\\ParameterObjectInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ParameterObjectInterface.php', - 'Zend\\Stdlib\\Parameters' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Parameters.php', - 'Zend\\Stdlib\\ParametersInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ParametersInterface.php', - 'Zend\\Stdlib\\PriorityList' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/PriorityList.php', - 'Zend\\Stdlib\\PriorityQueue' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/PriorityQueue.php', - 'Zend\\Stdlib\\Request' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Request.php', - 'Zend\\Stdlib\\RequestInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/RequestInterface.php', - 'Zend\\Stdlib\\Response' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/Response.php', - 'Zend\\Stdlib\\ResponseInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/ResponseInterface.php', - 'Zend\\Stdlib\\SplPriorityQueue' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/SplPriorityQueue.php', - 'Zend\\Stdlib\\SplQueue' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/SplQueue.php', - 'Zend\\Stdlib\\SplStack' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/SplStack.php', - 'Zend\\Stdlib\\StringUtils' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/StringUtils.php', - 'Zend\\Stdlib\\StringWrapper\\AbstractStringWrapper' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/StringWrapper/AbstractStringWrapper.php', - 'Zend\\Stdlib\\StringWrapper\\Iconv' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/StringWrapper/Iconv.php', - 'Zend\\Stdlib\\StringWrapper\\Intl' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/StringWrapper/Intl.php', - 'Zend\\Stdlib\\StringWrapper\\MbString' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/StringWrapper/MbString.php', - 'Zend\\Stdlib\\StringWrapper\\Native' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/StringWrapper/Native.php', - 'Zend\\Stdlib\\StringWrapper\\StringWrapperInterface' => __DIR__ . '/..' . '/zendframework/zend-stdlib/src/StringWrapper/StringWrapperInterface.php', + 'Sabre\\Event\\Emitter' => __DIR__ . '/..' . '/sabre/event/lib/Emitter.php', + 'Sabre\\Event\\EmitterInterface' => __DIR__ . '/..' . '/sabre/event/lib/EmitterInterface.php', + 'Sabre\\Event\\EmitterTrait' => __DIR__ . '/..' . '/sabre/event/lib/EmitterTrait.php', + 'Sabre\\Event\\EventEmitter' => __DIR__ . '/..' . '/sabre/event/lib/EventEmitter.php', + 'Sabre\\Event\\Loop\\Loop' => __DIR__ . '/..' . '/sabre/event/lib/Loop/Loop.php', + 'Sabre\\Event\\Promise' => __DIR__ . '/..' . '/sabre/event/lib/Promise.php', + 'Sabre\\Event\\PromiseAlreadyResolvedException' => __DIR__ . '/..' . '/sabre/event/lib/PromiseAlreadyResolvedException.php', + 'Sabre\\Event\\Version' => __DIR__ . '/..' . '/sabre/event/lib/Version.php', + 'Sabre\\Event\\WildcardEmitter' => __DIR__ . '/..' . '/sabre/event/lib/WildcardEmitter.php', + 'Sabre\\Event\\WildcardEmitterTrait' => __DIR__ . '/..' . '/sabre/event/lib/WildcardEmitterTrait.php', + 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'Symfony\\Component\\Console\\Application' => __DIR__ . '/..' . '/symfony/console/Application.php', + 'Symfony\\Component\\Console\\Color' => __DIR__ . '/..' . '/symfony/console/Color.php', + 'Symfony\\Component\\Console\\CommandLoader\\CommandLoaderInterface' => __DIR__ . '/..' . '/symfony/console/CommandLoader/CommandLoaderInterface.php', + 'Symfony\\Component\\Console\\CommandLoader\\ContainerCommandLoader' => __DIR__ . '/..' . '/symfony/console/CommandLoader/ContainerCommandLoader.php', + 'Symfony\\Component\\Console\\CommandLoader\\FactoryCommandLoader' => __DIR__ . '/..' . '/symfony/console/CommandLoader/FactoryCommandLoader.php', + 'Symfony\\Component\\Console\\Command\\Command' => __DIR__ . '/..' . '/symfony/console/Command/Command.php', + 'Symfony\\Component\\Console\\Command\\HelpCommand' => __DIR__ . '/..' . '/symfony/console/Command/HelpCommand.php', + 'Symfony\\Component\\Console\\Command\\ListCommand' => __DIR__ . '/..' . '/symfony/console/Command/ListCommand.php', + 'Symfony\\Component\\Console\\Command\\LockableTrait' => __DIR__ . '/..' . '/symfony/console/Command/LockableTrait.php', + 'Symfony\\Component\\Console\\Command\\SignalableCommandInterface' => __DIR__ . '/..' . '/symfony/console/Command/SignalableCommandInterface.php', + 'Symfony\\Component\\Console\\ConsoleEvents' => __DIR__ . '/..' . '/symfony/console/ConsoleEvents.php', + 'Symfony\\Component\\Console\\Cursor' => __DIR__ . '/..' . '/symfony/console/Cursor.php', + 'Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPass' => __DIR__ . '/..' . '/symfony/console/DependencyInjection/AddConsoleCommandPass.php', + 'Symfony\\Component\\Console\\Descriptor\\ApplicationDescription' => __DIR__ . '/..' . '/symfony/console/Descriptor/ApplicationDescription.php', + 'Symfony\\Component\\Console\\Descriptor\\Descriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/Descriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\DescriptorInterface' => __DIR__ . '/..' . '/symfony/console/Descriptor/DescriptorInterface.php', + 'Symfony\\Component\\Console\\Descriptor\\JsonDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/JsonDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\MarkdownDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/MarkdownDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\TextDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/TextDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\XmlDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/XmlDescriptor.php', + 'Symfony\\Component\\Console\\EventListener\\ErrorListener' => __DIR__ . '/..' . '/symfony/console/EventListener/ErrorListener.php', + 'Symfony\\Component\\Console\\Event\\ConsoleCommandEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleCommandEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleErrorEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleErrorEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleSignalEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleSignalEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleTerminateEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleTerminateEvent.php', + 'Symfony\\Component\\Console\\Exception\\CommandNotFoundException' => __DIR__ . '/..' . '/symfony/console/Exception/CommandNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/console/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Console\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/console/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Console\\Exception\\InvalidOptionException' => __DIR__ . '/..' . '/symfony/console/Exception/InvalidOptionException.php', + 'Symfony\\Component\\Console\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/console/Exception/LogicException.php', + 'Symfony\\Component\\Console\\Exception\\MissingInputException' => __DIR__ . '/..' . '/symfony/console/Exception/MissingInputException.php', + 'Symfony\\Component\\Console\\Exception\\NamespaceNotFoundException' => __DIR__ . '/..' . '/symfony/console/Exception/NamespaceNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/console/Exception/RuntimeException.php', + 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatter' => __DIR__ . '/..' . '/symfony/console/Formatter/NullOutputFormatter.php', + 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatterStyle' => __DIR__ . '/..' . '/symfony/console/Formatter/NullOutputFormatterStyle.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatter' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatter.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterInterface' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterInterface.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyle' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterStyle.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleInterface' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterStyleInterface.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleStack' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterStyleStack.php', + 'Symfony\\Component\\Console\\Formatter\\WrappableOutputFormatterInterface' => __DIR__ . '/..' . '/symfony/console/Formatter/WrappableOutputFormatterInterface.php', + 'Symfony\\Component\\Console\\Helper\\DebugFormatterHelper' => __DIR__ . '/..' . '/symfony/console/Helper/DebugFormatterHelper.php', + 'Symfony\\Component\\Console\\Helper\\DescriptorHelper' => __DIR__ . '/..' . '/symfony/console/Helper/DescriptorHelper.php', + 'Symfony\\Component\\Console\\Helper\\Dumper' => __DIR__ . '/..' . '/symfony/console/Helper/Dumper.php', + 'Symfony\\Component\\Console\\Helper\\FormatterHelper' => __DIR__ . '/..' . '/symfony/console/Helper/FormatterHelper.php', + 'Symfony\\Component\\Console\\Helper\\Helper' => __DIR__ . '/..' . '/symfony/console/Helper/Helper.php', + 'Symfony\\Component\\Console\\Helper\\HelperInterface' => __DIR__ . '/..' . '/symfony/console/Helper/HelperInterface.php', + 'Symfony\\Component\\Console\\Helper\\HelperSet' => __DIR__ . '/..' . '/symfony/console/Helper/HelperSet.php', + 'Symfony\\Component\\Console\\Helper\\InputAwareHelper' => __DIR__ . '/..' . '/symfony/console/Helper/InputAwareHelper.php', + 'Symfony\\Component\\Console\\Helper\\ProcessHelper' => __DIR__ . '/..' . '/symfony/console/Helper/ProcessHelper.php', + 'Symfony\\Component\\Console\\Helper\\ProgressBar' => __DIR__ . '/..' . '/symfony/console/Helper/ProgressBar.php', + 'Symfony\\Component\\Console\\Helper\\ProgressIndicator' => __DIR__ . '/..' . '/symfony/console/Helper/ProgressIndicator.php', + 'Symfony\\Component\\Console\\Helper\\QuestionHelper' => __DIR__ . '/..' . '/symfony/console/Helper/QuestionHelper.php', + 'Symfony\\Component\\Console\\Helper\\SymfonyQuestionHelper' => __DIR__ . '/..' . '/symfony/console/Helper/SymfonyQuestionHelper.php', + 'Symfony\\Component\\Console\\Helper\\Table' => __DIR__ . '/..' . '/symfony/console/Helper/Table.php', + 'Symfony\\Component\\Console\\Helper\\TableCell' => __DIR__ . '/..' . '/symfony/console/Helper/TableCell.php', + 'Symfony\\Component\\Console\\Helper\\TableCellStyle' => __DIR__ . '/..' . '/symfony/console/Helper/TableCellStyle.php', + 'Symfony\\Component\\Console\\Helper\\TableRows' => __DIR__ . '/..' . '/symfony/console/Helper/TableRows.php', + 'Symfony\\Component\\Console\\Helper\\TableSeparator' => __DIR__ . '/..' . '/symfony/console/Helper/TableSeparator.php', + 'Symfony\\Component\\Console\\Helper\\TableStyle' => __DIR__ . '/..' . '/symfony/console/Helper/TableStyle.php', + 'Symfony\\Component\\Console\\Input\\ArgvInput' => __DIR__ . '/..' . '/symfony/console/Input/ArgvInput.php', + 'Symfony\\Component\\Console\\Input\\ArrayInput' => __DIR__ . '/..' . '/symfony/console/Input/ArrayInput.php', + 'Symfony\\Component\\Console\\Input\\Input' => __DIR__ . '/..' . '/symfony/console/Input/Input.php', + 'Symfony\\Component\\Console\\Input\\InputArgument' => __DIR__ . '/..' . '/symfony/console/Input/InputArgument.php', + 'Symfony\\Component\\Console\\Input\\InputAwareInterface' => __DIR__ . '/..' . '/symfony/console/Input/InputAwareInterface.php', + 'Symfony\\Component\\Console\\Input\\InputDefinition' => __DIR__ . '/..' . '/symfony/console/Input/InputDefinition.php', + 'Symfony\\Component\\Console\\Input\\InputInterface' => __DIR__ . '/..' . '/symfony/console/Input/InputInterface.php', + 'Symfony\\Component\\Console\\Input\\InputOption' => __DIR__ . '/..' . '/symfony/console/Input/InputOption.php', + 'Symfony\\Component\\Console\\Input\\StreamableInputInterface' => __DIR__ . '/..' . '/symfony/console/Input/StreamableInputInterface.php', + 'Symfony\\Component\\Console\\Input\\StringInput' => __DIR__ . '/..' . '/symfony/console/Input/StringInput.php', + 'Symfony\\Component\\Console\\Logger\\ConsoleLogger' => __DIR__ . '/..' . '/symfony/console/Logger/ConsoleLogger.php', + 'Symfony\\Component\\Console\\Output\\BufferedOutput' => __DIR__ . '/..' . '/symfony/console/Output/BufferedOutput.php', + 'Symfony\\Component\\Console\\Output\\ConsoleOutput' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleOutput.php', + 'Symfony\\Component\\Console\\Output\\ConsoleOutputInterface' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleOutputInterface.php', + 'Symfony\\Component\\Console\\Output\\ConsoleSectionOutput' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleSectionOutput.php', + 'Symfony\\Component\\Console\\Output\\NullOutput' => __DIR__ . '/..' . '/symfony/console/Output/NullOutput.php', + 'Symfony\\Component\\Console\\Output\\Output' => __DIR__ . '/..' . '/symfony/console/Output/Output.php', + 'Symfony\\Component\\Console\\Output\\OutputInterface' => __DIR__ . '/..' . '/symfony/console/Output/OutputInterface.php', + 'Symfony\\Component\\Console\\Output\\StreamOutput' => __DIR__ . '/..' . '/symfony/console/Output/StreamOutput.php', + 'Symfony\\Component\\Console\\Output\\TrimmedBufferOutput' => __DIR__ . '/..' . '/symfony/console/Output/TrimmedBufferOutput.php', + 'Symfony\\Component\\Console\\Question\\ChoiceQuestion' => __DIR__ . '/..' . '/symfony/console/Question/ChoiceQuestion.php', + 'Symfony\\Component\\Console\\Question\\ConfirmationQuestion' => __DIR__ . '/..' . '/symfony/console/Question/ConfirmationQuestion.php', + 'Symfony\\Component\\Console\\Question\\Question' => __DIR__ . '/..' . '/symfony/console/Question/Question.php', + 'Symfony\\Component\\Console\\SignalRegistry\\SignalRegistry' => __DIR__ . '/..' . '/symfony/console/SignalRegistry/SignalRegistry.php', + 'Symfony\\Component\\Console\\SingleCommandApplication' => __DIR__ . '/..' . '/symfony/console/SingleCommandApplication.php', + 'Symfony\\Component\\Console\\Style\\OutputStyle' => __DIR__ . '/..' . '/symfony/console/Style/OutputStyle.php', + 'Symfony\\Component\\Console\\Style\\StyleInterface' => __DIR__ . '/..' . '/symfony/console/Style/StyleInterface.php', + 'Symfony\\Component\\Console\\Style\\SymfonyStyle' => __DIR__ . '/..' . '/symfony/console/Style/SymfonyStyle.php', + 'Symfony\\Component\\Console\\Terminal' => __DIR__ . '/..' . '/symfony/console/Terminal.php', + 'Symfony\\Component\\Console\\Tester\\ApplicationTester' => __DIR__ . '/..' . '/symfony/console/Tester/ApplicationTester.php', + 'Symfony\\Component\\Console\\Tester\\CommandTester' => __DIR__ . '/..' . '/symfony/console/Tester/CommandTester.php', + 'Symfony\\Component\\Console\\Tester\\TesterTrait' => __DIR__ . '/..' . '/symfony/console/Tester/TesterTrait.php', + 'Symfony\\Component\\String\\AbstractString' => __DIR__ . '/..' . '/symfony/string/AbstractString.php', + 'Symfony\\Component\\String\\AbstractUnicodeString' => __DIR__ . '/..' . '/symfony/string/AbstractUnicodeString.php', + 'Symfony\\Component\\String\\ByteString' => __DIR__ . '/..' . '/symfony/string/ByteString.php', + 'Symfony\\Component\\String\\CodePointString' => __DIR__ . '/..' . '/symfony/string/CodePointString.php', + 'Symfony\\Component\\String\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/string/Exception/ExceptionInterface.php', + 'Symfony\\Component\\String\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/string/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\String\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/string/Exception/RuntimeException.php', + 'Symfony\\Component\\String\\Inflector\\EnglishInflector' => __DIR__ . '/..' . '/symfony/string/Inflector/EnglishInflector.php', + 'Symfony\\Component\\String\\Inflector\\FrenchInflector' => __DIR__ . '/..' . '/symfony/string/Inflector/FrenchInflector.php', + 'Symfony\\Component\\String\\Inflector\\InflectorInterface' => __DIR__ . '/..' . '/symfony/string/Inflector/InflectorInterface.php', + 'Symfony\\Component\\String\\LazyString' => __DIR__ . '/..' . '/symfony/string/LazyString.php', + 'Symfony\\Component\\String\\Slugger\\AsciiSlugger' => __DIR__ . '/..' . '/symfony/string/Slugger/AsciiSlugger.php', + 'Symfony\\Component\\String\\Slugger\\SluggerInterface' => __DIR__ . '/..' . '/symfony/string/Slugger/SluggerInterface.php', + 'Symfony\\Component\\String\\UnicodeString' => __DIR__ . '/..' . '/symfony/string/UnicodeString.php', + 'Symfony\\Contracts\\Service\\Attribute\\Required' => __DIR__ . '/..' . '/symfony/service-contracts/Attribute/Required.php', + 'Symfony\\Contracts\\Service\\ResetInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ResetInterface.php', + 'Symfony\\Contracts\\Service\\ServiceLocatorTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceLocatorTrait.php', + 'Symfony\\Contracts\\Service\\ServiceProviderInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceProviderInterface.php', + 'Symfony\\Contracts\\Service\\ServiceSubscriberInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceSubscriberInterface.php', + 'Symfony\\Contracts\\Service\\ServiceSubscriberTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceSubscriberTrait.php', + 'Symfony\\Contracts\\Service\\Test\\ServiceLocatorTest' => __DIR__ . '/..' . '/symfony/service-contracts/Test/ServiceLocatorTest.php', + 'Symfony\\Polyfill\\Ctype\\Ctype' => __DIR__ . '/..' . '/symfony/polyfill-ctype/Ctype.php', + 'Symfony\\Polyfill\\Intl\\Grapheme\\Grapheme' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/Grapheme.php', + 'Symfony\\Polyfill\\Intl\\Normalizer\\Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Normalizer.php', + 'Symfony\\Polyfill\\Mbstring\\Mbstring' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/Mbstring.php', + 'Symfony\\Polyfill\\Php73\\Php73' => __DIR__ . '/..' . '/symfony/polyfill-php73/Php73.php', + 'Symfony\\Polyfill\\Php80\\Php80' => __DIR__ . '/..' . '/symfony/polyfill-php80/Php80.php', + 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', + 'Webmozart\\Assert\\Assert' => __DIR__ . '/..' . '/webmozart/assert/src/Assert.php', + 'Webmozart\\Assert\\InvalidArgumentException' => __DIR__ . '/..' . '/webmozart/assert/src/InvalidArgumentException.php', + 'Webmozart\\Assert\\Mixin' => __DIR__ . '/..' . '/webmozart/assert/src/Mixin.php', + 'phpDocumentor\\Reflection\\DocBlock' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock.php', + 'phpDocumentor\\Reflection\\DocBlockFactory' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlockFactory.php', + 'phpDocumentor\\Reflection\\DocBlockFactoryInterface' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlockFactoryInterface.php', + 'phpDocumentor\\Reflection\\DocBlock\\Description' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Description.php', + 'phpDocumentor\\Reflection\\DocBlock\\DescriptionFactory' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/DescriptionFactory.php', + 'phpDocumentor\\Reflection\\DocBlock\\ExampleFinder' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/ExampleFinder.php', + 'phpDocumentor\\Reflection\\DocBlock\\Serializer' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Serializer.php', + 'phpDocumentor\\Reflection\\DocBlock\\StandardTagFactory' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/StandardTagFactory.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tag' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tag.php', + 'phpDocumentor\\Reflection\\DocBlock\\TagFactory' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/TagFactory.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Author' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Author.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\BaseTag' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/BaseTag.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Covers' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Covers.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Deprecated' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Deprecated.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Example' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Example.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Factory\\StaticMethod' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Factory/StaticMethod.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Formatter' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Formatter.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Formatter\\AlignFormatter' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Formatter/AlignFormatter.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Formatter\\PassthroughFormatter' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Formatter/PassthroughFormatter.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Generic' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Generic.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\InvalidTag' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/InvalidTag.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Link' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Link.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Method' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Method.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Param' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Param.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Property' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Property.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\PropertyRead' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/PropertyRead.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\PropertyWrite' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/PropertyWrite.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Reference\\Fqsen' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Reference/Fqsen.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Reference\\Reference' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Reference/Reference.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Reference\\Url' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Reference/Url.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Return_' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Return_.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\See' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/See.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Since' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Since.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Source' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Source.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\TagWithType' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/TagWithType.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Throws' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Throws.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Uses' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Uses.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Var_' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Var_.php', + 'phpDocumentor\\Reflection\\DocBlock\\Tags\\Version' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Version.php', + 'phpDocumentor\\Reflection\\Element' => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src/Element.php', + 'phpDocumentor\\Reflection\\Exception\\PcreException' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/Exception/PcreException.php', + 'phpDocumentor\\Reflection\\File' => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src/File.php', + 'phpDocumentor\\Reflection\\Fqsen' => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src/Fqsen.php', + 'phpDocumentor\\Reflection\\FqsenResolver' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/FqsenResolver.php', + 'phpDocumentor\\Reflection\\Location' => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src/Location.php', + 'phpDocumentor\\Reflection\\Project' => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src/Project.php', + 'phpDocumentor\\Reflection\\ProjectFactory' => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src/ProjectFactory.php', + 'phpDocumentor\\Reflection\\PseudoType' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/PseudoType.php', + 'phpDocumentor\\Reflection\\PseudoTypes\\False_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/PseudoTypes/False_.php', + 'phpDocumentor\\Reflection\\PseudoTypes\\True_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/PseudoTypes/True_.php', + 'phpDocumentor\\Reflection\\Type' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Type.php', + 'phpDocumentor\\Reflection\\TypeResolver' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/TypeResolver.php', + 'phpDocumentor\\Reflection\\Types\\AbstractList' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/AbstractList.php', + 'phpDocumentor\\Reflection\\Types\\AggregatedType' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/AggregatedType.php', + 'phpDocumentor\\Reflection\\Types\\Array_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Array_.php', + 'phpDocumentor\\Reflection\\Types\\Boolean' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Boolean.php', + 'phpDocumentor\\Reflection\\Types\\Callable_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Callable_.php', + 'phpDocumentor\\Reflection\\Types\\ClassString' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/ClassString.php', + 'phpDocumentor\\Reflection\\Types\\Collection' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Collection.php', + 'phpDocumentor\\Reflection\\Types\\Compound' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Compound.php', + 'phpDocumentor\\Reflection\\Types\\Context' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Context.php', + 'phpDocumentor\\Reflection\\Types\\ContextFactory' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/ContextFactory.php', + 'phpDocumentor\\Reflection\\Types\\Expression' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Expression.php', + 'phpDocumentor\\Reflection\\Types\\Float_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Float_.php', + 'phpDocumentor\\Reflection\\Types\\Integer' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Integer.php', + 'phpDocumentor\\Reflection\\Types\\Intersection' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Intersection.php', + 'phpDocumentor\\Reflection\\Types\\Iterable_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Iterable_.php', + 'phpDocumentor\\Reflection\\Types\\Mixed_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Mixed_.php', + 'phpDocumentor\\Reflection\\Types\\Null_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Null_.php', + 'phpDocumentor\\Reflection\\Types\\Nullable' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Nullable.php', + 'phpDocumentor\\Reflection\\Types\\Object_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Object_.php', + 'phpDocumentor\\Reflection\\Types\\Parent_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Parent_.php', + 'phpDocumentor\\Reflection\\Types\\Resource_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Resource_.php', + 'phpDocumentor\\Reflection\\Types\\Scalar' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Scalar.php', + 'phpDocumentor\\Reflection\\Types\\Self_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Self_.php', + 'phpDocumentor\\Reflection\\Types\\Static_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Static_.php', + 'phpDocumentor\\Reflection\\Types\\String_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/String_.php', + 'phpDocumentor\\Reflection\\Types\\This' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/This.php', + 'phpDocumentor\\Reflection\\Types\\Void_' => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src/Types/Void_.php', + 'phpDocumentor\\Reflection\\Utils' => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src/Utils.php', 'voku\\cache\\AdapterApc' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/AdapterApc.php', 'voku\\cache\\AdapterApcu' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/AdapterApcu.php', 'voku\\cache\\AdapterArray' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/AdapterArray.php', @@ -338,9 +1294,15 @@ class ComposerStaticInitcbda25b16bb8365467298ce193f0f30c 'voku\\cache\\CacheAdapterAutoManager' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/CacheAdapterAutoManager.php', 'voku\\cache\\CacheChain' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/CacheChain.php', 'voku\\cache\\CachePsr16' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/CachePsr16.php', + 'voku\\cache\\Exception\\ChmodException' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/Exception/ChmodException.php', + 'voku\\cache\\Exception\\FileErrorExceptionInterface' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/Exception/FileErrorExceptionInterface.php', 'voku\\cache\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/Exception/InvalidArgumentException.php', + 'voku\\cache\\Exception\\RenameException' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/Exception/RenameException.php', + 'voku\\cache\\Exception\\RuntimeException' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/Exception/RuntimeException.php', + 'voku\\cache\\Exception\\WriteContentException' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/Exception/WriteContentException.php', 'voku\\cache\\SerializerDefault' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/SerializerDefault.php', 'voku\\cache\\SerializerIgbinary' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/SerializerIgbinary.php', + 'voku\\cache\\SerializerMsgpack' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/SerializerMsgpack.php', 'voku\\cache\\SerializerNo' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/SerializerNo.php', 'voku\\cache\\iAdapter' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/iAdapter.php', 'voku\\cache\\iCache' => __DIR__ . '/..' . '/voku/simple-cache/src/voku/cache/iCache.php', diff --git a/bundled-libs/composer/installed.json b/bundled-libs/composer/installed.json index 356a7e480..daf1012bd 100644 --- a/bundled-libs/composer/installed.json +++ b/bundled-libs/composer/installed.json @@ -1,378 +1,1958 @@ -[ - { - "name": "katzgrau/klogger", - "version": "1.0.0", - "version_normalized": "1.0.0.0", - "source": { - "type": "git", - "url": "https://github.com/katzgrau/klogger.git", - "reference": "46cdd92a9b4a8443120cc955bf831450cb274813" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/katzgrau/klogger/zipball/46cdd92a9b4a8443120cc955bf831450cb274813", - "reference": "46cdd92a9b4a8443120cc955bf831450cb274813", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "psr/log": "1.0.0" - }, - "require-dev": { - "phpunit/phpunit": "4.0.*" - }, - "time": "2014-03-20T02:36:36+00:00", - "type": "library", - "installation-source": "dist", - "autoload": { - "psr-4": { - "Katzgrau\\KLogger\\": "src/" - }, - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dan Horrigan", - "email": "dan@dhorrigan.com", - "homepage": "http://dhorrigan.com", - "role": "Lead Developer" - }, - { - "name": "Kenny Katzgrau", - "email": "katzgrau@gmail.com" - } - ], - "description": "A Simple Logging Class", - "keywords": [ - "logging" - ] - }, - { - "name": "psr/log", - "version": "1.0.0", - "version_normalized": "1.0.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", - "shasum": "" - }, - "time": "2012-12-21T11:40:51+00:00", - "type": "library", - "installation-source": "dist", - "autoload": { - "psr-0": { - "Psr\\Log\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "keywords": [ - "log", - "psr", - "psr-3" - ] - }, - { - "name": "psr/simple-cache", - "version": "1.0.1", - "version_normalized": "1.0.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" +{ + "packages": [ + { + "name": "composer/semver", + "version": "3.2.4", + "version_normalized": "3.2.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.54", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "time": "2020-11-13T08:59:24+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.2.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "install-path": "./semver" }, - "time": "2017-10-23T01:57:42+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } + { + "name": "composer/xdebug-handler", + "version": "1.4.6", + "version_normalized": "1.4.6.0", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "f27e06cd9675801df441b3656569b328e04aa37c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c", + "reference": "f27e06cd9675801df441b3656569b328e04aa37c", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "time": "2021-03-25T17:01:18+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/1.4.6" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "install-path": "./xdebug-handler" }, - "installation-source": "dist", - "autoload": { - "psr-4": { - "Psr\\SimpleCache\\": "src/" - } + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.2.0", + "version_normalized": "3.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/06f0b06043c7438959dbdeed8bb3f699a19be22e", + "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0 || ^2.0", + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "time": "2021-01-10T17:48:47+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.0" + }, + "install-path": "../felixfbecker/advanced-json-rpc" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interfaces for simple caching", - "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" - ] - }, - { - "name": "voku/simple-cache", - "version": "4.0.1", - "version_normalized": "4.0.1.0", - "source": { - "type": "git", - "url": "https://github.com/voku/simple-cache.git", - "reference": "dfda1d803fd79d9ee918e1bac700c94a6f30659c" + { + "name": "katzgrau/klogger", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/katzgrau/klogger.git", + "reference": "46cdd92a9b4a8443120cc955bf831450cb274813" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/katzgrau/klogger/zipball/46cdd92a9b4a8443120cc955bf831450cb274813", + "reference": "46cdd92a9b4a8443120cc955bf831450cb274813", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "psr/log": "1.0.0" + }, + "require-dev": { + "phpunit/phpunit": "4.0.*" + }, + "time": "2014-03-20T02:36:36+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Katzgrau\\KLogger\\": "src/" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dan Horrigan", + "email": "dan@dhorrigan.com", + "homepage": "http://dhorrigan.com", + "role": "Lead Developer" + }, + { + "name": "Kenny Katzgrau", + "email": "katzgrau@gmail.com" + } + ], + "description": "A Simple Logging Class", + "keywords": [ + "logging" + ], + "install-path": "../katzgrau/klogger" }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/voku/simple-cache/zipball/dfda1d803fd79d9ee918e1bac700c94a6f30659c", - "reference": "dfda1d803fd79d9ee918e1bac700c94a6f30659c", - "shasum": "" + { + "name": "laminas/laminas-db", + "version": "2.12.0", + "version_normalized": "2.12.0.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-db.git", + "reference": "80cbba4e749f9eb7d8036172acb9ad41e8b6923f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-db/zipball/80cbba4e749f9eb7d8036172acb9ad41e8b6923f", + "reference": "80cbba4e749f9eb7d8036172acb9ad41e8b6923f", + "shasum": "" + }, + "require": { + "laminas/laminas-stdlib": "^3.3", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ~8.0.0" + }, + "replace": { + "zendframework/zend-db": "^2.11.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-eventmanager": "^3.3", + "laminas/laminas-hydrator": "^3.2 || ^4.0", + "laminas/laminas-servicemanager": "^3.3", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "laminas/laminas-eventmanager": "Laminas\\EventManager component", + "laminas/laminas-hydrator": "(^3.2 || ^4.0) Laminas\\Hydrator component for using HydratingResultSets", + "laminas/laminas-servicemanager": "Laminas\\ServiceManager component" + }, + "time": "2021-02-22T22:27:56+00:00", + "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\Db", + "config-provider": "Laminas\\Db\\ConfigProvider" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Laminas\\Db\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Database abstraction layer, SQL abstraction, result set abstraction, and RowDataGateway and TableDataGateway implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "db", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-db/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-db/issues", + "rss": "https://github.com/laminas/laminas-db/releases.atom", + "source": "https://github.com/laminas/laminas-db" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "install-path": "../laminas/laminas-db" }, - "require": { - "php": ">=7.0.0", - "psr/simple-cache": "~1.0" + { + "name": "laminas/laminas-stdlib", + "version": "3.3.1", + "version_normalized": "3.3.1.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-stdlib.git", + "reference": "d81c7ffe602ed0e6ecb18691019111c0f4bf1efe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/d81c7ffe602ed0e6ecb18691019111c0f4bf1efe", + "reference": "d81c7ffe602ed0e6ecb18691019111c0f4bf1efe", + "shasum": "" + }, + "require": { + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ^8.0" + }, + "replace": { + "zendframework/zend-stdlib": "^3.2.1" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "phpbench/phpbench": "^0.17.1", + "phpunit/phpunit": "~9.3.7" + }, + "time": "2020-11-19T20:18:59+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Laminas\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "SPL extensions, array utilities, error handlers, and more", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "stdlib" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-stdlib/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-stdlib/issues", + "rss": "https://github.com/laminas/laminas-stdlib/releases.atom", + "source": "https://github.com/laminas/laminas-stdlib" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "install-path": "../laminas/laminas-stdlib" }, - "provide": { - "psr/simple-cache-implementation": "1.0" + { + "name": "laminas/laminas-zendframework-bridge", + "version": "1.2.0", + "version_normalized": "1.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-zendframework-bridge.git", + "reference": "6cccbddfcfc742eb02158d6137ca5687d92cee32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6cccbddfcfc742eb02158d6137ca5687d92cee32", + "reference": "6cccbddfcfc742eb02158d6137ca5687d92cee32", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", + "psalm/plugin-phpunit": "^0.15.1", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.6" + }, + "time": "2021-02-25T21:54:58+00:00", + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\ZendFrameworkBridge" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ZendFrameworkBridge\\": "src//" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "keywords": [ + "ZendFramework", + "autoloading", + "laminas", + "zf" + ], + "support": { + "forum": "https://discourse.laminas.dev/", + "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues", + "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom", + "source": "https://github.com/laminas/laminas-zendframework-bridge" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "install-path": "../laminas/laminas-zendframework-bridge" }, - "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0" + { + "name": "microsoft/tolerant-php-parser", + "version": "v0.0.23", + "version_normalized": "0.0.23.0", + "source": { + "type": "git", + "url": "https://github.com/microsoft/tolerant-php-parser.git", + "reference": "1d76657e3271754515ace52501d3e427eca42ad0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/microsoft/tolerant-php-parser/zipball/1d76657e3271754515ace52501d3e427eca42ad0", + "reference": "1d76657e3271754515ace52501d3e427eca42ad0", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "time": "2020-09-13T17:29:12+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Microsoft\\PhpParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Lourens", + "email": "roblou@microsoft.com" + } + ], + "description": "Tolerant PHP-to-AST parser designed for IDE usage scenarios", + "support": { + "issues": "https://github.com/microsoft/tolerant-php-parser/issues", + "source": "https://github.com/microsoft/tolerant-php-parser/tree/v0.0.23" + }, + "install-path": "../microsoft/tolerant-php-parser" }, - "time": "2019-03-03T10:23:55+00:00", - "type": "library", - "installation-source": "dist", - "autoload": { - "psr-4": { - "voku\\cache\\": "src/voku/cache/" - } + { + "name": "netresearch/jsonmapper", + "version": "v2.1.0", + "version_normalized": "2.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/e0f1e33a71587aca81be5cffbb9746510e1fe04e", + "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "time": "2020-04-16T18:48:43+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/master" + }, + "install-path": "../netresearch/jsonmapper" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Lars Moelleken", - "homepage": "http://www.moelleken.org/", - "role": "Developer" - } - ], - "description": "Simple Cache library", - "homepage": "https://github.com/voku/simple-cache", - "keywords": [ - "cache", - "caching", - "php", - "simple cache" - ] - }, - { - "name": "zendframework/zend-db", - "version": "2.10.0", - "version_normalized": "2.10.0.0", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-db.git", - "reference": "320f5faaa0f98ebc93be5476ec4eda28255935c4" + { + "name": "phan/phan", + "version": "3.2.10", + "version_normalized": "3.2.10.0", + "source": { + "type": "git", + "url": "https://github.com/phan/phan.git", + "reference": "08978125063189a3e43448c99d50afd3b216234c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phan/phan/zipball/08978125063189a3e43448c99d50afd3b216234c", + "reference": "08978125063189a3e43448c99d50afd3b216234c", + "shasum": "" + }, + "require": { + "composer/semver": "^1.4|^2.0|^3.0", + "composer/xdebug-handler": "^1.3.2", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "felixfbecker/advanced-json-rpc": "^3.0.4", + "microsoft/tolerant-php-parser": "0.0.23", + "netresearch/jsonmapper": "^1.6.0|^2.0|^3.0", + "php": "^7.2.0|^8.0.0", + "sabre/event": "^5.0.3", + "symfony/console": "^3.2|^4.0|^5.0", + "symfony/polyfill-mbstring": "^1.11.0", + "symfony/polyfill-php80": "^1.20.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.0" + }, + "suggest": { + "ext-ast": "Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed, 1.0.8+ is recommended.", + "ext-iconv": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", + "ext-igbinary": "Improves performance of polyfill when ext-ast is unavailable", + "ext-mbstring": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", + "ext-tokenizer": "Needed for fallback/polyfill parser support and file/line-based suppressions." + }, + "time": "2020-12-31T20:11:44+00:00", + "bin": [ + "phan", + "phan_client", + "tocheckstyle" + ], + "type": "project", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Phan\\": "src/Phan" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tyson Andre" + }, + { + "name": "Rasmus Lerdorf" + }, + { + "name": "Andrew S. Morrison" + } + ], + "description": "A static analyzer for PHP", + "keywords": [ + "analyzer", + "php", + "static" + ], + "support": { + "issues": "https://github.com/phan/phan/issues", + "source": "https://github.com/phan/phan/tree/3.2.10" + }, + "install-path": "../phan/phan" }, - "dist": { - "type": "zip", - "url": "https://packages.zendframework.com/composer/zendframework-zend-db-2.10.0-5a43dc.zip", - "reference": "320f5faaa0f98ebc93be5476ec4eda28255935c4", - "shasum": "3f285fe0d475cac25e350787779019caa6ddb188" + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "version_normalized": "2.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "time": "2020-06-27T09:03:43+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "install-path": "../phpdocumentor/reflection-common" }, - "require": { - "php": "^5.6 || ^7.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.2", + "version_normalized": "5.2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" + }, + "time": "2020-09-03T19:13:55+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + }, + "install-path": "../phpdocumentor/reflection-docblock" }, - "require-dev": { - "phpunit/phpunit": "^5.7.25 || ^6.4.4", - "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-hydrator": "^1.1 || ^2.1 || ^3.0", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" + { + "name": "phpdocumentor/type-resolver", + "version": "1.4.0", + "version_normalized": "1.4.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "time": "2020-09-17T18:55:26+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + }, + "install-path": "../phpdocumentor/type-resolver" }, - "suggest": { - "zendframework/zend-eventmanager": "Zend\\EventManager component", - "zendframework/zend-hydrator": "Zend\\Hydrator component for using HydratingResultSets", - "zendframework/zend-servicemanager": "Zend\\ServiceManager component" + { + "name": "psr/container", + "version": "1.1.1", + "version_normalized": "1.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "time": "2021-03-05T17:36:06+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "install-path": "../psr/container" }, - "time": "2019-02-25T11:37:45+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.9-dev", - "dev-develop": "2.10-dev" - }, - "zf": { - "component": "Zend\\Db", - "config-provider": "Zend\\Db\\ConfigProvider" - } + { + "name": "psr/log", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "shasum": "" + }, + "time": "2012-12-21T11:40:51+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Psr\\Log\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "install-path": "../psr/log" }, - "installation-source": "dist", - "autoload": { - "psr-4": { - "Zend\\Db\\": "src/" - } + { + "name": "psr/simple-cache", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2017-10-23T01:57:42+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "install-path": "../psr/simple-cache" }, - "autoload-dev": { - "files": [ - "test/autoload.php" - ], - "psr-4": { - "ZendTest\\Db\\": "test/unit", - "ZendIntegrationTest\\Db\\": "test/integration" - } + { + "name": "sabre/event", + "version": "5.1.2", + "version_normalized": "5.1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "c120bec57c17b6251a496efc82b732418b49d50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/c120bec57c17b6251a496efc82b732418b49d50a", + "reference": "c120bec57c17b6251a496efc82b732418b49d50a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "time": "2020-10-03T11:02:22+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Sabre\\Event\\": "lib/" + }, + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "install-path": "../sabre/event" }, - "scripts": { - "check": [ - "@cs-check", - "@test" + { + "name": "symfony/console", + "version": "v5.2.6", + "version_normalized": "5.2.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d", + "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2", + "symfony/string": "^5.1" + }, + "conflict": { + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0", + "symfony/var-dumper": "^4.4|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "time": "2021-03-28T09:42:18+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], - "cs-check": [ - "phpcs" + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } ], - "cs-fix": [ - "phpcbf" + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" ], - "test": [ - "phpunit --colors=always --testsuite \"unit test\"" + "support": { + "source": "https://github.com/symfony/console/tree/v5.2.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "test-coverage": [ - "phpunit --colors=always --coverage-clover clover.xml" + "install-path": "../symfony/console" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.22.1", + "version_normalized": "1.22.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "time": "2021-01-07T16:49:33+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } ], - "test-integration": [ - "phpunit --colors=always --testsuite \"integration test\"" + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "upload-coverage": [ - "coveralls -v" - ] + "install-path": "../symfony/polyfill-ctype" }, - "license": [ - "BSD-3-Clause" - ], - "description": "Database abstraction layer, SQL abstraction, result set abstraction, and RowDataGateway and TableDataGateway implementations", - "keywords": [ - "db", - "zendframework", - "zf" - ], - "support": { - "docs": "https://docs.zendframework.com/zend-db/", - "issues": "https://github.com/zendframework/zend-db/issues", - "source": "https://github.com/zendframework/zend-db", - "rss": "https://github.com/zendframework/zend-db/releases.atom", - "slack": "https://zendframework-slack.herokuapp.com", - "forum": "https://discourse.zendframework.com/c/questions/components" - } - }, - { - "name": "zendframework/zend-stdlib", - "version": "3.2.1", - "version_normalized": "3.2.1.0", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-stdlib.git", - "reference": "04e09d7f961b2271b0e1cbbb6f5f53ad23cf8562" + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.22.1", + "version_normalized": "1.22.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170", + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2021-01-22T09:19:47+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-grapheme" }, - "dist": { - "type": "zip", - "url": "https://packages.zendframework.com/composer/zendframework-zend-stdlib-3.2.1-90faf1.zip", - "reference": "04e09d7f961b2271b0e1cbbb6f5f53ad23cf8562", - "shasum": "36f654611fab0d6fe1ea1c72184a43f9810ce7c8" + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.22.1", + "version_normalized": "1.22.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2021-01-22T09:19:47+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-normalizer" }, - "require": { - "php": "^5.6 || ^7.0" + { + "name": "symfony/polyfill-mbstring", + "version": "v1.22.1", + "version_normalized": "1.22.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "time": "2021-01-22T09:19:47+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-mbstring" }, - "require-dev": { - "phpbench/phpbench": "^0.13", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", - "zendframework/zend-coding-standard": "~1.0.0" + { + "name": "symfony/polyfill-php73", + "version": "v1.22.1", + "version_normalized": "1.22.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "time": "2021-01-07T16:49:33+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php73" }, - "time": "2018-08-28T21:34:05+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2.x-dev", - "dev-develop": "3.3.x-dev" - } + { + "name": "symfony/polyfill-php80", + "version": "v1.22.1", + "version_normalized": "1.22.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "time": "2021-01-07T16:49:33+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php80" }, - "installation-source": "dist", - "autoload": { - "psr-4": { - "Zend\\Stdlib\\": "src/" - } + { + "name": "symfony/service-contracts", + "version": "v2.4.0", + "version_normalized": "2.4.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "time": "2021-04-01T10:43:52+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/service-contracts" }, - "autoload-dev": { - "psr-4": { - "ZendTest\\Stdlib\\": "test/", - "ZendBench\\Stdlib\\": "benchmark/" - } + { + "name": "symfony/string", + "version": "v5.2.6", + "version_normalized": "5.2.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" + }, + "time": "2021-03-17T17:12:15+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.2.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/string" }, - "scripts": { - "check": [ - "@cs-check", - "@test" + { + "name": "voku/simple-cache", + "version": "4.0.5", + "version_normalized": "4.0.5.0", + "source": { + "type": "git", + "url": "https://github.com/voku/simple-cache.git", + "reference": "416cf88902991f3bf6168b71c0683e6dabb3d5e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/simple-cache/zipball/416cf88902991f3bf6168b71c0683e6dabb3d5e1", + "reference": "416cf88902991f3bf6168b71c0683e6dabb3d5e1", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/simple-cache": "~1.0" + }, + "provide": { + "psr/simple-cache-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0" + }, + "suggest": { + "predis/predis": "~1.1", + "symfony/var-exporter": "~3.0 || ~4.0 || ~5.0" + }, + "time": "2020-03-15T21:00:57+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "voku\\cache\\": "src/voku/cache/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], - "cs-check": [ - "phpcs" + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "http://www.moelleken.org/", + "role": "Developer" + } ], - "cs-fix": [ - "phpcbf" + "description": "Simple Cache library", + "homepage": "https://github.com/voku/simple-cache", + "keywords": [ + "cache", + "caching", + "php", + "simple cache" ], - "test": [ - "phpunit --colors=always" + "support": { + "issues": "https://github.com/voku/simple-cache/issues", + "source": "https://github.com/voku/simple-cache/tree/master" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/simple-cache", + "type": "tidelift" + } ], - "test-coverage": [ - "phpunit --colors=always --coverage-clover clover.xml" - ] + "install-path": "../voku/simple-cache" }, - "license": [ - "BSD-3-Clause" - ], - "description": "SPL extensions, array utilities, error handlers, and more", - "keywords": [ - "stdlib", - "zendframework", - "zf" - ], - "support": { - "docs": "https://docs.zendframework.com/zend-stdlib/", - "issues": "https://github.com/zendframework/zend-stdlib/issues", - "source": "https://github.com/zendframework/zend-stdlib", - "rss": "https://github.com/zendframework/zend-stdlib/releases.atom", - "slack": "https://zendframework-slack.herokuapp.com", - "forum": "https://discourse.zendframework.com/c/questions/components" + { + "name": "webmozart/assert", + "version": "1.10.0", + "version_normalized": "1.10.0.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "time": "2021-03-09T10:59:23+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "install-path": "../webmozart/assert" } - } -] + ], + "dev": true, + "dev-package-names": [ + "composer/semver", + "composer/xdebug-handler", + "felixfbecker/advanced-json-rpc", + "microsoft/tolerant-php-parser", + "netresearch/jsonmapper", + "phan/phan", + "phpdocumentor/reflection-common", + "phpdocumentor/reflection-docblock", + "phpdocumentor/type-resolver", + "psr/container", + "sabre/event", + "symfony/console", + "symfony/polyfill-ctype", + "symfony/polyfill-intl-grapheme", + "symfony/polyfill-intl-normalizer", + "symfony/polyfill-mbstring", + "symfony/polyfill-php73", + "symfony/polyfill-php80", + "symfony/service-contracts", + "symfony/string", + "webmozart/assert" + ] +} diff --git a/bundled-libs/composer/installed.php b/bundled-libs/composer/installed.php new file mode 100644 index 000000000..97b861871 --- /dev/null +++ b/bundled-libs/composer/installed.php @@ -0,0 +1,304 @@ + + array ( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => + array ( + ), + 'reference' => '05f58f90d743fe9ade24f3fdfe9a934d0b87c6a1', + 'name' => '__root__', + ), + 'versions' => + array ( + '__root__' => + array ( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => + array ( + ), + 'reference' => '05f58f90d743fe9ade24f3fdfe9a934d0b87c6a1', + ), + 'composer/semver' => + array ( + 'pretty_version' => '3.2.4', + 'version' => '3.2.4.0', + 'aliases' => + array ( + ), + 'reference' => 'a02fdf930a3c1c3ed3a49b5f63859c0c20e10464', + ), + 'composer/xdebug-handler' => + array ( + 'pretty_version' => '1.4.6', + 'version' => '1.4.6.0', + 'aliases' => + array ( + ), + 'reference' => 'f27e06cd9675801df441b3656569b328e04aa37c', + ), + 'felixfbecker/advanced-json-rpc' => + array ( + 'pretty_version' => 'v3.2.0', + 'version' => '3.2.0.0', + 'aliases' => + array ( + ), + 'reference' => '06f0b06043c7438959dbdeed8bb3f699a19be22e', + ), + 'katzgrau/klogger' => + array ( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'aliases' => + array ( + ), + 'reference' => '46cdd92a9b4a8443120cc955bf831450cb274813', + ), + 'laminas/laminas-db' => + array ( + 'pretty_version' => '2.12.0', + 'version' => '2.12.0.0', + 'aliases' => + array ( + ), + 'reference' => '80cbba4e749f9eb7d8036172acb9ad41e8b6923f', + ), + 'laminas/laminas-stdlib' => + array ( + 'pretty_version' => '3.3.1', + 'version' => '3.3.1.0', + 'aliases' => + array ( + ), + 'reference' => 'd81c7ffe602ed0e6ecb18691019111c0f4bf1efe', + ), + 'laminas/laminas-zendframework-bridge' => + array ( + 'pretty_version' => '1.2.0', + 'version' => '1.2.0.0', + 'aliases' => + array ( + ), + 'reference' => '6cccbddfcfc742eb02158d6137ca5687d92cee32', + ), + 'microsoft/tolerant-php-parser' => + array ( + 'pretty_version' => 'v0.0.23', + 'version' => '0.0.23.0', + 'aliases' => + array ( + ), + 'reference' => '1d76657e3271754515ace52501d3e427eca42ad0', + ), + 'netresearch/jsonmapper' => + array ( + 'pretty_version' => 'v2.1.0', + 'version' => '2.1.0.0', + 'aliases' => + array ( + ), + 'reference' => 'e0f1e33a71587aca81be5cffbb9746510e1fe04e', + ), + 'phan/phan' => + array ( + 'pretty_version' => '3.2.10', + 'version' => '3.2.10.0', + 'aliases' => + array ( + ), + 'reference' => '08978125063189a3e43448c99d50afd3b216234c', + ), + 'phpdocumentor/reflection-common' => + array ( + 'pretty_version' => '2.2.0', + 'version' => '2.2.0.0', + 'aliases' => + array ( + ), + 'reference' => '1d01c49d4ed62f25aa84a747ad35d5a16924662b', + ), + 'phpdocumentor/reflection-docblock' => + array ( + 'pretty_version' => '5.2.2', + 'version' => '5.2.2.0', + 'aliases' => + array ( + ), + 'reference' => '069a785b2141f5bcf49f3e353548dc1cce6df556', + ), + 'phpdocumentor/type-resolver' => + array ( + 'pretty_version' => '1.4.0', + 'version' => '1.4.0.0', + 'aliases' => + array ( + ), + 'reference' => '6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0', + ), + 'psr/container' => + array ( + 'pretty_version' => '1.1.1', + 'version' => '1.1.1.0', + 'aliases' => + array ( + ), + 'reference' => '8622567409010282b7aeebe4bb841fe98b58dcaf', + ), + 'psr/log' => + array ( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'aliases' => + array ( + ), + 'reference' => 'fe0936ee26643249e916849d48e3a51d5f5e278b', + ), + 'psr/log-implementation' => + array ( + 'provided' => + array ( + 0 => '1.0', + ), + ), + 'psr/simple-cache' => + array ( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'aliases' => + array ( + ), + 'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b', + ), + 'psr/simple-cache-implementation' => + array ( + 'provided' => + array ( + 0 => '1.0', + ), + ), + 'sabre/event' => + array ( + 'pretty_version' => '5.1.2', + 'version' => '5.1.2.0', + 'aliases' => + array ( + ), + 'reference' => 'c120bec57c17b6251a496efc82b732418b49d50a', + ), + 'symfony/console' => + array ( + 'pretty_version' => 'v5.2.6', + 'version' => '5.2.6.0', + 'aliases' => + array ( + ), + 'reference' => '35f039df40a3b335ebf310f244cb242b3a83ac8d', + ), + 'symfony/polyfill-ctype' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => 'c6c942b1ac76c82448322025e084cadc56048b4e', + ), + 'symfony/polyfill-intl-grapheme' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => '5601e09b69f26c1828b13b6bb87cb07cddba3170', + ), + 'symfony/polyfill-intl-normalizer' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => '43a0283138253ed1d48d352ab6d0bdb3f809f248', + ), + 'symfony/polyfill-mbstring' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => '5232de97ee3b75b0360528dae24e73db49566ab1', + ), + 'symfony/polyfill-php73' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => 'a678b42e92f86eca04b7fa4c0f6f19d097fb69e2', + ), + 'symfony/polyfill-php80' => + array ( + 'pretty_version' => 'v1.22.1', + 'version' => '1.22.1.0', + 'aliases' => + array ( + ), + 'reference' => 'dc3063ba22c2a1fd2f45ed856374d79114998f91', + ), + 'symfony/service-contracts' => + array ( + 'pretty_version' => 'v2.4.0', + 'version' => '2.4.0.0', + 'aliases' => + array ( + ), + 'reference' => 'f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb', + ), + 'symfony/string' => + array ( + 'pretty_version' => 'v5.2.6', + 'version' => '5.2.6.0', + 'aliases' => + array ( + ), + 'reference' => 'ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572', + ), + 'voku/simple-cache' => + array ( + 'pretty_version' => '4.0.5', + 'version' => '4.0.5.0', + 'aliases' => + array ( + ), + 'reference' => '416cf88902991f3bf6168b71c0683e6dabb3d5e1', + ), + 'webmozart/assert' => + array ( + 'pretty_version' => '1.10.0', + 'version' => '1.10.0.0', + 'aliases' => + array ( + ), + 'reference' => '6964c76c7804814a842473e0c8fd15bab0f18e25', + ), + 'zendframework/zend-db' => + array ( + 'replaced' => + array ( + 0 => '^2.11.0', + ), + ), + 'zendframework/zend-stdlib' => + array ( + 'replaced' => + array ( + 0 => '^3.2.1', + ), + ), + ), +); diff --git a/bundled-libs/composer/platform_check.php b/bundled-libs/composer/platform_check.php new file mode 100644 index 000000000..92370c5a0 --- /dev/null +++ b/bundled-libs/composer/platform_check.php @@ -0,0 +1,26 @@ += 70300)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/bundled-libs/composer/semver/.github/workflows/continuous-integration.yml b/bundled-libs/composer/semver/.github/workflows/continuous-integration.yml new file mode 100644 index 000000000..fe333c717 --- /dev/null +++ b/bundled-libs/composer/semver/.github/workflows/continuous-integration.yml @@ -0,0 +1,67 @@ +name: "Continuous Integration" + +on: + - push + - pull_request + +env: + COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" + SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: "1" + +jobs: + tests: + name: "CI" + + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: + - "5.3" + - "5.4" + - "5.5" + - "5.6" + - "7.0" + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "8.0" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Choose PHPUnit version" + run: | + if [ "${{ matrix.php-version }}" = "7.4" ]; then + echo "SYMFONY_PHPUNIT_VERSION=7.5" >> $GITHUB_ENV; + elif [ "${{ matrix.php-version }}" = "8.0" ]; then + echo "SYMFONY_PHPUNIT_VERSION=9.4" >> $GITHUB_ENV; + fi + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: Get composer cache directory + id: composercache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: "Install latest dependencies" + run: | + # Remove PHPStan as it requires a newer PHP + composer remove phpstan/phpstan --dev --no-update + composer update ${{ env.COMPOSER_FLAGS }} + + - name: "Run tests" + run: "vendor/bin/simple-phpunit --verbose" diff --git a/bundled-libs/composer/semver/.github/workflows/lint.yml b/bundled-libs/composer/semver/.github/workflows/lint.yml new file mode 100644 index 000000000..f8722473f --- /dev/null +++ b/bundled-libs/composer/semver/.github/workflows/lint.yml @@ -0,0 +1,30 @@ +name: "PHP Lint" + +on: + - push + - pull_request + +jobs: + tests: + name: "Lint" + + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: + - "5.3" + - "7.4" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Lint PHP files" + run: "find src/ -type f -name '*.php' -print0 | xargs -0 -L1 -P4 -- php -l -f" diff --git a/bundled-libs/composer/semver/.github/workflows/phpstan.yml b/bundled-libs/composer/semver/.github/workflows/phpstan.yml new file mode 100644 index 000000000..b97e6bde9 --- /dev/null +++ b/bundled-libs/composer/semver/.github/workflows/phpstan.yml @@ -0,0 +1,50 @@ +name: "PHPStan" + +on: + - push + - pull_request + +env: + COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" + SYMFONY_PHPUNIT_VERSION: "" + +jobs: + tests: + name: "PHPStan" + + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: + - "7.4" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: Get composer cache directory + id: composercache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: "Install latest dependencies" + run: "composer update ${{ env.COMPOSER_FLAGS }}" + + - name: Run PHPStan + # Locked to phpunit 7.5 here as newer ones have void return types which break inheritance + run: | + composer require --dev phpunit/phpunit:^7.5.20 --with-all-dependencies ${{ env.COMPOSER_FLAGS }} + vendor/bin/phpstan analyse diff --git a/bundled-libs/composer/semver/CHANGELOG.md b/bundled-libs/composer/semver/CHANGELOG.md new file mode 100644 index 000000000..3483eea50 --- /dev/null +++ b/bundled-libs/composer/semver/CHANGELOG.md @@ -0,0 +1,161 @@ +# Change Log + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +### [3.2.4] 2020-11-13 + + * Fixed: code clean-up + +### [3.2.3] 2020-11-12 + + * Fixed: constraints in the form of `X || Y, >=Y.1` and other such complex constructs were in some cases being optimized into a more restrictive constraint + +### [3.2.2] 2020-10-14 + + * Fixed: internal code cleanups + +### [3.2.1] 2020-09-27 + + * Fixed: accidental validation of broken constraints combining ^/~ and wildcards, and -dev suffix allowing weird cases + * Fixed: normalization of beta0 and such which was dropping the 0 + +### [3.2.0] 2020-09-09 + + * Added: support for `x || @dev`, not very useful but seen in the wild and failed to validate with 1.5.2/1.6.0 + * Added: support for `foobar-dev` being equal to `dev-foobar`, dev-foobar is the official way to write it but we need to support the other for BC and convenience + +### [3.1.0] 2020-09-08 + + * Added: support for constraints like `^2.x-dev` and `~2.x-dev`, not very useful but seen in the wild and failed to validate with 3.0.1 + * Fixed: invalid aliases will no longer throw, unless explicitly validated by Composer in the root package + +### [3.0.1] 2020-09-08 + + * Fixed: handling of some invalid -dev versions which were seen as valid + +### [3.0.0] 2020-05-26 + + * Break: Renamed `EmptyConstraint`, replace it with `MatchAllConstraint` + * Break: Unlikely to affect anyone but strictly speaking a breaking change, `*.*` and such variants will not match all `dev-*` versions anymore, only `*` does + * Break: ConstraintInterface is now considered internal/private and not meant to be implemented by third parties anymore + * Added `Intervals` class to check if a constraint is a subsets of another one, and allow compacting complex MultiConstraints into simpler ones + * Added `CompilingMatcher` class to speed up constraint matching against simple Constraint instances + * Added `MatchAllConstraint` and `MatchNoneConstraint` which match everything and nothing + * Added more advanced optimization of contiguous constraints inside MultiConstraint + * Added tentative support for PHP 8 + * Fixed ConstraintInterface::matches to be commutative in all cases + +### [2.0.0] 2020-04-21 + + * Break: `dev-master`, `dev-trunk` and `dev-default` now normalize to `dev-master`, `dev-trunk` and `dev-default` instead of `9999999-dev` in 1.x + * Break: Removed the deprecated `AbstractConstraint` + * Added `getUpperBound` and `getLowerBound` to ConstraintInterface. They return `Composer\Semver\Constraint\Bound` instances + * Added `MultiConstraint::create` to create the most-optimal form of ConstraintInterface from an array of constraint strings + +### [1.7.1] 2020-09-27 + + * Fixed: accidental validation of broken constraints combining ^/~ and wildcards, and -dev suffix allowing weird cases + * Fixed: normalization of beta0 and such which was dropping the 0 + +### [1.7.0] 2020-09-09 + + * Added: support for `x || @dev`, not very useful but seen in the wild and failed to validate with 1.5.2/1.6.0 + * Added: support for `foobar-dev` being equal to `dev-foobar`, dev-foobar is the official way to write it but we need to support the other for BC and convenience + +### [1.6.0] 2020-09-08 + + * Added: support for constraints like `^2.x-dev` and `~2.x-dev`, not very useful but seen in the wild and failed to validate with 1.5.2 + * Fixed: invalid aliases will no longer throw, unless explicitly validated by Composer in the root package + +### [1.5.2] 2020-09-08 + + * Fixed: handling of some invalid -dev versions which were seen as valid + * Fixed: some doctypes + +### [1.5.1] 2020-01-13 + + * Fixed: Parsing of aliased version was not validating the alias to be a valid version + +### [1.5.0] 2019-03-19 + + * Added: some support for date versions (e.g. 201903) in `~` operator + * Fixed: support for stabilities in `~` operator was inconsistent + +### [1.4.2] 2016-08-30 + + * Fixed: collapsing of complex constraints lead to buggy constraints + +### [1.4.1] 2016-06-02 + + * Changed: branch-like requirements no longer strip build metadata - [composer/semver#38](https://github.com/composer/semver/pull/38). + +### [1.4.0] 2016-03-30 + + * Added: getters on MultiConstraint - [composer/semver#35](https://github.com/composer/semver/pull/35). + +### [1.3.0] 2016-02-25 + + * Fixed: stability parsing - [composer/composer#1234](https://github.com/composer/composer/issues/4889). + * Changed: collapse contiguous constraints when possible. + +### [1.2.0] 2015-11-10 + + * Changed: allow multiple numerical identifiers in 'pre-release' version part. + * Changed: add more 'v' prefix support. + +### [1.1.0] 2015-11-03 + + * Changed: dropped redundant `test` namespace. + * Changed: minor adjustment in datetime parsing normalization. + * Changed: `ConstraintInterface` relaxed, setPrettyString is not required anymore. + * Changed: `AbstractConstraint` marked deprecated, will be removed in 2.0. + * Changed: `Constraint` is now extensible. + +### [1.0.0] 2015-09-21 + + * Break: `VersionConstraint` renamed to `Constraint`. + * Break: `SpecificConstraint` renamed to `AbstractConstraint`. + * Break: `LinkConstraintInterface` renamed to `ConstraintInterface`. + * Break: `VersionParser::parseNameVersionPairs` was removed. + * Changed: `VersionParser::parseConstraints` allows (but ignores) build metadata now. + * Changed: `VersionParser::parseConstraints` allows (but ignores) prefixing numeric versions with a 'v' now. + * Changed: Fixed namespace(s) of test files. + * Changed: `Comparator::compare` no longer throws `InvalidArgumentException`. + * Changed: `Constraint` now throws `InvalidArgumentException`. + +### [0.1.0] 2015-07-23 + + * Added: `Composer\Semver\Comparator`, various methods to compare versions. + * Added: various documents such as README.md, LICENSE, etc. + * Added: configuration files for Git, Travis, php-cs-fixer, phpunit. + * Break: the following namespaces were renamed: + - Namespace: `Composer\Package\Version` -> `Composer\Semver` + - Namespace: `Composer\Package\LinkConstraint` -> `Composer\Semver\Constraint` + - Namespace: `Composer\Test\Package\Version` -> `Composer\Test\Semver` + - Namespace: `Composer\Test\Package\LinkConstraint` -> `Composer\Test\Semver\Constraint` + * Changed: code style using php-cs-fixer. + +[3.2.4]: https://github.com/composer/semver/compare/3.2.3...3.2.4 +[3.2.3]: https://github.com/composer/semver/compare/3.2.2...3.2.3 +[3.2.2]: https://github.com/composer/semver/compare/3.2.1...3.2.2 +[3.2.1]: https://github.com/composer/semver/compare/3.2.0...3.2.1 +[3.2.0]: https://github.com/composer/semver/compare/3.1.0...3.2.0 +[3.1.0]: https://github.com/composer/semver/compare/3.0.1...3.1.0 +[3.0.1]: https://github.com/composer/semver/compare/3.0.0...3.0.1 +[3.0.0]: https://github.com/composer/semver/compare/2.0.0...3.0.0 +[2.0.0]: https://github.com/composer/semver/compare/1.5.1...2.0.0 +[1.7.1]: https://github.com/composer/semver/compare/1.7.0...1.7.1 +[1.7.0]: https://github.com/composer/semver/compare/1.6.0...1.7.0 +[1.6.0]: https://github.com/composer/semver/compare/1.5.2...1.6.0 +[1.5.2]: https://github.com/composer/semver/compare/1.5.1...1.5.2 +[1.5.1]: https://github.com/composer/semver/compare/1.5.0...1.5.1 +[1.5.0]: https://github.com/composer/semver/compare/1.4.2...1.5.0 +[1.4.2]: https://github.com/composer/semver/compare/1.4.1...1.4.2 +[1.4.1]: https://github.com/composer/semver/compare/1.4.0...1.4.1 +[1.4.0]: https://github.com/composer/semver/compare/1.3.0...1.4.0 +[1.3.0]: https://github.com/composer/semver/compare/1.2.0...1.3.0 +[1.2.0]: https://github.com/composer/semver/compare/1.1.0...1.2.0 +[1.1.0]: https://github.com/composer/semver/compare/1.0.0...1.1.0 +[1.0.0]: https://github.com/composer/semver/compare/0.1.0...1.0.0 +[0.1.0]: https://github.com/composer/semver/compare/5e0b9a4da...0.1.0 diff --git a/bundled-libs/composer/semver/LICENSE b/bundled-libs/composer/semver/LICENSE new file mode 100644 index 000000000..466975862 --- /dev/null +++ b/bundled-libs/composer/semver/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015 Composer + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bundled-libs/composer/semver/README.md b/bundled-libs/composer/semver/README.md new file mode 100644 index 000000000..17824ebb4 --- /dev/null +++ b/bundled-libs/composer/semver/README.md @@ -0,0 +1,70 @@ +composer/semver +=============== + +Semver library that offers utilities, version constraint parsing and validation. + +Originally written as part of [composer/composer](https://github.com/composer/composer), +now extracted and made available as a stand-alone library. + +[![Continuous Integration](https://github.com/composer/semver/workflows/Continuous%20Integration/badge.svg?branch=main)](https://github.com/composer/semver/actions) + + +Installation +------------ + +Install the latest version with: + +```bash +$ composer require composer/semver +``` + + +Requirements +------------ + +* PHP 5.3.2 is required but using the latest version of PHP is highly recommended. + + +Version Comparison +------------------ + +For details on how versions are compared, refer to the [Versions](https://getcomposer.org/doc/articles/versions.md) +article in the documentation section of the [getcomposer.org](https://getcomposer.org) website. + + +Basic usage +----------- + +### Comparator + +The `Composer\Semver\Comparator` class provides the following methods for comparing versions: + +* greaterThan($v1, $v2) +* greaterThanOrEqualTo($v1, $v2) +* lessThan($v1, $v2) +* lessThanOrEqualTo($v1, $v2) +* equalTo($v1, $v2) +* notEqualTo($v1, $v2) + +Each function takes two version strings as arguments and returns a boolean. For example: + +```php +use Composer\Semver\Comparator; + +Comparator::greaterThan('1.25.0', '1.24.0'); // 1.25.0 > 1.24.0 +``` + +### Semver + +The `Composer\Semver\Semver` class provides the following methods: + +* satisfies($version, $constraints) +* satisfiedBy(array $versions, $constraint) +* sort($versions) +* rsort($versions) + + +License +------- + +composer/semver is licensed under the MIT License, see the LICENSE file for details. diff --git a/bundled-libs/composer/semver/composer.json b/bundled-libs/composer/semver/composer.json new file mode 100644 index 000000000..155106340 --- /dev/null +++ b/bundled-libs/composer/semver/composer.json @@ -0,0 +1,58 @@ +{ + "name": "composer/semver", + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "type": "library", + "license": "MIT", + "keywords": [ + "semver", + "semantic", + "versioning", + "validation" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.2 || ^5", + "phpstan/phpstan": "^0.12.54" + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Composer\\Semver\\": "tests" + } + }, + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "scripts": { + "test": "vendor/bin/simple-phpunit" + } +} diff --git a/bundled-libs/composer/semver/src/Comparator.php b/bundled-libs/composer/semver/src/Comparator.php new file mode 100644 index 000000000..d2b62c7f9 --- /dev/null +++ b/bundled-libs/composer/semver/src/Comparator.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver; + +use Composer\Semver\Constraint\Constraint; + +class Comparator +{ + /** + * Evaluates the expression: $version1 > $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function greaterThan($version1, $version2) + { + return self::compare($version1, '>', $version2); + } + + /** + * Evaluates the expression: $version1 >= $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function greaterThanOrEqualTo($version1, $version2) + { + return self::compare($version1, '>=', $version2); + } + + /** + * Evaluates the expression: $version1 < $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function lessThan($version1, $version2) + { + return self::compare($version1, '<', $version2); + } + + /** + * Evaluates the expression: $version1 <= $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function lessThanOrEqualTo($version1, $version2) + { + return self::compare($version1, '<=', $version2); + } + + /** + * Evaluates the expression: $version1 == $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function equalTo($version1, $version2) + { + return self::compare($version1, '==', $version2); + } + + /** + * Evaluates the expression: $version1 != $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function notEqualTo($version1, $version2) + { + return self::compare($version1, '!=', $version2); + } + + /** + * Evaluates the expression: $version1 $operator $version2. + * + * @param string $version1 + * @param string $operator + * @param string $version2 + * + * @return bool + */ + public static function compare($version1, $operator, $version2) + { + $constraint = new Constraint($operator, $version2); + + return $constraint->matchSpecific(new Constraint('==', $version1), true); + } +} diff --git a/bundled-libs/composer/semver/src/CompilingMatcher.php b/bundled-libs/composer/semver/src/CompilingMatcher.php new file mode 100644 index 000000000..63c79f41c --- /dev/null +++ b/bundled-libs/composer/semver/src/CompilingMatcher.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver; + +use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\ConstraintInterface; + +/** + * Helper class to evaluate constraint by compiling and reusing the code to evaluate + */ +class CompilingMatcher +{ + private static $compiledCheckerCache = array(); + private static $enabled; + + /** + * @phpstan-var array + */ + private static $transOpInt = array( + Constraint::OP_EQ => '==', + Constraint::OP_LT => '<', + Constraint::OP_LE => '<=', + Constraint::OP_GT => '>', + Constraint::OP_GE => '>=', + Constraint::OP_NE => '!=', + ); + + /** + * Evaluates the expression: $constraint match $operator $version + * + * @param ConstraintInterface $constraint + * @param int $operator + * @phpstan-param Constraint::OP_* $operator + * @param string $version + * + * @return mixed + */ + public static function match(ConstraintInterface $constraint, $operator, $version) + { + if (self::$enabled === null) { + self::$enabled = !\in_array('eval', explode(',', ini_get('disable_functions')), true); + } + if (!self::$enabled) { + return $constraint->matches(new Constraint(self::$transOpInt[$operator], $version)); + } + + $cacheKey = $operator.$constraint; + if (!isset(self::$compiledCheckerCache[$cacheKey])) { + $code = $constraint->compile($operator); + self::$compiledCheckerCache[$cacheKey] = $function = eval('return function($v, $b){return '.$code.';};'); + } else { + $function = self::$compiledCheckerCache[$cacheKey]; + } + + return $function($version, strpos($version, 'dev-') === 0); + } +} diff --git a/bundled-libs/composer/semver/src/Constraint/Bound.php b/bundled-libs/composer/semver/src/Constraint/Bound.php new file mode 100644 index 000000000..e9b9417b8 --- /dev/null +++ b/bundled-libs/composer/semver/src/Constraint/Bound.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +class Bound +{ + /** + * @var string + */ + private $version; + + /** + * @var bool + */ + private $isInclusive; + + /** + * @param string $version + * @param bool $isInclusive + */ + public function __construct($version, $isInclusive) + { + $this->version = $version; + $this->isInclusive = $isInclusive; + } + + /** + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * @return bool + */ + public function isInclusive() + { + return $this->isInclusive; + } + + public function isZero() + { + return $this->getVersion() === '0.0.0.0-dev' && $this->isInclusive(); + } + + public function isPositiveInfinity() + { + return $this->getVersion() === PHP_INT_MAX.'.0.0.0' && !$this->isInclusive(); + } + + /** + * Compares a bound to another with a given operator. + * + * @param Bound $other + * @param string $operator + * + * @return bool + */ + public function compareTo(Bound $other, $operator) + { + if (!\in_array($operator, array('<', '>'), true)) { + throw new \InvalidArgumentException('Does not support any other operator other than > or <.'); + } + + // If they are the same it doesn't matter + if ($this == $other) { + return false; + } + + $compareResult = version_compare($this->getVersion(), $other->getVersion()); + + // Not the same version means we don't need to check if the bounds are inclusive or not + if (0 !== $compareResult) { + return (('>' === $operator) ? 1 : -1) === $compareResult; + } + + // Question we're answering here is "am I higher than $other?" + return '>' === $operator ? $other->isInclusive() : !$other->isInclusive(); + } + + public function __toString() + { + return sprintf( + '%s [%s]', + $this->getVersion(), + $this->isInclusive() ? 'inclusive' : 'exclusive' + ); + } + + /** + * @return self + */ + public static function zero() + { + return new Bound('0.0.0.0-dev', true); + } + + /** + * @return self + */ + public static function positiveInfinity() + { + return new Bound(PHP_INT_MAX.'.0.0.0', false); + } +} diff --git a/bundled-libs/composer/semver/src/Constraint/Constraint.php b/bundled-libs/composer/semver/src/Constraint/Constraint.php new file mode 100644 index 000000000..c60d0b9e4 --- /dev/null +++ b/bundled-libs/composer/semver/src/Constraint/Constraint.php @@ -0,0 +1,404 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +/** + * Defines a constraint. + */ +class Constraint implements ConstraintInterface +{ + /* operator integer values */ + const OP_EQ = 0; + const OP_LT = 1; + const OP_LE = 2; + const OP_GT = 3; + const OP_GE = 4; + const OP_NE = 5; + + /** + * Operator to integer translation table. + * + * @var array + * @phpstan-var array + */ + private static $transOpStr = array( + '=' => self::OP_EQ, + '==' => self::OP_EQ, + '<' => self::OP_LT, + '<=' => self::OP_LE, + '>' => self::OP_GT, + '>=' => self::OP_GE, + '<>' => self::OP_NE, + '!=' => self::OP_NE, + ); + + /** + * Integer to operator translation table. + * + * @var array + * @phpstan-var array + */ + private static $transOpInt = array( + self::OP_EQ => '==', + self::OP_LT => '<', + self::OP_LE => '<=', + self::OP_GT => '>', + self::OP_GE => '>=', + self::OP_NE => '!=', + ); + + /** + * @var int + * @phpstan-var self::OP_* + */ + protected $operator; + + /** @var string */ + protected $version; + + /** @var string|null */ + protected $prettyString; + + /** @var Bound */ + protected $lowerBound; + + /** @var Bound */ + protected $upperBound; + + /** + * Sets operator and version to compare with. + * + * @param string $operator + * @param string $version + * + * @throws \InvalidArgumentException if invalid operator is given. + */ + public function __construct($operator, $version) + { + if (!isset(self::$transOpStr[$operator])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid operator "%s" given, expected one of: %s', + $operator, + implode(', ', self::getSupportedOperators()) + )); + } + + $this->operator = self::$transOpStr[$operator]; + $this->version = $version; + } + + public function getVersion() + { + return $this->version; + } + + public function getOperator() + { + return self::$transOpInt[$this->operator]; + } + + /** + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider) + { + if ($provider instanceof self) { + return $this->matchSpecific($provider); + } + + // turn matching around to find a match + return $provider->matches($this); + } + + /** + * @param string|null $prettyString + */ + public function setPrettyString($prettyString) + { + $this->prettyString = $prettyString; + } + + /** + * @return string + */ + public function getPrettyString() + { + if ($this->prettyString) { + return $this->prettyString; + } + + return $this->__toString(); + } + + /** + * Get all supported comparison operators. + * + * @return array + */ + public static function getSupportedOperators() + { + return array_keys(self::$transOpStr); + } + + /** + * @param string $operator + * @return int + * + * @phpstan-return self::OP_* + */ + public static function getOperatorConstant($operator) + { + return self::$transOpStr[$operator]; + } + + /** + * @param string $a + * @param string $b + * @param string $operator + * @param bool $compareBranches + * + * @throws \InvalidArgumentException if invalid operator is given. + * + * @return bool + */ + public function versionCompare($a, $b, $operator, $compareBranches = false) + { + if (!isset(self::$transOpStr[$operator])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid operator "%s" given, expected one of: %s', + $operator, + implode(', ', self::getSupportedOperators()) + )); + } + + $aIsBranch = strpos($a, 'dev-') === 0; + $bIsBranch = strpos($b, 'dev-') === 0; + + if ($operator === '!=' && ($aIsBranch || $bIsBranch)) { + return $a !== $b; + } + + if ($aIsBranch && $bIsBranch) { + return $operator === '==' && $a === $b; + } + + // when branches are not comparable, we make sure dev branches never match anything + if (!$compareBranches && ($aIsBranch || $bIsBranch)) { + return false; + } + + return \version_compare($a, $b, $operator); + } + + public function compile($otherOperator) + { + if (strpos($this->version, 'dev-') === 0) { + if (self::OP_EQ === $this->operator) { + if (self::OP_EQ === $otherOperator) { + return sprintf('$b && $v === %s', \var_export($this->version, true)); + } + if (self::OP_NE === $otherOperator) { + return sprintf('!$b || $v !== %s', \var_export($this->version, true)); + } + return 'false'; + } + + if (self::OP_NE === $this->operator) { + if (self::OP_EQ === $otherOperator) { + return sprintf('!$b || $v !== %s', \var_export($this->version, true)); + } + if (self::OP_NE === $otherOperator) { + return 'true'; + } + return '!$b'; + } + + return 'false'; + } + + if (self::OP_EQ === $this->operator) { + if (self::OP_EQ === $otherOperator) { + return sprintf('\version_compare($v, %s, \'==\')', \var_export($this->version, true)); + } + if (self::OP_NE === $otherOperator) { + return sprintf('$b || \version_compare($v, %s, \'!=\')', \var_export($this->version, true)); + } + + return sprintf('!$b && \version_compare(%s, $v, \'%s\')', \var_export($this->version, true), self::$transOpInt[$otherOperator]); + } + + if (self::OP_NE === $this->operator) { + if (self::OP_EQ === $otherOperator) { + return sprintf('$b || (!$b && \version_compare($v, %s, \'!=\'))', \var_export($this->version, true)); + } + + if (self::OP_NE === $otherOperator) { + return 'true'; + } + return '!$b'; + } + + if (self::OP_LT === $this->operator || self::OP_LE === $this->operator) { + if (self::OP_LT === $otherOperator || self::OP_LE === $otherOperator) { + return '!$b'; + } + } elseif (self::OP_GT === $this->operator || self::OP_GE === $this->operator) { + if (self::OP_GT === $otherOperator || self::OP_GE === $otherOperator) { + return '!$b'; + } + } + + if (self::OP_NE === $otherOperator) { + return 'true'; + } + + $codeComparison = sprintf('\version_compare($v, %s, \'%s\')', \var_export($this->version, true), self::$transOpInt[$this->operator]); + if ($this->operator === self::OP_LE) { + if ($otherOperator === self::OP_GT) { + return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison; + } + } elseif ($this->operator === self::OP_GE) { + if ($otherOperator === self::OP_LT) { + return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison; + } + } + + return sprintf('!$b && %s', $codeComparison); + } + + /** + * @param Constraint $provider + * @param bool $compareBranches + * + * @return bool + */ + public function matchSpecific(Constraint $provider, $compareBranches = false) + { + $noEqualOp = str_replace('=', '', self::$transOpInt[$this->operator]); + $providerNoEqualOp = str_replace('=', '', self::$transOpInt[$provider->operator]); + + $isEqualOp = self::OP_EQ === $this->operator; + $isNonEqualOp = self::OP_NE === $this->operator; + $isProviderEqualOp = self::OP_EQ === $provider->operator; + $isProviderNonEqualOp = self::OP_NE === $provider->operator; + + // '!=' operator is match when other operator is not '==' operator or version is not match + // these kinds of comparisons always have a solution + if ($isNonEqualOp || $isProviderNonEqualOp) { + if ($isNonEqualOp && !$isProviderNonEqualOp && !$isProviderEqualOp && strpos($provider->version, 'dev-') === 0) { + return false; + } + + if ($isProviderNonEqualOp && !$isNonEqualOp && !$isEqualOp && strpos($this->version, 'dev-') === 0) { + return false; + } + + if (!$isEqualOp && !$isProviderEqualOp) { + return true; + } + return $this->versionCompare($provider->version, $this->version, '!=', $compareBranches); + } + + // an example for the condition is <= 2.0 & < 1.0 + // these kinds of comparisons always have a solution + if ($this->operator !== self::OP_EQ && $noEqualOp === $providerNoEqualOp) { + return !(strpos($this->version, 'dev-') === 0 || strpos($provider->version, 'dev-') === 0); + } + + $version1 = $isEqualOp ? $this->version : $provider->version; + $version2 = $isEqualOp ? $provider->version : $this->version; + $operator = $isEqualOp ? $provider->operator : $this->operator; + + if ($this->versionCompare($version1, $version2, self::$transOpInt[$operator], $compareBranches)) { + // special case, e.g. require >= 1.0 and provide < 1.0 + // 1.0 >= 1.0 but 1.0 is outside of the provided interval + + return !(self::$transOpInt[$provider->operator] === $providerNoEqualOp + && self::$transOpInt[$this->operator] !== $noEqualOp + && \version_compare($provider->version, $this->version, '==')); + } + + return false; + } + + /** + * @return string + */ + public function __toString() + { + return self::$transOpInt[$this->operator] . ' ' . $this->version; + } + + /** + * {@inheritDoc} + */ + public function getLowerBound() + { + $this->extractBounds(); + + return $this->lowerBound; + } + + /** + * {@inheritDoc} + */ + public function getUpperBound() + { + $this->extractBounds(); + + return $this->upperBound; + } + + private function extractBounds() + { + if (null !== $this->lowerBound) { + return; + } + + // Branches + if (strpos($this->version, 'dev-') === 0) { + $this->lowerBound = Bound::zero(); + $this->upperBound = Bound::positiveInfinity(); + + return; + } + + switch ($this->operator) { + case self::OP_EQ: + $this->lowerBound = new Bound($this->version, true); + $this->upperBound = new Bound($this->version, true); + break; + case self::OP_LT: + $this->lowerBound = Bound::zero(); + $this->upperBound = new Bound($this->version, false); + break; + case self::OP_LE: + $this->lowerBound = Bound::zero(); + $this->upperBound = new Bound($this->version, true); + break; + case self::OP_GT: + $this->lowerBound = new Bound($this->version, false); + $this->upperBound = Bound::positiveInfinity(); + break; + case self::OP_GE: + $this->lowerBound = new Bound($this->version, true); + $this->upperBound = Bound::positiveInfinity(); + break; + case self::OP_NE: + $this->lowerBound = Bound::zero(); + $this->upperBound = Bound::positiveInfinity(); + break; + } + } +} diff --git a/bundled-libs/composer/semver/src/Constraint/ConstraintInterface.php b/bundled-libs/composer/semver/src/Constraint/ConstraintInterface.php new file mode 100644 index 000000000..89c12682e --- /dev/null +++ b/bundled-libs/composer/semver/src/Constraint/ConstraintInterface.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +/** + * DO NOT IMPLEMENT this interface. It is only meant for usage as a type hint + * in libraries relying on composer/semver but creating your own constraint class + * that implements this interface is not a supported use case and will cause the + * composer/semver components to return unexpected results. + */ +interface ConstraintInterface +{ + /** + * Checks whether the given constraint intersects in any way with this constraint + * + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider); + + /** + * Provides a compiled version of the constraint for the given operator + * The compiled version must be a PHP expression. + * Executor of compile version must provide 2 variables: + * - $v = the string version to compare with + * - $b = whether or not the version is a non-comparable branch (starts with "dev-") + * + * @see Constraint::OP_* for the list of available operators. + * @example return '!$b && version_compare($v, '1.0', '>')'; + * + * @param int $operator one Constraint::OP_* + * + * @return string + */ + public function compile($operator); + + /** + * @return Bound + */ + public function getUpperBound(); + + /** + * @return Bound + */ + public function getLowerBound(); + + /** + * @return string + */ + public function getPrettyString(); + + /** + * @param string|null $prettyString + */ + public function setPrettyString($prettyString); + + /** + * @return string + */ + public function __toString(); +} diff --git a/bundled-libs/composer/semver/src/Constraint/MatchAllConstraint.php b/bundled-libs/composer/semver/src/Constraint/MatchAllConstraint.php new file mode 100644 index 000000000..fcd297058 --- /dev/null +++ b/bundled-libs/composer/semver/src/Constraint/MatchAllConstraint.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +/** + * Defines the absence of a constraint. + * + * This constraint matches everything. + */ +class MatchAllConstraint implements ConstraintInterface +{ + /** @var string|null */ + protected $prettyString; + + /** + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider) + { + return true; + } + + public function compile($operator) + { + return 'true'; + } + + /** + * @param string|null $prettyString + */ + public function setPrettyString($prettyString) + { + $this->prettyString = $prettyString; + } + + /** + * @return string + */ + public function getPrettyString() + { + if ($this->prettyString) { + return $this->prettyString; + } + + return (string) $this; + } + + /** + * @return string + */ + public function __toString() + { + return '*'; + } + + /** + * {@inheritDoc} + */ + public function getUpperBound() + { + return Bound::positiveInfinity(); + } + + /** + * {@inheritDoc} + */ + public function getLowerBound() + { + return Bound::zero(); + } +} diff --git a/bundled-libs/composer/semver/src/Constraint/MatchNoneConstraint.php b/bundled-libs/composer/semver/src/Constraint/MatchNoneConstraint.php new file mode 100644 index 000000000..47f7f56cd --- /dev/null +++ b/bundled-libs/composer/semver/src/Constraint/MatchNoneConstraint.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +/** + * Blackhole of constraints, nothing escapes it + */ +class MatchNoneConstraint implements ConstraintInterface +{ + /** @var string|null */ + protected $prettyString; + + /** + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider) + { + return false; + } + + public function compile($operator) + { + return 'false'; + } + + /** + * @param string|null $prettyString + */ + public function setPrettyString($prettyString) + { + $this->prettyString = $prettyString; + } + + /** + * @return string + */ + public function getPrettyString() + { + if ($this->prettyString) { + return $this->prettyString; + } + + return (string) $this; + } + + /** + * @return string + */ + public function __toString() + { + return '[]'; + } + + /** + * {@inheritDoc} + */ + public function getUpperBound() + { + return new Bound('0.0.0.0-dev', false); + } + + /** + * {@inheritDoc} + */ + public function getLowerBound() + { + return new Bound('0.0.0.0-dev', false); + } +} diff --git a/bundled-libs/composer/semver/src/Constraint/MultiConstraint.php b/bundled-libs/composer/semver/src/Constraint/MultiConstraint.php new file mode 100644 index 000000000..da27ffcd4 --- /dev/null +++ b/bundled-libs/composer/semver/src/Constraint/MultiConstraint.php @@ -0,0 +1,297 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +/** + * Defines a conjunctive or disjunctive set of constraints. + */ +class MultiConstraint implements ConstraintInterface +{ + /** @var ConstraintInterface[] */ + protected $constraints; + + /** @var string|null */ + protected $prettyString; + + /** @var string|null */ + protected $string; + + /** @var bool */ + protected $conjunctive; + + /** @var Bound|null */ + protected $lowerBound; + + /** @var Bound|null */ + protected $upperBound; + + /** + * @param ConstraintInterface[] $constraints A set of constraints + * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive + * + * @throws \InvalidArgumentException If less than 2 constraints are passed + */ + public function __construct(array $constraints, $conjunctive = true) + { + if (\count($constraints) < 2) { + throw new \InvalidArgumentException( + 'Must provide at least two constraints for a MultiConstraint. Use '. + 'the regular Constraint class for one constraint only or MatchAllConstraint for none. You may use '. + 'MultiConstraint::create() which optimizes and handles those cases automatically.' + ); + } + + $this->constraints = $constraints; + $this->conjunctive = $conjunctive; + } + + /** + * @return ConstraintInterface[] + */ + public function getConstraints() + { + return $this->constraints; + } + + /** + * @return bool + */ + public function isConjunctive() + { + return $this->conjunctive; + } + + /** + * @return bool + */ + public function isDisjunctive() + { + return !$this->conjunctive; + } + + public function compile($otherOperator) + { + $parts = array(); + foreach ($this->constraints as $constraint) { + $code = $constraint->compile($otherOperator); + if ($code === 'true') { + if (!$this->conjunctive) { + return 'true'; + } + } elseif ($code === 'false') { + if ($this->conjunctive) { + return 'false'; + } + } else { + $parts[] = '('.$code.')'; + } + } + + if (!$parts) { + return $this->conjunctive ? 'true' : 'false'; + } + + return $this->conjunctive ? implode('&&', $parts) : implode('||', $parts); + } + + /** + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider) + { + if (false === $this->conjunctive) { + foreach ($this->constraints as $constraint) { + if ($provider->matches($constraint)) { + return true; + } + } + + return false; + } + + foreach ($this->constraints as $constraint) { + if (!$provider->matches($constraint)) { + return false; + } + } + + return true; + } + + /** + * @param string|null $prettyString + */ + public function setPrettyString($prettyString) + { + $this->prettyString = $prettyString; + } + + /** + * @return string + */ + public function getPrettyString() + { + if ($this->prettyString) { + return $this->prettyString; + } + + return (string) $this; + } + + /** + * @return string + */ + public function __toString() + { + if ($this->string !== null) { + return $this->string; + } + + $constraints = array(); + foreach ($this->constraints as $constraint) { + $constraints[] = (string) $constraint; + } + + return $this->string = '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']'; + } + + /** + * {@inheritDoc} + */ + public function getLowerBound() + { + $this->extractBounds(); + + return $this->lowerBound; + } + + /** + * {@inheritDoc} + */ + public function getUpperBound() + { + $this->extractBounds(); + + return $this->upperBound; + } + + /** + * Tries to optimize the constraints as much as possible, meaning + * reducing/collapsing congruent constraints etc. + * Does not necessarily return a MultiConstraint instance if + * things can be reduced to a simple constraint + * + * @param ConstraintInterface[] $constraints A set of constraints + * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive + * + * @return ConstraintInterface + */ + public static function create(array $constraints, $conjunctive = true) + { + if (0 === \count($constraints)) { + return new MatchAllConstraint(); + } + + if (1 === \count($constraints)) { + return $constraints[0]; + } + + $optimized = self::optimizeConstraints($constraints, $conjunctive); + if ($optimized !== null) { + list($constraints, $conjunctive) = $optimized; + if (\count($constraints) === 1) { + return $constraints[0]; + } + } + + return new self($constraints, $conjunctive); + } + + /** + * @return array|null + */ + private static function optimizeConstraints(array $constraints, $conjunctive) + { + // parse the two OR groups and if they are contiguous we collapse + // them into one constraint + // [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4] + if (!$conjunctive) { + $left = $constraints[0]; + $mergedConstraints = array(); + $optimized = false; + for ($i = 1, $l = \count($constraints); $i < $l; $i++) { + $right = $constraints[$i]; + if ( + $left instanceof self + && $left->conjunctive + && $right instanceof self + && $right->conjunctive + && \count($left->constraints) === 2 + && \count($right->constraints) === 2 + && ($left0 = (string) $left->constraints[0]) + && $left0[0] === '>' && $left0[1] === '=' + && ($left1 = (string) $left->constraints[1]) + && $left1[0] === '<' + && ($right0 = (string) $right->constraints[0]) + && $right0[0] === '>' && $right0[1] === '=' + && ($right1 = (string) $right->constraints[1]) + && $right1[0] === '<' + && substr($left1, 2) === substr($right0, 3) + ) { + $optimized = true; + $left = new MultiConstraint( + array( + $left->constraints[0], + $right->constraints[1], + ), + true); + } else { + $mergedConstraints[] = $left; + $left = $right; + } + } + if ($optimized) { + $mergedConstraints[] = $left; + return array($mergedConstraints, false); + } + } + + // TODO: Here's the place to put more optimizations + + return null; + } + + private function extractBounds() + { + if (null !== $this->lowerBound) { + return; + } + + foreach ($this->constraints as $constraint) { + if (null === $this->lowerBound && null === $this->upperBound) { + $this->lowerBound = $constraint->getLowerBound(); + $this->upperBound = $constraint->getUpperBound(); + continue; + } + + if ($constraint->getLowerBound()->compareTo($this->lowerBound, $this->isConjunctive() ? '>' : '<')) { + $this->lowerBound = $constraint->getLowerBound(); + } + + if ($constraint->getUpperBound()->compareTo($this->upperBound, $this->isConjunctive() ? '<' : '>')) { + $this->upperBound = $constraint->getUpperBound(); + } + } + } +} diff --git a/bundled-libs/composer/semver/src/Interval.php b/bundled-libs/composer/semver/src/Interval.php new file mode 100644 index 000000000..43d5a4f5c --- /dev/null +++ b/bundled-libs/composer/semver/src/Interval.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver; + +use Composer\Semver\Constraint\Constraint; + +class Interval +{ + /** @var Constraint */ + private $start; + /** @var Constraint */ + private $end; + + public function __construct(Constraint $start, Constraint $end) + { + $this->start = $start; + $this->end = $end; + } + + /** + * @return Constraint + */ + public function getStart() + { + return $this->start; + } + + /** + * @return Constraint + */ + public function getEnd() + { + return $this->end; + } + + /** + * @return Constraint + */ + public static function fromZero() + { + static $zero; + + if (null === $zero) { + $zero = new Constraint('>=', '0.0.0.0-dev'); + } + + return $zero; + } + + /** + * @return Constraint + */ + public static function untilPositiveInfinity() + { + static $positiveInfinity; + + if (null === $positiveInfinity) { + $positiveInfinity = new Constraint('<', PHP_INT_MAX.'.0.0.0'); + } + + return $positiveInfinity; + } + + /** + * @return self + */ + public static function any() + { + return new self(self::fromZero(), self::untilPositiveInfinity()); + } + + /** + * @return array{'names': string[], 'exclude': bool} + */ + public static function anyDev() + { + // any == exclude nothing + return array('names' => array(), 'exclude' => true); + } + + /** + * @return array{'names': string[], 'exclude': bool} + */ + public static function noDev() + { + // nothing == no names included + return array('names' => array(), 'exclude' => false); + } +} diff --git a/bundled-libs/composer/semver/src/Intervals.php b/bundled-libs/composer/semver/src/Intervals.php new file mode 100644 index 000000000..b2aa5338a --- /dev/null +++ b/bundled-libs/composer/semver/src/Intervals.php @@ -0,0 +1,481 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver; + +use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\ConstraintInterface; +use Composer\Semver\Constraint\MatchAllConstraint; +use Composer\Semver\Constraint\MatchNoneConstraint; +use Composer\Semver\Constraint\MultiConstraint; + +/** + * Helper class generating intervals from constraints + * + * This contains utilities for: + * + * - compacting an existing constraint which can be used to combine several into one + * by creating a MultiConstraint out of the many constraints you have. + * + * - checking whether one subset is a subset of another. + * + * Note: You should call clear to free memoization memory usage when you are done using this class + */ +class Intervals +{ + /** + * @phpstan-var array + */ + private static $intervalsCache = array(); + + /** + * @phpstan-var array + */ + private static $opSortOrder = array( + '>=' => -3, + '<' => -2, + '>' => 2, + '<=' => 3, + ); + + /** + * Clears the memoization cache once you are done + * + * @return void + */ + public static function clear() + { + self::$intervalsCache = array(); + } + + /** + * Checks whether $candidate is a subset of $constraint + * + * @return bool + */ + public static function isSubsetOf(ConstraintInterface $candidate, ConstraintInterface $constraint) + { + if ($constraint instanceof MatchAllConstraint) { + return true; + } + + if ($candidate instanceof MatchNoneConstraint || $constraint instanceof MatchNoneConstraint) { + return false; + } + + $intersectionIntervals = self::get(new MultiConstraint(array($candidate, $constraint), true)); + $candidateIntervals = self::get($candidate); + if (\count($intersectionIntervals['numeric']) !== \count($candidateIntervals['numeric'])) { + return false; + } + + foreach ($intersectionIntervals['numeric'] as $index => $interval) { + if (!isset($candidateIntervals['numeric'][$index])) { + return false; + } + + if ((string) $candidateIntervals['numeric'][$index]->getStart() !== (string) $interval->getStart()) { + return false; + } + + if ((string) $candidateIntervals['numeric'][$index]->getEnd() !== (string) $interval->getEnd()) { + return false; + } + } + + if ($intersectionIntervals['branches']['exclude'] !== $candidateIntervals['branches']['exclude']) { + return false; + } + if (\count($intersectionIntervals['branches']['names']) !== \count($candidateIntervals['branches']['names'])) { + return false; + } + foreach ($intersectionIntervals['branches']['names'] as $index => $name) { + if ($name !== $candidateIntervals['branches']['names'][$index]) { + return false; + } + } + + return true; + } + + /** + * Checks whether $a and $b have any intersection, equivalent to $a->matches($b) + * + * @return bool + */ + public static function haveIntersections(ConstraintInterface $a, ConstraintInterface $b) + { + if ($a instanceof MatchAllConstraint || $b instanceof MatchAllConstraint) { + return true; + } + + if ($a instanceof MatchNoneConstraint || $b instanceof MatchNoneConstraint) { + return false; + } + + $intersectionIntervals = self::generateIntervals(new MultiConstraint(array($a, $b), true), true); + + return \count($intersectionIntervals['numeric']) > 0 || $intersectionIntervals['branches']['exclude'] || \count($intersectionIntervals['branches']['names']) > 0; + } + + /** + * Attempts to optimize a MultiConstraint + * + * When merging MultiConstraints together they can get very large, this will + * compact it by looking at the real intervals covered by all the constraints + * and then creates a new constraint containing only the smallest amount of rules + * to match the same intervals. + * + * @return ConstraintInterface + */ + public static function compactConstraint(ConstraintInterface $constraint) + { + if (!$constraint instanceof MultiConstraint) { + return $constraint; + } + + $intervals = self::generateIntervals($constraint); + $constraints = array(); + $hasNumericMatchAll = false; + + if (\count($intervals['numeric']) === 1 && (string) $intervals['numeric'][0]->getStart() === (string) Interval::fromZero() && (string) $intervals['numeric'][0]->getEnd() === (string) Interval::untilPositiveInfinity()) { + $constraints[] = $intervals['numeric'][0]->getStart(); + $hasNumericMatchAll = true; + } else { + $unEqualConstraints = array(); + for ($i = 0, $count = \count($intervals['numeric']); $i < $count; $i++) { + $interval = $intervals['numeric'][$i]; + + // if current interval ends with < N and next interval begins with > N we can swap this out for != N + // but this needs to happen as a conjunctive expression together with the start of the current interval + // and end of next interval, so [>=M, N, [>=M, !=N, getEnd()->getOperator() === '<' && $i+1 < $count) { + $nextInterval = $intervals['numeric'][$i+1]; + if ($interval->getEnd()->getVersion() === $nextInterval->getStart()->getVersion() && $nextInterval->getStart()->getOperator() === '>') { + // only add a start if we didn't already do so, can be skipped if we're looking at second + // interval in [>=M, N, P, =M, !=N] already and we only want to add !=P right now + if (\count($unEqualConstraints) === 0 && (string) $interval->getStart() !== (string) Interval::fromZero()) { + $unEqualConstraints[] = $interval->getStart(); + } + $unEqualConstraints[] = new Constraint('!=', $interval->getEnd()->getVersion()); + continue; + } + } + + if (\count($unEqualConstraints) > 0) { + // this is where the end of the following interval of a != constraint is added as explained above + if ((string) $interval->getEnd() !== (string) Interval::untilPositiveInfinity()) { + $unEqualConstraints[] = $interval->getEnd(); + } + + // count is 1 if entire constraint is just one != expression + if (\count($unEqualConstraints) > 1) { + $constraints[] = new MultiConstraint($unEqualConstraints, true); + } else { + $constraints[] = $unEqualConstraints[0]; + } + + $unEqualConstraints = array(); + continue; + } + + // convert back >= x - <= x intervals to == x + if ($interval->getStart()->getVersion() === $interval->getEnd()->getVersion() && $interval->getStart()->getOperator() === '>=' && $interval->getEnd()->getOperator() === '<=') { + $constraints[] = new Constraint('==', $interval->getStart()->getVersion()); + continue; + } + + if ((string) $interval->getStart() === (string) Interval::fromZero()) { + $constraints[] = $interval->getEnd(); + } elseif ((string) $interval->getEnd() === (string) Interval::untilPositiveInfinity()) { + $constraints[] = $interval->getStart(); + } else { + $constraints[] = new MultiConstraint(array($interval->getStart(), $interval->getEnd()), true); + } + } + } + + $devConstraints = array(); + + if (0 === \count($intervals['branches']['names'])) { + if ($intervals['branches']['exclude']) { + if ($hasNumericMatchAll) { + return new MatchAllConstraint; + } + // otherwise constraint should contain a != operator and already cover this + } + } else { + foreach ($intervals['branches']['names'] as $branchName) { + if ($intervals['branches']['exclude']) { + $devConstraints[] = new Constraint('!=', $branchName); + } else { + $devConstraints[] = new Constraint('==', $branchName); + } + } + + // excluded branches, e.g. != dev-foo are conjunctive with the interval, so + // > 2.0 != dev-foo must return a conjunctive constraint + if ($intervals['branches']['exclude']) { + if (\count($constraints) > 1) { + return new MultiConstraint(array_merge( + array(new MultiConstraint($constraints, false)), + $devConstraints + ), true); + } + + if (\count($constraints) === 1 && (string)$constraints[0] === (string)Interval::fromZero()) { + if (\count($devConstraints) > 1) { + return new MultiConstraint($devConstraints, true); + } + return $devConstraints[0]; + } + + return new MultiConstraint(array_merge($constraints, $devConstraints), true); + } + + // otherwise devConstraints contains a list of == operators for branches which are disjunctive with the + // rest of the constraint + $constraints = array_merge($constraints, $devConstraints); + } + + if (\count($constraints) > 1) { + return new MultiConstraint($constraints, false); + } + + if (\count($constraints) === 1) { + return $constraints[0]; + } + + return new MatchNoneConstraint; + } + + /** + * Creates an array of numeric intervals and branch constraints representing a given constraint + * + * if the returned numeric array is empty it means the constraint matches nothing in the numeric range (0 - +inf) + * if the returned branches array is empty it means no dev-* versions are matched + * if a constraint matches all possible dev-* versions, branches will contain Interval::anyDev() + * + * @return array + * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}} + */ + public static function get(ConstraintInterface $constraint) + { + $key = (string) $constraint; + + if (!isset(self::$intervalsCache[$key])) { + self::$intervalsCache[$key] = self::generateIntervals($constraint); + } + + return self::$intervalsCache[$key]; + } + + /** + * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}} + */ + private static function generateIntervals(ConstraintInterface $constraint, $stopOnFirstValidInterval = false) + { + if ($constraint instanceof MatchAllConstraint) { + return array('numeric' => array(new Interval(Interval::fromZero(), Interval::untilPositiveInfinity())), 'branches' => Interval::anyDev()); + } + + if ($constraint instanceof MatchNoneConstraint) { + return array('numeric' => array(), 'branches' => array('names' => array(), 'exclude' => false)); + } + + if ($constraint instanceof Constraint) { + return self::generateSingleConstraintIntervals($constraint); + } + + if (!$constraint instanceof MultiConstraint) { + throw new \UnexpectedValueException('The constraint passed in should be an MatchAllConstraint, Constraint or MultiConstraint instance, got '.\get_class($constraint).'.'); + } + + $constraints = $constraint->getConstraints(); + + $numericGroups = array(); + $constraintBranches = array(); + foreach ($constraints as $c) { + $res = self::get($c); + $numericGroups[] = $res['numeric']; + $constraintBranches[] = $res['branches']; + } + + if ($constraint->isDisjunctive()) { + $branches = Interval::noDev(); + foreach ($constraintBranches as $b) { + if ($b['exclude']) { + if ($branches['exclude']) { + // disjunctive constraint, so only exclude what's excluded in all constraints + // !=a,!=b || !=b,!=c => !=b + $branches['names'] = array_intersect($branches['names'], $b['names']); + } else { + // disjunctive constraint so exclude all names which are not explicitly included in the alternative + // (==b || ==c) || !=a,!=b => !=a + $branches['exclude'] = true; + $branches['names'] = array_diff($b['names'], $branches['names']); + } + } else { + if ($branches['exclude']) { + // disjunctive constraint so exclude all names which are not explicitly included in the alternative + // !=a,!=b || (==b || ==c) => !=a + $branches['names'] = array_diff($branches['names'], $b['names']); + } else { + // disjunctive constraint, so just add all the other branches + // (==a || ==b) || ==c => ==a || ==b || ==c + $branches['names'] = array_merge($branches['names'], $b['names']); + } + } + } + } else { + $branches = Interval::anyDev(); + foreach ($constraintBranches as $b) { + if ($b['exclude']) { + if ($branches['exclude']) { + // conjunctive, so just add all branch names to be excluded + // !=a && !=b => !=a,!=b + $branches['names'] = array_merge($branches['names'], $b['names']); + } else { + // conjunctive, so only keep included names which are not excluded + // (==a||==c) && !=a,!=b => ==c + $branches['names'] = array_diff($branches['names'], $b['names']); + } + } else { + if ($branches['exclude']) { + // conjunctive, so only keep included names which are not excluded + // !=a,!=b && (==a||==c) => ==c + $branches['names'] = array_diff($b['names'], $branches['names']); + $branches['exclude'] = false; + } else { + // conjunctive, so only keep names that are included in both + // (==a||==b) && (==a||==c) => ==a + $branches['names'] = array_intersect($branches['names'], $b['names']); + } + } + } + } + + $branches['names'] = array_unique($branches['names']); + + if (\count($numericGroups) === 1) { + return array('numeric' => $numericGroups[0], 'branches' => $branches); + } + + $borders = array(); + foreach ($numericGroups as $group) { + foreach ($group as $interval) { + $borders[] = array('version' => $interval->getStart()->getVersion(), 'operator' => $interval->getStart()->getOperator(), 'side' => 'start'); + $borders[] = array('version' => $interval->getEnd()->getVersion(), 'operator' => $interval->getEnd()->getOperator(), 'side' => 'end'); + } + } + + $opSortOrder = self::$opSortOrder; + usort($borders, function ($a, $b) use ($opSortOrder) { + $order = version_compare($a['version'], $b['version']); + if ($order === 0) { + return $opSortOrder[$a['operator']] - $opSortOrder[$b['operator']]; + } + + return $order; + }); + + $activeIntervals = 0; + $intervals = array(); + $index = 0; + $activationThreshold = $constraint->isConjunctive() ? \count($numericGroups) : 1; + $active = false; + $start = null; + foreach ($borders as $border) { + if ($border['side'] === 'start') { + $activeIntervals++; + } else { + $activeIntervals--; + } + if (!$active && $activeIntervals >= $activationThreshold) { + $start = new Constraint($border['operator'], $border['version']); + $active = true; + } + if ($active && $activeIntervals < $activationThreshold) { + $active = false; + + // filter out invalid intervals like > x - <= x, or >= x - < x + if ( + version_compare($start->getVersion(), $border['version'], '=') + && ( + ($start->getOperator() === '>' && $border['operator'] === '<=') + || ($start->getOperator() === '>=' && $border['operator'] === '<') + ) + ) { + unset($intervals[$index]); + } else { + $intervals[$index] = new Interval($start, new Constraint($border['operator'], $border['version'])); + $index++; + + if ($stopOnFirstValidInterval) { + break; + } + } + + $start = null; + } + } + + return array('numeric' => $intervals, 'branches' => $branches); + } + + /** + * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}} + */ + private static function generateSingleConstraintIntervals(Constraint $constraint) + { + $op = $constraint->getOperator(); + + // handle branch constraints first + if (strpos($constraint->getVersion(), 'dev-') === 0) { + $intervals = array(); + $branches = array('names' => array(), 'exclude' => false); + + // != dev-foo means any numeric version may match, we treat >/< like != they are not really defined for branches + if ($op === '!=') { + $intervals[] = new Interval(Interval::fromZero(), Interval::untilPositiveInfinity()); + $branches = array('names' => array($constraint->getVersion()), 'exclude' => true); + } elseif ($op === '==') { + $branches['names'][] = $constraint->getVersion(); + } + + return array( + 'numeric' => $intervals, + 'branches' => $branches, + ); + } + + if ($op[0] === '>') { // > & >= + return array('numeric' => array(new Interval($constraint, Interval::untilPositiveInfinity())), 'branches' => Interval::noDev()); + } + if ($op[0] === '<') { // < & <= + return array('numeric' => array(new Interval(Interval::fromZero(), $constraint)), 'branches' => Interval::noDev()); + } + if ($op === '!=') { + // convert !=x to intervals of 0 - x - +inf + dev* + return array('numeric' => array( + new Interval(Interval::fromZero(), new Constraint('<', $constraint->getVersion())), + new Interval(new Constraint('>', $constraint->getVersion()), Interval::untilPositiveInfinity()), + ), 'branches' => Interval::anyDev()); + } + + // convert ==x to an interval of >=x - <=x + return array('numeric' => array( + new Interval(new Constraint('>=', $constraint->getVersion()), new Constraint('<=', $constraint->getVersion())), + ), 'branches' => Interval::noDev()); + } +} diff --git a/bundled-libs/composer/semver/src/Semver.php b/bundled-libs/composer/semver/src/Semver.php new file mode 100644 index 000000000..440aa1070 --- /dev/null +++ b/bundled-libs/composer/semver/src/Semver.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver; + +use Composer\Semver\Constraint\Constraint; + +class Semver +{ + const SORT_ASC = 1; + const SORT_DESC = -1; + + /** @var VersionParser */ + private static $versionParser; + + /** + * Determine if given version satisfies given constraints. + * + * @param string $version + * @param string $constraints + * + * @return bool + */ + public static function satisfies($version, $constraints) + { + if (null === self::$versionParser) { + self::$versionParser = new VersionParser(); + } + + $versionParser = self::$versionParser; + $provider = new Constraint('==', $versionParser->normalize($version)); + $parsedConstraints = $versionParser->parseConstraints($constraints); + + return $parsedConstraints->matches($provider); + } + + /** + * Return all versions that satisfy given constraints. + * + * @param array $versions + * @param string $constraints + * + * @return array + */ + public static function satisfiedBy(array $versions, $constraints) + { + $versions = array_filter($versions, function ($version) use ($constraints) { + return Semver::satisfies($version, $constraints); + }); + + return array_values($versions); + } + + /** + * Sort given array of versions. + * + * @param array $versions + * + * @return array + */ + public static function sort(array $versions) + { + return self::usort($versions, self::SORT_ASC); + } + + /** + * Sort given array of versions in reverse. + * + * @param array $versions + * + * @return array + */ + public static function rsort(array $versions) + { + return self::usort($versions, self::SORT_DESC); + } + + /** + * @param array $versions + * @param int $direction + * + * @return array + */ + private static function usort(array $versions, $direction) + { + if (null === self::$versionParser) { + self::$versionParser = new VersionParser(); + } + + $versionParser = self::$versionParser; + $normalized = array(); + + // Normalize outside of usort() scope for minor performance increase. + // Creates an array of arrays: [[normalized, key], ...] + foreach ($versions as $key => $version) { + $normalizedVersion = $versionParser->normalize($version); + $normalizedVersion = $versionParser->normalizeDefaultBranch($normalizedVersion); + $normalized[] = array($normalizedVersion, $key); + } + + usort($normalized, function (array $left, array $right) use ($direction) { + if ($left[0] === $right[0]) { + return 0; + } + + if (Comparator::lessThan($left[0], $right[0])) { + return -$direction; + } + + return $direction; + }); + + // Recreate input array, using the original indexes which are now in sorted order. + $sorted = array(); + foreach ($normalized as $item) { + $sorted[] = $versions[$item[1]]; + } + + return $sorted; + } +} diff --git a/bundled-libs/composer/semver/src/VersionParser.php b/bundled-libs/composer/semver/src/VersionParser.php new file mode 100644 index 000000000..55679500b --- /dev/null +++ b/bundled-libs/composer/semver/src/VersionParser.php @@ -0,0 +1,573 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver; + +use Composer\Semver\Constraint\ConstraintInterface; +use Composer\Semver\Constraint\MatchAllConstraint; +use Composer\Semver\Constraint\MultiConstraint; +use Composer\Semver\Constraint\Constraint; + +/** + * Version parser. + * + * @author Jordi Boggiano + */ +class VersionParser +{ + /** + * Regex to match pre-release data (sort of). + * + * Due to backwards compatibility: + * - Instead of enforcing hyphen, an underscore, dot or nothing at all are also accepted. + * - Only stabilities as recognized by Composer are allowed to precede a numerical identifier. + * - Numerical-only pre-release identifiers are not supported, see tests. + * + * |--------------| + * [major].[minor].[patch] -[pre-release] +[build-metadata] + * + * @var string + */ + private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?'; + + /** @var string */ + private static $stabilitiesRegex = 'stable|RC|beta|alpha|dev'; + + /** + * Returns the stability of a version. + * + * @param string $version + * + * @return string + */ + public static function parseStability($version) + { + $version = preg_replace('{#.+$}', '', $version); + + if (strpos($version, 'dev-') === 0 || '-dev' === substr($version, -4)) { + return 'dev'; + } + + preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match); + + if (!empty($match[3])) { + return 'dev'; + } + + if (!empty($match[1])) { + if ('beta' === $match[1] || 'b' === $match[1]) { + return 'beta'; + } + if ('alpha' === $match[1] || 'a' === $match[1]) { + return 'alpha'; + } + if ('rc' === $match[1]) { + return 'RC'; + } + } + + return 'stable'; + } + + /** + * @param string $stability + * + * @return string + */ + public static function normalizeStability($stability) + { + $stability = strtolower($stability); + + return $stability === 'rc' ? 'RC' : $stability; + } + + /** + * Normalizes a version string to be able to perform comparisons on it. + * + * @param string $version + * @param string $fullVersion optional complete version string to give more context + * + * @throws \UnexpectedValueException + * + * @return string + */ + public function normalize($version, $fullVersion = null) + { + $version = trim($version); + $origVersion = $version; + if (null === $fullVersion) { + $fullVersion = $version; + } + + // strip off aliasing + if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) { + $version = $match[1]; + } + + // strip off stability flag + if (preg_match('{@(?:' . self::$stabilitiesRegex . ')$}i', $version, $match)) { + $version = substr($version, 0, strlen($version) - strlen($match[0])); + } + + // normalize master/trunk/default branches to dev-name for BC with 1.x as these used to be valid constraints + if (\in_array($version, array('master', 'trunk', 'default'), true)) { + $version = 'dev-' . $version; + } + + // if requirement is branch-like, use full name + if (stripos($version, 'dev-') === 0) { + return 'dev-' . substr($version, 4); + } + + // strip off build metadata + if (preg_match('{^([^,\s+]++)\+[^\s]++$}', $version, $match)) { + $version = $match[1]; + } + + // match classical versioning + if (preg_match('{^v?(\d{1,5})(\.\d++)?(\.\d++)?(\.\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) { + $version = $matches[1] + . (!empty($matches[2]) ? $matches[2] : '.0') + . (!empty($matches[3]) ? $matches[3] : '.0') + . (!empty($matches[4]) ? $matches[4] : '.0'); + $index = 5; + // match date(time) based versioning + } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) { + $version = preg_replace('{\D}', '.', $matches[1]); + $index = 2; + } + + // add version modifiers if a version was matched + if (isset($index)) { + if (!empty($matches[$index])) { + if ('stable' === $matches[$index]) { + return $version; + } + $version .= '-' . $this->expandStability($matches[$index]) . (isset($matches[$index + 1]) && '' !== $matches[$index + 1] ? ltrim($matches[$index + 1], '.-') : ''); + } + + if (!empty($matches[$index + 2])) { + $version .= '-dev'; + } + + return $version; + } + + // match dev branches + if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) { + try { + $normalized = $this->normalizeBranch($match[1]); + // a branch ending with -dev is only valid if it is numeric + // if it gets prefixed with dev- it means the branch name should + // have had a dev- prefix already when passed to normalize + if (strpos($normalized, 'dev-') === false) { + return $normalized; + } + } catch (\Exception $e) { + } + } + + $extraMessage = ''; + if (preg_match('{ +as +' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))?$}', $fullVersion)) { + $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version'; + } elseif (preg_match('{^' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))? +as +}', $fullVersion)) { + $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-'; + } + + throw new \UnexpectedValueException('Invalid version string "' . $origVersion . '"' . $extraMessage); + } + + /** + * Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison. + * + * @param string $branch Branch name (e.g. 2.1.x-dev) + * + * @return string|false Numeric prefix if present (e.g. 2.1.) or false + */ + public function parseNumericAliasPrefix($branch) + { + if (preg_match('{^(?P(\d++\\.)*\d++)(?:\.x)?-dev$}i', $branch, $matches)) { + return $matches['version'] . '.'; + } + + return false; + } + + /** + * Normalizes a branch name to be able to perform comparisons on it. + * + * @param string $name + * + * @return string + */ + public function normalizeBranch($name) + { + $name = trim($name); + + if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) { + $version = ''; + for ($i = 1; $i < 5; ++$i) { + $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x'; + } + + return str_replace('x', '9999999', $version) . '-dev'; + } + + return 'dev-' . $name; + } + + /** + * Normalizes a default branch name (i.e. master on git) to 9999999-dev. + * + * @param string $name + * + * @return string + */ + public function normalizeDefaultBranch($name) + { + if ($name === 'dev-master' || $name === 'dev-default' || $name === 'dev-trunk') { + return '9999999-dev'; + } + + return $name; + } + + /** + * Parses a constraint string into MultiConstraint and/or Constraint objects. + * + * @param string $constraints + * + * @return ConstraintInterface + */ + public function parseConstraints($constraints) + { + $prettyConstraint = $constraints; + + $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints)); + $orGroups = array(); + + foreach ($orConstraints as $constraints) { + $andConstraints = preg_split('{(?< ,]) *(? 1) { + $constraintObjects = array(); + foreach ($andConstraints as $constraint) { + foreach ($this->parseConstraint($constraint) as $parsedConstraint) { + $constraintObjects[] = $parsedConstraint; + } + } + } else { + $constraintObjects = $this->parseConstraint($andConstraints[0]); + } + + if (1 === \count($constraintObjects)) { + $constraint = $constraintObjects[0]; + } else { + $constraint = new MultiConstraint($constraintObjects); + } + + $orGroups[] = $constraint; + } + + $constraint = MultiConstraint::create($orGroups, false); + + $constraint->setPrettyString($prettyConstraint); + + return $constraint; + } + + /** + * @param string $constraint + * + * @throws \UnexpectedValueException + * + * @return array + */ + private function parseConstraint($constraint) + { + // strip off aliasing + if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $constraint, $match)) { + $constraint = $match[1]; + } + + // strip @stability flags, and keep it for later use + if (preg_match('{^([^,\s]*?)@(' . self::$stabilitiesRegex . ')$}i', $constraint, $match)) { + $constraint = '' !== $match[1] ? $match[1] : '*'; + if ($match[2] !== 'stable') { + $stabilityModifier = $match[2]; + } + } + + // get rid of #refs as those are used by composer only + if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraint, $match)) { + $constraint = $match[1]; + } + + if (preg_match('{^(v)?[xX*](\.[xX*])*$}i', $constraint, $match)) { + if (!empty($match[1]) || !empty($match[2])) { + return array(new Constraint('>=', '0.0.0.0-dev')); + } + + return array(new MatchAllConstraint()); + } + + $versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?(?:' . self::$modifierRegex . '|\.([xX*][.-]?dev))(?:\+[^\s]+)?'; + + // Tilde Range + // + // Like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous + // version, to ensure that unstable instances of the current version are allowed. However, if a stability + // suffix is added to the constraint, then a >= match on the current version is used instead. + if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) { + if (strpos($constraint, '~>') === 0) { + throw new \UnexpectedValueException( + 'Could not parse version constraint ' . $constraint . ': ' . + 'Invalid operator "~>", you probably meant to use the "~" operator' + ); + } + + // Work out which position in the version we are operating at + if (isset($matches[4]) && '' !== $matches[4] && null !== $matches[4]) { + $position = 4; + } elseif (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) { + $position = 3; + } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) { + $position = 2; + } else { + $position = 1; + } + + // when matching 2.x-dev or 3.0.x-dev we have to shift the second or third number, despite no second/third number matching above + if (!empty($matches[8])) { + $position++; + } + + // Calculate the stability suffix + $stabilitySuffix = ''; + if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) { + $stabilitySuffix .= '-dev'; + } + + $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); + $lowerBound = new Constraint('>=', $lowVersion); + + // For upper bound, we increment the position of one more significance, + // but highPosition = 0 would be illegal + $highPosition = max(1, $position - 1); + $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev'; + $upperBound = new Constraint('<', $highVersion); + + return array( + $lowerBound, + $upperBound, + ); + } + + // Caret Range + // + // Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple. + // In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for + // versions 0.X >=0.1.0, and no updates for versions 0.0.X + if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) { + // Work out which position in the version we are operating at + if ('0' !== $matches[1] || '' === $matches[2] || null === $matches[2]) { + $position = 1; + } elseif ('0' !== $matches[2] || '' === $matches[3] || null === $matches[3]) { + $position = 2; + } else { + $position = 3; + } + + // Calculate the stability suffix + $stabilitySuffix = ''; + if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) { + $stabilitySuffix .= '-dev'; + } + + $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); + $lowerBound = new Constraint('>=', $lowVersion); + + // For upper bound, we increment the position of one more significance, + // but highPosition = 0 would be illegal + $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; + $upperBound = new Constraint('<', $highVersion); + + return array( + $lowerBound, + $upperBound, + ); + } + + // X Range + // + // Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple. + // A partial version range is treated as an X-Range, so the special character is in fact optional. + if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) { + if (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) { + $position = 3; + } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) { + $position = 2; + } else { + $position = 1; + } + + $lowVersion = $this->manipulateVersionString($matches, $position) . '-dev'; + $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; + + if ($lowVersion === '0.0.0.0-dev') { + return array(new Constraint('<', $highVersion)); + } + + return array( + new Constraint('>=', $lowVersion), + new Constraint('<', $highVersion), + ); + } + + // Hyphen Range + // + // Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range, + // then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in + // the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but + // nothing that would be greater than the provided tuple parts. + if (preg_match('{^(?P' . $versionRegex . ') +- +(?P' . $versionRegex . ')($)}i', $constraint, $matches)) { + // Calculate the stability suffix + $lowStabilitySuffix = ''; + if (empty($matches[6]) && empty($matches[8]) && empty($matches[9])) { + $lowStabilitySuffix = '-dev'; + } + + $lowVersion = $this->normalize($matches['from']); + $lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix); + + $empty = function ($x) { + return ($x === 0 || $x === '0') ? false : empty($x); + }; + + if ((!$empty($matches[12]) && !$empty($matches[13])) || !empty($matches[15]) || !empty($matches[17]) || !empty($matches[18])) { + $highVersion = $this->normalize($matches['to']); + $upperBound = new Constraint('<=', $highVersion); + } else { + $highMatch = array('', $matches[11], $matches[12], $matches[13], $matches[14]); + + // validate to version + $this->normalize($matches['to']); + + $highVersion = $this->manipulateVersionString($highMatch, $empty($matches[12]) ? 1 : 2, 1) . '-dev'; + $upperBound = new Constraint('<', $highVersion); + } + + return array( + $lowerBound, + $upperBound, + ); + } + + // Basic Comparators + if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) { + try { + try { + $version = $this->normalize($matches[2]); + } catch (\UnexpectedValueException $e) { + // recover from an invalid constraint like foobar-dev which should be dev-foobar + // except if the constraint uses a known operator, in which case it must be a parse error + if (substr($matches[2], -4) === '-dev' && preg_match('{^[0-9a-zA-Z-./]+$}', $matches[2])) { + $version = $this->normalize('dev-'.substr($matches[2], 0, -4)); + } else { + throw $e; + } + } + + $op = $matches[1] ?: '='; + + if ($op !== '==' && $op !== '=' && !empty($stabilityModifier) && self::parseStability($version) === 'stable') { + $version .= '-' . $stabilityModifier; + } elseif ('<' === $op || '>=' === $op) { + if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) { + if (strpos($matches[2], 'dev-') !== 0) { + $version .= '-dev'; + } + } + } + + return array(new Constraint($matches[1] ?: '=', $version)); + } catch (\Exception $e) { + } + } + + $message = 'Could not parse version constraint ' . $constraint; + if (isset($e)) { + $message .= ': ' . $e->getMessage(); + } + + throw new \UnexpectedValueException($message); + } + + /** + * Increment, decrement, or simply pad a version number. + * + * Support function for {@link parseConstraint()} + * + * @param array $matches Array with version parts in array indexes 1,2,3,4 + * @param int $position 1,2,3,4 - which segment of the version to increment/decrement + * @param int $increment + * @param string $pad The string to pad version parts after $position + * + * @return string|null The new version + */ + private function manipulateVersionString($matches, $position, $increment = 0, $pad = '0') + { + for ($i = 4; $i > 0; --$i) { + if ($i > $position) { + $matches[$i] = $pad; + } elseif ($i === $position && $increment) { + $matches[$i] += $increment; + // If $matches[$i] was 0, carry the decrement + if ($matches[$i] < 0) { + $matches[$i] = $pad; + --$position; + + // Return null on a carry overflow + if ($i === 1) { + return null; + } + } + } + } + + return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4]; + } + + /** + * Expand shorthand stability string to long version. + * + * @param string $stability + * + * @return string + */ + private function expandStability($stability) + { + $stability = strtolower($stability); + + switch ($stability) { + case 'a': + return 'alpha'; + case 'b': + return 'beta'; + case 'p': + case 'pl': + return 'patch'; + case 'rc': + return 'RC'; + default: + return $stability; + } + } +} diff --git a/bundled-libs/composer/xdebug-handler/CHANGELOG.md b/bundled-libs/composer/xdebug-handler/CHANGELOG.md new file mode 100644 index 000000000..38fa75c7e --- /dev/null +++ b/bundled-libs/composer/xdebug-handler/CHANGELOG.md @@ -0,0 +1,91 @@ +## [Unreleased] + +## [1.4.6] - 2021-03-25 + * Fixed: fail restart if `proc_open` has been disabled in `disable_functions`. + * Fixed: enable Windows CTRL event handling in the restarted process. + +## [1.4.5] - 2020-11-13 + * Fixed: use `proc_open` when available for correct FD forwarding to the restarted process. + +## [1.4.4] - 2020-10-24 + * Fix: exception if 'pcntl_signal' is disabled. + +## [1.4.3] - 2020-08-19 + * Fixed: restore SIGINT to default handler in restarted process if no other handler exists. + +## [1.4.2] - 2020-06-04 + * Fixed: ignore SIGINTs to let the restarted process handle them. + +## [1.4.1] - 2020-03-01 + * Fixed: restart fails if an ini file is empty. + +## [1.4.0] - 2019-11-06 + * Added: support for `NO_COLOR` environment variable: https://no-color.org + * Added: color support for Hyper terminal: https://github.com/zeit/hyper + * Fixed: correct capitalization of Xdebug (apparently). + * Fixed: improved handling for uopz extension. + +## [1.3.3] - 2019-05-27 + * Fixed: add environment changes to `$_ENV` if it is being used. + +## [1.3.2] - 2019-01-28 + * Fixed: exit call being blocked by uopz extension, resulting in application code running twice. + +## [1.3.1] - 2018-11-29 + * Fixed: fail restart if `passthru` has been disabled in `disable_functions`. + * Fixed: fail restart if an ini file cannot be opened, otherwise settings will be missing. + +## [1.3.0] - 2018-08-31 + * Added: `setPersistent` method to use environment variables for the restart. + * Fixed: improved debugging by writing output to stderr. + * Fixed: no restart when `php_ini_scanned_files` is not functional and is needed. + +## [1.2.1] - 2018-08-23 + * Fixed: fatal error with apc, when using `apc.mmap_file_mask`. + +## [1.2.0] - 2018-08-16 + * Added: debug information using `XDEBUG_HANDLER_DEBUG`. + * Added: fluent interface for setters. + * Added: `PhpConfig` helper class for calling PHP sub-processes. + * Added: `PHPRC` original value to restart stettings, for use in a restarted process. + * Changed: internal procedure to disable ini-scanning, using `-n` command-line option. + * Fixed: replaced `escapeshellarg` usage to avoid locale problems. + * Fixed: improved color-option handling to respect double-dash delimiter. + * Fixed: color-option handling regression from main script changes. + * Fixed: improved handling when checking main script. + * Fixed: handling for standard input, that never actually did anything. + * Fixed: fatal error when ctype extension is not available. + +## [1.1.0] - 2018-04-11 + * Added: `getRestartSettings` method for calling PHP processes in a restarted process. + * Added: API definition and @internal class annotations. + * Added: protected `requiresRestart` method for extending classes. + * Added: `setMainScript` method for applications that change the working directory. + * Changed: private `tmpIni` variable to protected for extending classes. + * Fixed: environment variables not available in $_SERVER when restored in the restart. + * Fixed: relative path problems caused by Phar::interceptFileFuncs. + * Fixed: incorrect handling when script file cannot be found. + +## [1.0.0] - 2018-03-08 + * Added: PSR3 logging for optional status output. + * Added: existing ini settings are merged to catch command-line overrides. + * Added: code, tests and other artefacts to decouple from Composer. + * Break: the following class was renamed: + - `Composer\XdebugHandler` -> `Composer\XdebugHandler\XdebugHandler` + +[Unreleased]: https://github.com/composer/xdebug-handler/compare/1.4.6...HEAD +[1.4.6]: https://github.com/composer/xdebug-handler/compare/1.4.5...1.4.6 +[1.4.5]: https://github.com/composer/xdebug-handler/compare/1.4.4...1.4.5 +[1.4.4]: https://github.com/composer/xdebug-handler/compare/1.4.3...1.4.4 +[1.4.3]: https://github.com/composer/xdebug-handler/compare/1.4.2...1.4.3 +[1.4.2]: https://github.com/composer/xdebug-handler/compare/1.4.1...1.4.2 +[1.4.1]: https://github.com/composer/xdebug-handler/compare/1.4.0...1.4.1 +[1.4.0]: https://github.com/composer/xdebug-handler/compare/1.3.3...1.4.0 +[1.3.3]: https://github.com/composer/xdebug-handler/compare/1.3.2...1.3.3 +[1.3.2]: https://github.com/composer/xdebug-handler/compare/1.3.1...1.3.2 +[1.3.1]: https://github.com/composer/xdebug-handler/compare/1.3.0...1.3.1 +[1.3.0]: https://github.com/composer/xdebug-handler/compare/1.2.1...1.3.0 +[1.2.1]: https://github.com/composer/xdebug-handler/compare/1.2.0...1.2.1 +[1.2.0]: https://github.com/composer/xdebug-handler/compare/1.1.0...1.2.0 +[1.1.0]: https://github.com/composer/xdebug-handler/compare/1.0.0...1.1.0 +[1.0.0]: https://github.com/composer/xdebug-handler/compare/d66f0d15cb57...1.0.0 diff --git a/bundled-libs/composer/xdebug-handler/LICENSE b/bundled-libs/composer/xdebug-handler/LICENSE new file mode 100644 index 000000000..963618a14 --- /dev/null +++ b/bundled-libs/composer/xdebug-handler/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Composer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bundled-libs/composer/xdebug-handler/README.md b/bundled-libs/composer/xdebug-handler/README.md new file mode 100644 index 000000000..734d95847 --- /dev/null +++ b/bundled-libs/composer/xdebug-handler/README.md @@ -0,0 +1,293 @@ +# composer/xdebug-handler + +[![packagist](https://img.shields.io/packagist/v/composer/xdebug-handler.svg)](https://packagist.org/packages/composer/xdebug-handler) +[![Continuous Integration](https://github.com/composer/xdebug-handler/workflows/Continuous%20Integration/badge.svg?branch=main)](https://github.com/composer/xdebug-handler/actions) +![license](https://img.shields.io/github/license/composer/xdebug-handler.svg) +![php](https://img.shields.io/packagist/php-v/composer/xdebug-handler.svg?colorB=8892BF&label=php) + +Restart a CLI process without loading the Xdebug extension. + +Originally written as part of [composer/composer](https://github.com/composer/composer), +now extracted and made available as a stand-alone library. + +## Installation + +Install the latest version with: + +```bash +$ composer require composer/xdebug-handler +``` + +## Requirements + +* PHP 5.3.2 minimum, although functionality is disabled below PHP 5.4.0. Using the latest PHP version is highly recommended. + +## Basic Usage +```php +use Composer\XdebugHandler\XdebugHandler; + +$xdebug = new XdebugHandler('myapp'); +$xdebug->check(); +unset($xdebug); +``` + +The constructor takes two parameters: + +#### _$envPrefix_ +This is used to create distinct environment variables and is upper-cased and prepended to default base values. The above example enables the use of: + +- `MYAPP_ALLOW_XDEBUG=1` to override automatic restart and allow Xdebug +- `MYAPP_ORIGINAL_INIS` to obtain ini file locations in a restarted process + +#### _$colorOption_ +This optional value is added to the restart command-line and is needed to force color output in a piped child process. Only long-options are supported, for example `--ansi` or `--colors=always` etc. + +If the original command-line contains an argument that pattern matches this value (for example `--no-ansi`, `--colors=never`) then _$colorOption_ is ignored. + +If the pattern match ends with `=auto` (for example `--colors=auto`), the argument is replaced by _$colorOption_. Otherwise it is added at either the end of the command-line, or preceding the first double-dash `--` delimiter. + +## Advanced Usage + +* [How it works](#how-it-works) +* [Limitations](#limitations) +* [Helper methods](#helper-methods) +* [Setter methods](#setter-methods) +* [Process configuration](#process-configuration) +* [Troubleshooting](#troubleshooting) +* [Extending the library](#extending-the-library) + +### How it works + +A temporary ini file is created from the loaded (and scanned) ini files, with any references to the Xdebug extension commented out. Current ini settings are merged, so that most ini settings made on the command-line or by the application are included (see [Limitations](#limitations)) + +* `MYAPP_ALLOW_XDEBUG` is set with internal data to flag and use in the restart. +* The command-line and environment are [configured](#process-configuration) for the restart. +* The application is restarted in a new process. + * The restart settings are stored in the environment. + * `MYAPP_ALLOW_XDEBUG` is unset. + * The application runs and exits. +* The main process exits with the exit code from the restarted process. + +#### Signal handling +From PHP 7.1 with the pcntl extension loaded, asynchronous signal handling is automatically enabled. `SIGINT` is set to `SIG_IGN` in the parent +process and restored to `SIG_DFL` in the restarted process (if no other handler has been set). + +From PHP 7.4 on Windows, `CTRL+C` and `CTRL+BREAK` handling is ignored in the parent process and automatically enabled in the restarted process. + +### Limitations +There are a few things to be aware of when running inside a restarted process. + +* Extensions set on the command-line will not be loaded. +* Ini file locations will be reported as per the restart - see [getAllIniFiles()](#getallinifiles). +* Php sub-processes may be loaded with Xdebug enabled - see [Process configuration](#process-configuration). + +### Helper methods +These static methods provide information from the current process, regardless of whether it has been restarted or not. + +#### _getAllIniFiles()_ +Returns an array of the original ini file locations. Use this instead of calling `php_ini_loaded_file` and `php_ini_scanned_files`, which will report the wrong values in a restarted process. + +```php +use Composer\XdebugHandler\XdebugHandler; + +$files = XdebugHandler::getAllIniFiles(); + +# $files[0] always exists, it could be an empty string +$loadedIni = array_shift($files); +$scannedInis = $files; +``` + +These locations are also available in the `MYAPP_ORIGINAL_INIS` environment variable. This is a path-separated string comprising the location returned from `php_ini_loaded_file`, which could be empty, followed by locations parsed from calling `php_ini_scanned_files`. + +#### _getRestartSettings()_ +Returns an array of settings that can be used with PHP [sub-processes](#sub-processes), or null if the process was not restarted. + +```php +use Composer\XdebugHandler\XdebugHandler; + +$settings = XdebugHandler::getRestartSettings(); +/** + * $settings: array (if the current process was restarted, + * or called with the settings from a previous restart), or null + * + * 'tmpIni' => the temporary ini file used in the restart (string) + * 'scannedInis' => if there were any scanned inis (bool) + * 'scanDir' => the original PHP_INI_SCAN_DIR value (false|string) + * 'phprc' => the original PHPRC value (false|string) + * 'inis' => the original inis from getAllIniFiles (array) + * 'skipped' => the skipped version from getSkippedVersion (string) + */ +``` + +#### _getSkippedVersion()_ +Returns the Xdebug version string that was skipped by the restart, or an empty value if there was no restart (or Xdebug is still loaded, perhaps by an extending class restarting for a reason other than removing Xdebug). + +```php +use Composer\XdebugHandler\XdebugHandler; + +$version = XdebugHandler::getSkippedVersion(); +# $version: '2.6.0' (for example), or an empty string +``` + +### Setter methods +These methods implement a fluent interface and must be called before the main `check()` method. + +#### _setLogger($logger)_ +Enables the output of status messages to an external PSR3 logger. All messages are reported with either `DEBUG` or `WARNING` log levels. For example (showing the level and message): + +``` +// Restart overridden +DEBUG Checking MYAPP_ALLOW_XDEBUG +DEBUG The Xdebug extension is loaded (2.5.0) +DEBUG No restart (MYAPP_ALLOW_XDEBUG=1) + +// Failed restart +DEBUG Checking MYAPP_ALLOW_XDEBUG +DEBUG The Xdebug extension is loaded (2.5.0) +WARNING No restart (Unable to create temp ini file at: ...) +``` + +Status messages can also be output with `XDEBUG_HANDLER_DEBUG`. See [Troubleshooting](#troubleshooting). + +#### _setMainScript($script)_ +Sets the location of the main script to run in the restart. This is only needed in more esoteric use-cases, or if the `argv[0]` location is inaccessible. The script name `--` is supported for standard input. + +#### _setPersistent()_ +Configures the restart using [persistent settings](#persistent-settings), so that Xdebug is not loaded in any sub-process. + +Use this method if your application invokes one or more PHP sub-process and the Xdebug extension is not needed. This avoids the overhead of implementing specific [sub-process](#sub-processes) strategies. + +Alternatively, this method can be used to set up a default _Xdebug-free_ environment which can be changed if a sub-process requires Xdebug, then restored afterwards: + +```php +function SubProcessWithXdebug() +{ + $phpConfig = new Composer\XdebugHandler\PhpConfig(); + + # Set the environment to the original configuration + $phpConfig->useOriginal(); + + # run the process with Xdebug loaded + ... + + # Restore Xdebug-free environment + $phpConfig->usePersistent(); +} +``` + +### Process configuration +The library offers two strategies to invoke a new PHP process without loading Xdebug, using either _standard_ or _persistent_ settings. Note that this is only important if the application calls a PHP sub-process. + +#### Standard settings +Uses command-line options to remove Xdebug from the new process only. + +* The -n option is added to the command-line. This tells PHP not to scan for additional inis. +* The temporary ini is added to the command-line with the -c option. + +>_If the new process calls a PHP sub-process, Xdebug will be loaded in that sub-process (unless it implements xdebug-handler, in which case there will be another restart)._ + +This is the default strategy used in the restart. + +#### Persistent settings +Uses environment variables to remove Xdebug from the new process and persist these settings to any sub-process. + +* `PHP_INI_SCAN_DIR` is set to an empty string. This tells PHP not to scan for additional inis. +* `PHPRC` is set to the temporary ini. + +>_If the new process calls a PHP sub-process, Xdebug will not be loaded in that sub-process._ + +This strategy can be used in the restart by calling [setPersistent()](#setpersistent). + +#### Sub-processes +The `PhpConfig` helper class makes it easy to invoke a PHP sub-process (with or without Xdebug loaded), regardless of whether there has been a restart. + +Each of its methods returns an array of PHP options (to add to the command-line) and sets up the environment for the required strategy. The [getRestartSettings()](#getrestartsettings) method is used internally. + +* `useOriginal()` - Xdebug will be loaded in the new process. +* `useStandard()` - Xdebug will **not** be loaded in the new process - see [standard settings](#standard-settings). +* `userPersistent()` - Xdebug will **not** be loaded in the new process - see [persistent settings](#persistent-settings) + +If there was no restart, an empty options array is returned and the environment is not changed. + +```php +use Composer\XdebugHandler\PhpConfig; + +$config = new PhpConfig; + +$options = $config->useOriginal(); +# $options: empty array +# environment: PHPRC and PHP_INI_SCAN_DIR set to original values + +$options = $config->useStandard(); +# $options: [-n, -c, tmpIni] +# environment: PHPRC and PHP_INI_SCAN_DIR set to original values + +$options = $config->usePersistent(); +# $options: empty array +# environment: PHPRC=tmpIni, PHP_INI_SCAN_DIR='' +``` + +### Troubleshooting +The following environment settings can be used to troubleshoot unexpected behavior: + +* `XDEBUG_HANDLER_DEBUG=1` Outputs status messages to `STDERR`, if it is defined, irrespective of any PSR3 logger. Each message is prefixed `xdebug-handler[pid]`, where pid is the process identifier. + +* `XDEBUG_HANDLER_DEBUG=2` As above, but additionally saves the temporary ini file and reports its location in a status message. + +### Extending the library +The API is defined by classes and their accessible elements that are not annotated as @internal. The main class has two protected methods that can be overridden to provide additional functionality: + +#### _requiresRestart($isLoaded)_ +By default the process will restart if Xdebug is loaded. Extending this method allows an application to decide, by returning a boolean (or equivalent) value. It is only called if `MYAPP_ALLOW_XDEBUG` is empty, so it will not be called in the restarted process (where this variable contains internal data), or if the restart has been overridden. + +Note that the [setMainScript()](#setmainscriptscript) and [setPersistent()](#setpersistent) setters can be used here, if required. + +#### _restart($command)_ +An application can extend this to modify the temporary ini file, its location given in the `tmpIni` property. New settings can be safely appended to the end of the data, which is `PHP_EOL` terminated. + +Note that the `$command` parameter is the escaped command-line string that will be used for the new process and must be treated accordingly. + +Remember to finish with `parent::restart($command)`. + +#### Example +This example demonstrates two ways to extend basic functionality: + +* To avoid the overhead of spinning up a new process, the restart is skipped if a simple help command is requested. + +* The application needs write-access to phar files, so it will force a restart if `phar.readonly` is set (regardless of whether Xdebug is loaded) and change this value in the temporary ini file. + +```php +use Composer\XdebugHandler\XdebugHandler; +use MyApp\Command; + +class MyRestarter extends XdebugHandler +{ + private $required; + + protected function requiresRestart($isLoaded) + { + if (Command::isHelp()) { + # No need to disable Xdebug for this + return false; + } + + $this->required = (bool) ini_get('phar.readonly'); + return $isLoaded || $this->required; + } + + protected function restart($command) + { + if ($this->required) { + # Add required ini setting to tmpIni + $content = file_get_contents($this->tmpIni); + $content .= 'phar.readonly=0'.PHP_EOL; + file_put_contents($this->tmpIni, $content); + } + + parent::restart($command); + } +} +``` + +## License +composer/xdebug-handler is licensed under the MIT License, see the LICENSE file for details. diff --git a/bundled-libs/composer/xdebug-handler/composer.json b/bundled-libs/composer/xdebug-handler/composer.json new file mode 100644 index 000000000..7df9ea649 --- /dev/null +++ b/bundled-libs/composer/xdebug-handler/composer.json @@ -0,0 +1,42 @@ +{ + "name": "composer/xdebug-handler", + "description": "Restarts a process without Xdebug.", + "type": "library", + "license": "MIT", + "keywords": [ + "xdebug", + "performance" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.2 || ^5", + "phpstan/phpstan": "^0.12.55" + }, + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Composer\\XdebugHandler\\": "tests" + } + }, + "scripts": { + "test": "SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1 vendor/bin/simple-phpunit", + "phpstan": "vendor/bin/phpstan analyse" + } +} diff --git a/bundled-libs/composer/xdebug-handler/phpstan.neon.dist b/bundled-libs/composer/xdebug-handler/phpstan.neon.dist new file mode 100644 index 000000000..46b0f4776 --- /dev/null +++ b/bundled-libs/composer/xdebug-handler/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + level: 5 + paths: + - src + - tests diff --git a/bundled-libs/composer/xdebug-handler/src/PhpConfig.php b/bundled-libs/composer/xdebug-handler/src/PhpConfig.php new file mode 100644 index 000000000..5535eca56 --- /dev/null +++ b/bundled-libs/composer/xdebug-handler/src/PhpConfig.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler; + +/** + * @author John Stevenson + */ +class PhpConfig +{ + /** + * Use the original PHP configuration + * + * @return array PHP cli options + */ + public function useOriginal() + { + $this->getDataAndReset(); + return array(); + } + + /** + * Use standard restart settings + * + * @return array PHP cli options + */ + public function useStandard() + { + if ($data = $this->getDataAndReset()) { + return array('-n', '-c', $data['tmpIni']); + } + + return array(); + } + + /** + * Use environment variables to persist settings + * + * @return array PHP cli options + */ + public function usePersistent() + { + if ($data = $this->getDataAndReset()) { + Process::setEnv('PHPRC', $data['tmpIni']); + Process::setEnv('PHP_INI_SCAN_DIR', ''); + } + + return array(); + } + + /** + * Returns restart data if available and resets the environment + * + * @return array|null + */ + private function getDataAndReset() + { + if ($data = XdebugHandler::getRestartSettings()) { + Process::setEnv('PHPRC', $data['phprc']); + Process::setEnv('PHP_INI_SCAN_DIR', $data['scanDir']); + } + + return $data; + } +} diff --git a/bundled-libs/composer/xdebug-handler/src/Process.php b/bundled-libs/composer/xdebug-handler/src/Process.php new file mode 100644 index 000000000..eb2ad2b4e --- /dev/null +++ b/bundled-libs/composer/xdebug-handler/src/Process.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler; + +/** + * Provides utility functions to prepare a child process command-line and set + * environment variables in that process. + * + * @author John Stevenson + * @internal + */ +class Process +{ + /** + * Returns an array of parameters, including a color option if required + * + * A color option is needed because child process output is piped. + * + * @param array $args The script parameters + * @param string $colorOption The long option to force color output + * + * @return array + */ + public static function addColorOption(array $args, $colorOption) + { + if (!$colorOption + || in_array($colorOption, $args) + || !preg_match('/^--([a-z]+$)|(^--[a-z]+=)/', $colorOption, $matches)) { + return $args; + } + + if (isset($matches[2])) { + // Handle --color(s)= options + if (false !== ($index = array_search($matches[2].'auto', $args))) { + $args[$index] = $colorOption; + return $args; + } elseif (preg_grep('/^'.$matches[2].'/', $args)) { + return $args; + } + } elseif (in_array('--no-'.$matches[1], $args)) { + return $args; + } + + // Check for NO_COLOR variable (https://no-color.org/) + if (false !== getenv('NO_COLOR')) { + return $args; + } + + if (false !== ($index = array_search('--', $args))) { + // Position option before double-dash delimiter + array_splice($args, $index, 0, $colorOption); + } else { + $args[] = $colorOption; + } + + return $args; + } + + /** + * Escapes a string to be used as a shell argument. + * + * From https://github.com/johnstevenson/winbox-args + * MIT Licensed (c) John Stevenson + * + * @param string $arg The argument to be escaped + * @param bool $meta Additionally escape cmd.exe meta characters + * @param bool $module The argument is the module to invoke + * + * @return string The escaped argument + */ + public static function escape($arg, $meta = true, $module = false) + { + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + return "'".str_replace("'", "'\\''", $arg)."'"; + } + + $quote = strpbrk($arg, " \t") !== false || $arg === ''; + + $arg = preg_replace('/(\\\\*)"/', '$1$1\\"', $arg, -1, $dquotes); + + if ($meta) { + $meta = $dquotes || preg_match('/%[^%]+%/', $arg); + + if (!$meta) { + $quote = $quote || strpbrk($arg, '^&|<>()') !== false; + } elseif ($module && !$dquotes && $quote) { + $meta = false; + } + } + + if ($quote) { + $arg = '"'.preg_replace('/(\\\\*)$/', '$1$1', $arg).'"'; + } + + if ($meta) { + $arg = preg_replace('/(["^&|<>()%])/', '^$1', $arg); + } + + return $arg; + } + + /** + * Returns true if the output stream supports colors + * + * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo + * terminals via named pipes, so we can only check the environment. + * + * @param mixed $output A valid CLI output stream + * + * @return bool + */ + public static function supportsColor($output) + { + if ('Hyper' === getenv('TERM_PROGRAM')) { + return true; + } + + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + return (function_exists('sapi_windows_vt100_support') + && sapi_windows_vt100_support($output)) + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); + } + + if (function_exists('stream_isatty')) { + return stream_isatty($output); + } + + if (function_exists('posix_isatty')) { + return posix_isatty($output); + } + + $stat = fstat($output); + // Check if formatted mode is S_IFCHR + return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; + } + + /** + * Makes putenv environment changes available in $_SERVER and $_ENV + * + * @param string $name + * @param string|false $value A false value unsets the variable + * + * @return bool Whether the environment variable was set + */ + public static function setEnv($name, $value = false) + { + $unset = false === $value; + + if (!putenv($unset ? $name : $name.'='.$value)) { + return false; + } + + if ($unset) { + unset($_SERVER[$name]); + } else { + $_SERVER[$name] = $value; + } + + // Update $_ENV if it is being used + if (false !== stripos((string) ini_get('variables_order'), 'E')) { + if ($unset) { + unset($_ENV[$name]); + } else { + $_ENV[$name] = $value; + } + } + + return true; + } +} diff --git a/bundled-libs/composer/xdebug-handler/src/Status.php b/bundled-libs/composer/xdebug-handler/src/Status.php new file mode 100644 index 000000000..e714b1c2c --- /dev/null +++ b/bundled-libs/composer/xdebug-handler/src/Status.php @@ -0,0 +1,163 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; + +/** + * @author John Stevenson + * @internal + */ +class Status +{ + const ENV_RESTART = 'XDEBUG_HANDLER_RESTART'; + const CHECK = 'Check'; + const ERROR = 'Error'; + const INFO = 'Info'; + const NORESTART = 'NoRestart'; + const RESTART = 'Restart'; + const RESTARTING = 'Restarting'; + const RESTARTED = 'Restarted'; + + private $debug; + private $envAllowXdebug; + private $loaded; + private $logger; + private $time; + + /** + * Constructor + * + * @param string $envAllowXdebug Prefixed _ALLOW_XDEBUG name + * @param bool $debug Whether debug output is required + */ + public function __construct($envAllowXdebug, $debug) + { + $start = getenv(self::ENV_RESTART); + Process::setEnv(self::ENV_RESTART); + $this->time = $start ? round((microtime(true) - $start) * 1000) : 0; + + $this->envAllowXdebug = $envAllowXdebug; + $this->debug = $debug && defined('STDERR'); + } + + /** + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * Calls a handler method to report a message + * + * @param string $op The handler constant + * @param null|string $data Data required by the handler + */ + public function report($op, $data) + { + if ($this->logger || $this->debug) { + call_user_func(array($this, 'report'.$op), $data); + } + } + + /** + * Outputs a status message + * + * @param string $text + * @param string $level + */ + private function output($text, $level = null) + { + if ($this->logger) { + $this->logger->log($level ?: LogLevel::DEBUG, $text); + } + + if ($this->debug) { + fwrite(STDERR, sprintf('xdebug-handler[%d] %s', getmypid(), $text.PHP_EOL)); + } + } + + private function reportCheck($loaded) + { + $this->loaded = $loaded; + $this->output('Checking '.$this->envAllowXdebug); + } + + private function reportError($error) + { + $this->output(sprintf('No restart (%s)', $error), LogLevel::WARNING); + } + + private function reportInfo($info) + { + $this->output($info); + } + + private function reportNoRestart() + { + $this->output($this->getLoadedMessage()); + + if ($this->loaded) { + $text = sprintf('No restart (%s)', $this->getEnvAllow()); + if (!getenv($this->envAllowXdebug)) { + $text .= ' Allowed by application'; + } + $this->output($text); + } + } + + private function reportRestart() + { + $this->output($this->getLoadedMessage()); + Process::setEnv(self::ENV_RESTART, (string) microtime(true)); + } + + private function reportRestarted() + { + $loaded = $this->getLoadedMessage(); + $text = sprintf('Restarted (%d ms). %s', $this->time, $loaded); + $level = $this->loaded ? LogLevel::WARNING : null; + $this->output($text, $level); + } + + private function reportRestarting($command) + { + $text = sprintf('Process restarting (%s)', $this->getEnvAllow()); + $this->output($text); + $text = 'Running '.$command; + $this->output($text); + } + + /** + * Returns the _ALLOW_XDEBUG environment variable as name=value + * + * @return string + */ + private function getEnvAllow() + { + return $this->envAllowXdebug.'='.getenv($this->envAllowXdebug); + } + + /** + * Returns the Xdebug status and version + * + * @return string + */ + private function getLoadedMessage() + { + $loaded = $this->loaded ? sprintf('loaded (%s)', $this->loaded) : 'not loaded'; + return 'The Xdebug extension is '.$loaded; + } +} diff --git a/bundled-libs/composer/xdebug-handler/src/XdebugHandler.php b/bundled-libs/composer/xdebug-handler/src/XdebugHandler.php new file mode 100644 index 000000000..30391f096 --- /dev/null +++ b/bundled-libs/composer/xdebug-handler/src/XdebugHandler.php @@ -0,0 +1,620 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler; + +use Psr\Log\LoggerInterface; + +/** + * @author John Stevenson + */ +class XdebugHandler +{ + const SUFFIX_ALLOW = '_ALLOW_XDEBUG'; + const SUFFIX_INIS = '_ORIGINAL_INIS'; + const RESTART_ID = 'internal'; + const RESTART_SETTINGS = 'XDEBUG_HANDLER_SETTINGS'; + const DEBUG = 'XDEBUG_HANDLER_DEBUG'; + + /** @var string|null */ + protected $tmpIni; + + private static $inRestart; + private static $name; + private static $skipped; + + private $cli; + private $colorOption; + private $debug; + private $envAllowXdebug; + private $envOriginalInis; + private $loaded; + private $persistent; + private $script; + /** @var Status|null */ + private $statusWriter; + + /** + * Constructor + * + * The $envPrefix is used to create distinct environment variables. It is + * uppercased and prepended to the default base values. For example 'myapp' + * would result in MYAPP_ALLOW_XDEBUG and MYAPP_ORIGINAL_INIS. + * + * @param string $envPrefix Value used in environment variables + * @param string $colorOption Command-line long option to force color output + * @throws \RuntimeException If a parameter is invalid + */ + public function __construct($envPrefix, $colorOption = '') + { + if (!is_string($envPrefix) || empty($envPrefix) || !is_string($colorOption)) { + throw new \RuntimeException('Invalid constructor parameter'); + } + + self::$name = strtoupper($envPrefix); + $this->envAllowXdebug = self::$name.self::SUFFIX_ALLOW; + $this->envOriginalInis = self::$name.self::SUFFIX_INIS; + + $this->colorOption = $colorOption; + + if (extension_loaded('xdebug')) { + $ext = new \ReflectionExtension('xdebug'); + $this->loaded = $ext->getVersion() ?: 'unknown'; + } + + if ($this->cli = PHP_SAPI === 'cli') { + $this->debug = getenv(self::DEBUG); + } + + $this->statusWriter = new Status($this->envAllowXdebug, (bool) $this->debug); + } + + /** + * Activates status message output to a PSR3 logger + * + * @param LoggerInterface $logger + * + * @return $this + */ + public function setLogger(LoggerInterface $logger) + { + $this->statusWriter->setLogger($logger); + return $this; + } + + /** + * Sets the main script location if it cannot be called from argv + * + * @param string $script + * + * @return $this + */ + public function setMainScript($script) + { + $this->script = $script; + return $this; + } + + /** + * Persist the settings to keep Xdebug out of sub-processes + * + * @return $this + */ + public function setPersistent() + { + $this->persistent = true; + return $this; + } + + /** + * Checks if Xdebug is loaded and the process needs to be restarted + * + * This behaviour can be disabled by setting the MYAPP_ALLOW_XDEBUG + * environment variable to 1. This variable is used internally so that + * the restarted process is created only once. + */ + public function check() + { + $this->notify(Status::CHECK, $this->loaded); + $envArgs = explode('|', (string) getenv($this->envAllowXdebug)); + + if (empty($envArgs[0]) && $this->requiresRestart((bool) $this->loaded)) { + // Restart required + $this->notify(Status::RESTART); + + if ($this->prepareRestart()) { + $command = $this->getCommand(); + $this->restart($command); + } + return; + } + + if (self::RESTART_ID === $envArgs[0] && count($envArgs) === 5) { + // Restarted, so unset environment variable and use saved values + $this->notify(Status::RESTARTED); + + Process::setEnv($this->envAllowXdebug); + self::$inRestart = true; + + if (!$this->loaded) { + // Skipped version is only set if Xdebug is not loaded + self::$skipped = $envArgs[1]; + } + + $this->tryEnableSignals(); + + // Put restart settings in the environment + $this->setEnvRestartSettings($envArgs); + return; + } + + $this->notify(Status::NORESTART); + + if ($settings = self::getRestartSettings()) { + // Called with existing settings, so sync our settings + $this->syncSettings($settings); + } + } + + /** + * Returns an array of php.ini locations with at least one entry + * + * The equivalent of calling php_ini_loaded_file then php_ini_scanned_files. + * The loaded ini location is the first entry and may be empty. + * + * @return array + */ + public static function getAllIniFiles() + { + if (!empty(self::$name)) { + $env = getenv(self::$name.self::SUFFIX_INIS); + + if (false !== $env) { + return explode(PATH_SEPARATOR, $env); + } + } + + $paths = array((string) php_ini_loaded_file()); + + if ($scanned = php_ini_scanned_files()) { + $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); + } + + return $paths; + } + + /** + * Returns an array of restart settings or null + * + * Settings will be available if the current process was restarted, or + * called with the settings from an existing restart. + * + * @return array|null + */ + public static function getRestartSettings() + { + $envArgs = explode('|', (string) getenv(self::RESTART_SETTINGS)); + + if (count($envArgs) !== 6 + || (!self::$inRestart && php_ini_loaded_file() !== $envArgs[0])) { + return null; + } + + return array( + 'tmpIni' => $envArgs[0], + 'scannedInis' => (bool) $envArgs[1], + 'scanDir' => '*' === $envArgs[2] ? false : $envArgs[2], + 'phprc' => '*' === $envArgs[3] ? false : $envArgs[3], + 'inis' => explode(PATH_SEPARATOR, $envArgs[4]), + 'skipped' => $envArgs[5], + ); + } + + /** + * Returns the Xdebug version that triggered a successful restart + * + * @return string + */ + public static function getSkippedVersion() + { + return (string) self::$skipped; + } + + /** + * Returns true if Xdebug is loaded, or as directed by an extending class + * + * @param bool $isLoaded Whether Xdebug is loaded + * + * @return bool + */ + protected function requiresRestart($isLoaded) + { + return $isLoaded; + } + + /** + * Allows an extending class to access the tmpIni + * + * @param string $command + */ + protected function restart($command) + { + $this->doRestart($command); + } + + /** + * Executes the restarted command then deletes the tmp ini + * + * @param string $command + */ + private function doRestart($command) + { + $this->tryEnableSignals(); + $this->notify(Status::RESTARTING, $command); + + // Prefer proc_open to keep fds intact, because passthru pipes to stdout + if (function_exists('proc_open')) { + if (defined('PHP_WINDOWS_VERSION_BUILD') && PHP_VERSION_ID < 80000) { + $command = '"'.$command.'"'; + } + $process = proc_open($command, array(), $pipes); + if (is_resource($process)) { + $exitCode = proc_close($process); + } + } else { + passthru($command, $exitCode); + } + + if (!isset($exitCode)) { + // Unlikely that the default shell cannot be invoked + $this->notify(Status::ERROR, 'Unable to restart process'); + $exitCode = -1; + } else { + $this->notify(Status::INFO, 'Restarted process exited '.$exitCode); + } + + if ($this->debug === '2') { + $this->notify(Status::INFO, 'Temp ini saved: '.$this->tmpIni); + } else { + @unlink($this->tmpIni); + } + + exit($exitCode); + } + + /** + * Returns true if everything was written for the restart + * + * If any of the following fails (however unlikely) we must return false to + * stop potential recursion: + * - tmp ini file creation + * - environment variable creation + * + * @return bool + */ + private function prepareRestart() + { + $error = ''; + $iniFiles = self::getAllIniFiles(); + $scannedInis = count($iniFiles) > 1; + $tmpDir = sys_get_temp_dir(); + + if (!$this->cli) { + $error = 'Unsupported SAPI: '.PHP_SAPI; + } elseif (!defined('PHP_BINARY')) { + $error = 'PHP version is too old: '.PHP_VERSION; + } elseif (!$this->checkConfiguration($info)) { + $error = $info; + } elseif (!$this->checkScanDirConfig()) { + $error = 'PHP version does not report scanned inis: '.PHP_VERSION; + } elseif (!$this->checkMainScript()) { + $error = 'Unable to access main script: '.$this->script; + } elseif (!$this->writeTmpIni($iniFiles, $tmpDir, $error)) { + $error = $error ?: 'Unable to create temp ini file at: '.$tmpDir; + } elseif (!$this->setEnvironment($scannedInis, $iniFiles)) { + $error = 'Unable to set environment variables'; + } + + if ($error) { + $this->notify(Status::ERROR, $error); + } + + return empty($error); + } + + /** + * Returns true if the tmp ini file was written + * + * @param array $iniFiles All ini files used in the current process + * @param string $tmpDir The system temporary directory + * @param string $error Set by method if ini file cannot be read + * + * @return bool + */ + private function writeTmpIni(array $iniFiles, $tmpDir, &$error) + { + if (!$this->tmpIni = @tempnam($tmpDir, '')) { + return false; + } + + // $iniFiles has at least one item and it may be empty + if (empty($iniFiles[0])) { + array_shift($iniFiles); + } + + $content = ''; + $regex = '/^\s*(zend_extension\s*=.*xdebug.*)$/mi'; + + foreach ($iniFiles as $file) { + // Check for inaccessible ini files + if (($data = @file_get_contents($file)) === false) { + $error = 'Unable to read ini: '.$file; + return false; + } + $content .= preg_replace($regex, ';$1', $data).PHP_EOL; + } + + // Merge loaded settings into our ini content, if it is valid + if ($config = parse_ini_string($content)) { + $loaded = ini_get_all(null, false); + $content .= $this->mergeLoadedConfig($loaded, $config); + } + + // Work-around for https://bugs.php.net/bug.php?id=75932 + $content .= 'opcache.enable_cli=0'.PHP_EOL; + + return @file_put_contents($this->tmpIni, $content); + } + + /** + * Returns the restart command line + * + * @return string + */ + private function getCommand() + { + $php = array(PHP_BINARY); + $args = array_slice($_SERVER['argv'], 1); + + if (!$this->persistent) { + // Use command-line options + array_push($php, '-n', '-c', $this->tmpIni); + } + + if (defined('STDOUT') && Process::supportsColor(STDOUT)) { + $args = Process::addColorOption($args, $this->colorOption); + } + + $args = array_merge($php, array($this->script), $args); + + $cmd = Process::escape(array_shift($args), true, true); + foreach ($args as $arg) { + $cmd .= ' '.Process::escape($arg); + } + + return $cmd; + } + + /** + * Returns true if the restart environment variables were set + * + * No need to update $_SERVER since this is set in the restarted process. + * + * @param bool $scannedInis Whether there were scanned ini files + * @param array $iniFiles All ini files used in the current process + * + * @return bool + */ + private function setEnvironment($scannedInis, array $iniFiles) + { + $scanDir = getenv('PHP_INI_SCAN_DIR'); + $phprc = getenv('PHPRC'); + + // Make original inis available to restarted process + if (!putenv($this->envOriginalInis.'='.implode(PATH_SEPARATOR, $iniFiles))) { + return false; + } + + if ($this->persistent) { + // Use the environment to persist the settings + if (!putenv('PHP_INI_SCAN_DIR=') || !putenv('PHPRC='.$this->tmpIni)) { + return false; + } + } + + // Flag restarted process and save values for it to use + $envArgs = array( + self::RESTART_ID, + $this->loaded, + (int) $scannedInis, + false === $scanDir ? '*' : $scanDir, + false === $phprc ? '*' : $phprc, + ); + + return putenv($this->envAllowXdebug.'='.implode('|', $envArgs)); + } + + /** + * Logs status messages + * + * @param string $op Status handler constant + * @param null|string $data Optional data + */ + private function notify($op, $data = null) + { + $this->statusWriter->report($op, $data); + } + + /** + * Returns default, changed and command-line ini settings + * + * @param array $loadedConfig All current ini settings + * @param array $iniConfig Settings from user ini files + * + * @return string + */ + private function mergeLoadedConfig(array $loadedConfig, array $iniConfig) + { + $content = ''; + + foreach ($loadedConfig as $name => $value) { + // Value will either be null, string or array (HHVM only) + if (!is_string($value) + || strpos($name, 'xdebug') === 0 + || $name === 'apc.mmap_file_mask') { + continue; + } + + if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) { + // Double-quote escape each value + $content .= $name.'="'.addcslashes($value, '\\"').'"'.PHP_EOL; + } + } + + return $content; + } + + /** + * Returns true if the script name can be used + * + * @return bool + */ + private function checkMainScript() + { + if (null !== $this->script) { + // Allow an application to set -- for standard input + return file_exists($this->script) || '--' === $this->script; + } + + if (file_exists($this->script = $_SERVER['argv'][0])) { + return true; + } + + // Use a backtrace to resolve Phar and chdir issues + $options = PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : false; + $trace = debug_backtrace($options); + + if (($main = end($trace)) && isset($main['file'])) { + return file_exists($this->script = $main['file']); + } + + return false; + } + + /** + * Adds restart settings to the environment + * + * @param string[] $envArgs + */ + private function setEnvRestartSettings($envArgs) + { + $settings = array( + php_ini_loaded_file(), + $envArgs[2], + $envArgs[3], + $envArgs[4], + getenv($this->envOriginalInis), + self::$skipped, + ); + + Process::setEnv(self::RESTART_SETTINGS, implode('|', $settings)); + } + + /** + * Syncs settings and the environment if called with existing settings + * + * @param array $settings + */ + private function syncSettings(array $settings) + { + if (false === getenv($this->envOriginalInis)) { + // Called by another app, so make original inis available + Process::setEnv($this->envOriginalInis, implode(PATH_SEPARATOR, $settings['inis'])); + } + + self::$skipped = $settings['skipped']; + $this->notify(Status::INFO, 'Process called with existing restart settings'); + } + + /** + * Returns true if there are scanned inis and PHP is able to report them + * + * php_ini_scanned_files will fail when PHP_CONFIG_FILE_SCAN_DIR is empty. + * Fixed in 7.1.13 and 7.2.1 + * + * @return bool + */ + private function checkScanDirConfig() + { + return !(getenv('PHP_INI_SCAN_DIR') + && !PHP_CONFIG_FILE_SCAN_DIR + && (PHP_VERSION_ID < 70113 + || PHP_VERSION_ID === 70200)); + } + + /** + * Returns true if there are no known configuration issues + * + * @param string $info Set by method + * @return bool + */ + private function checkConfiguration(&$info) + { + if (!function_exists('proc_open') && !function_exists('passthru')) { + $info = 'execution functions have been disabled (proc_open or passthru required)'; + return false; + } + + if (extension_loaded('uopz') && !ini_get('uopz.disable')) { + // uopz works at opcode level and disables exit calls + if (function_exists('uopz_allow_exit')) { + @uopz_allow_exit(true); + } else { + $info = 'uopz extension is not compatible'; + return false; + } + } + + return true; + } + + /** + * Enables async signals and control interrupts in the restarted process + * + * Available on Unix PHP 7.1+ with the pcntl extension and Windows PHP 7.4+. + */ + private function tryEnableSignals() + { + if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { + pcntl_async_signals(true); + $message = 'Async signals enabled'; + + if (!self::$inRestart) { + // Restarting, so ignore SIGINT in parent + pcntl_signal(SIGINT, SIG_IGN); + $message .= ' (SIGINT = SIG_IGN)'; + } elseif (is_int(pcntl_signal_get_handler(SIGINT))) { + // Restarted, no handler set so force default action + pcntl_signal(SIGINT, SIG_DFL); + $message .= ' (SIGINT = SIG_DFL)'; + } + $this->notify(Status::INFO, $message); + } + + if (!self::$inRestart && function_exists('sapi_windows_set_ctrl_handler')) { + // Restarting, so set a handler to ignore CTRL events in the parent. + // This ensures that CTRL+C events will be available in the child + // process without having to enable them there, which is unreliable. + sapi_windows_set_ctrl_handler(function ($evt) {}); + $this->notify(Status::INFO, 'CTRL signals suppressed'); + } + } +} diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/.travis.yml b/bundled-libs/felixfbecker/advanced-json-rpc/.travis.yml new file mode 100644 index 000000000..989cccb89 --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/.travis.yml @@ -0,0 +1,56 @@ +# TravisCI configuration for felixfbecker/php-advanced-json-rpc + +branches: + except: + - /^v\d+\.\d+\.\d+$/ + +language: php +os: + - "linux" +dist: "xenial" + +php: + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "8.0" + - nightly + +stages: + - name: test + - name: release + if: branch = master AND type = push AND fork = false + +jobs: + fast_finish: true + include: + - stage: release + language: node_js + node_js: "8" + install: + - npm ci + script: + - npm run semantic-release + after_success: skip + allow_failures: + - stage: test + php: nightly + +env: + global: + - FORCE_COLOR=1 + +cache: + npm: true + directories: + - $HOME/.composer/cache + +install: + - composer install --prefer-dist + +script: + - vendor/bin/phpunit --coverage-clover=coverage.xml --whitelist lib --bootstrap vendor/autoload.php tests + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/LICENSE b/bundled-libs/felixfbecker/advanced-json-rpc/LICENSE new file mode 100644 index 000000000..fc354170c --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2016, Felix Frederick Becker + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/composer.json b/bundled-libs/felixfbecker/advanced-json-rpc/composer.json new file mode 100644 index 000000000..1c154df1c --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/composer.json @@ -0,0 +1,32 @@ +{ + "name": "felixfbecker/advanced-json-rpc", + "description": "A more advanced JSONRPC implementation", + "type": "library", + "license": "ISC", + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "autoload-dev": { + "psr-4": { + "AdvancedJsonRpc\\Tests\\": "tests/" + } + }, + "require": { + "php": "^7.1 || ^8.0", + "netresearch/jsonmapper": "^1.0 || ^2.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/lib/Dispatcher.php b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Dispatcher.php new file mode 100644 index 000000000..5f045df62 --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Dispatcher.php @@ -0,0 +1,171 @@ + ReflectionMethod[] + * + * @var ReflectionMethod + */ + private $methods; + + /** + * @var \phpDocumentor\Reflection\DocBlockFactory + */ + private $docBlockFactory; + + /** + * @var \phpDocumentor\Reflection\Types\ContextFactory + */ + private $contextFactory; + + /** + * @param object $target The target object that should receive the method calls + * @param string $delimiter A delimiter for method calls on properties, for example someProperty->someMethod + */ + public function __construct($target, $delimiter = '->') + { + $this->target = $target; + $this->delimiter = $delimiter; + $this->docBlockFactory = DocBlockFactory::createInstance(); + $this->contextFactory = new Types\ContextFactory(); + $this->mapper = new JsonMapper(); + } + + /** + * Calls the appropriate method handler for an incoming Message + * + * @param string|object $msg The incoming message + * @return mixed + */ + public function dispatch($msg) + { + if (is_string($msg)) { + $msg = json_decode($msg); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Error(json_last_error_msg(), ErrorCode::PARSE_ERROR); + } + } + // Find out the object and function that should be called + $obj = $this->target; + $parts = explode($this->delimiter, $msg->method); + // The function to call is always the last part of the method + $fn = array_pop($parts); + // For namespaced methods like textDocument/didOpen, call the didOpen method on the $textDocument property + // For simple methods like initialize, shutdown, exit, this loop will simply not be entered and $obj will be + // the target + foreach ($parts as $part) { + if (!isset($obj->$part)) { + throw new Error("Method {$msg->method} is not implemented", ErrorCode::METHOD_NOT_FOUND); + } + $obj = $obj->$part; + } + if (!isset($this->methods[$msg->method])) { + try { + $method = new ReflectionMethod($obj, $fn); + $this->methods[$msg->method] = $method; + } catch (ReflectionException $e) { + throw new Error($e->getMessage(), ErrorCode::METHOD_NOT_FOUND, null, $e); + } + } + $method = $this->methods[$msg->method]; + $parameters = $method->getParameters(); + if ($method->getDocComment()) { + $docBlock = $this->docBlockFactory->create( + $method->getDocComment(), + $this->contextFactory->createFromReflector($method->getDeclaringClass()) + ); + $paramTags = $docBlock->getTagsByName('param'); + } + $args = []; + if (isset($msg->params)) { + // Find out the position + if (is_array($msg->params)) { + $args = $msg->params; + } else if (is_object($msg->params)) { + foreach ($parameters as $pos => $parameter) { + $value = null; + foreach(get_object_vars($msg->params) as $key => $val) { + if ($parameter->name === $key) { + $value = $val; + break; + } + } + $args[$pos] = $value; + } + } else { + throw new Error('Params must be structured or omitted', ErrorCode::INVALID_REQUEST); + } + foreach ($args as $position => $value) { + try { + // If the type is structured (array or object), map it with JsonMapper + if (is_object($value)) { + // Does the parameter have a type hint? + $param = $parameters[$position]; + if ($param->hasType()) { + $paramType = $param->getType(); + if ($paramType instanceof ReflectionNamedType) { + // We have object data to map and want the class name. + // This should not include the `?` if the type was nullable. + $class = $paramType->getName(); + } else { + // Fallback for php 7.0, which is still supported (and doesn't have nullable). + $class = (string)$paramType; + } + $value = $this->mapper->map($value, new $class()); + } + } else if (is_array($value) && isset($docBlock)) { + // Get the array type from the DocBlock + $type = $paramTags[$position]->getType(); + // For union types, use the first one that is a class array (often it is SomeClass[]|null) + if ($type instanceof Types\Compound) { + for ($i = 0; $t = $type->get($i); $i++) { + if ( + $t instanceof Types\Array_ + && $t->getValueType() instanceof Types\Object_ + && (string)$t->getValueType() !== 'object' + ) { + $class = (string)$t->getValueType()->getFqsen(); + $value = $this->mapper->mapArray($value, [], $class); + break; + } + } + } else if ($type instanceof Types\Array_) { + $class = (string)$type->getValueType()->getFqsen(); + $value = $this->mapper->mapArray($value, [], $class); + } else { + throw new Error('Type is not matching @param tag', ErrorCode::INVALID_PARAMS); + } + } + } catch (JsonMapper_Exception $e) { + throw new Error($e->getMessage(), ErrorCode::INVALID_PARAMS, null, $e); + } + $args[$position] = $value; + } + } + ksort($args); + $result = $obj->$fn(...$args); + return $result; + } +} diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/lib/Error.php b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Error.php new file mode 100644 index 000000000..b2801918b --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Error.php @@ -0,0 +1,38 @@ +data = $data; + } +} diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/lib/ErrorCode.php b/bundled-libs/felixfbecker/advanced-json-rpc/lib/ErrorCode.php new file mode 100644 index 000000000..f0ef47927 --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/lib/ErrorCode.php @@ -0,0 +1,48 @@ +id) && isset($msg->error); + } + + /** + * @param int|string $id + * @param \AdvancedJsonRpc\Error $error + */ + public function __construct($id, Error $error) + { + parent::__construct($id); + $this->error = $error; + } +} diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/lib/Message.php b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Message.php new file mode 100644 index 000000000..e2231dc53 --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Message.php @@ -0,0 +1,52 @@ +method, $decoded->params ?? null); + } else if (Request::isRequest($decoded)) { + $obj = new Request($decoded->id, $decoded->method, $decoded->params ?? null); + } else if (SuccessResponse::isSuccessResponse($decoded)) { + $obj = new SuccessResponse($decoded->id, $decoded->result); + } else if (ErrorResponse::isErrorResponse($decoded)) { + $obj = new ErrorResponse($decoded->id, new Error($decoded->error->message, $decoded->error->code, $decoded->error->data ?? null)); + } else { + throw new Error('Invalid message', ErrorCode::INVALID_REQUEST); + } + return $obj; + } + + public function __toString(): string + { + $encoded = json_encode($this); + if ($encoded === false) { + throw new Error(json_last_error_msg(), ErrorCode::INTERNAL_ERROR); + } + return $encoded; + } +} diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/lib/Notification.php b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Notification.php new file mode 100644 index 000000000..3440164d9 --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Notification.php @@ -0,0 +1,56 @@ +method); + } + + /** + * @param string $method + * @param mixed $params + */ + public function __construct(string $method, $params = null) + { + $this->method = $method; + $this->params = $params; + } +} diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/lib/Request.php b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Request.php new file mode 100644 index 000000000..142900825 --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Request.php @@ -0,0 +1,63 @@ +method); + } + + /** + * @param string|int $id + * @param string $method + * @param object|array $params + */ + public function __construct($id, string $method, $params = null) + { + $this->id = $id; + $this->method = $method; + $this->params = $params; + } +} diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/lib/Response.php b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Response.php new file mode 100644 index 000000000..a871eeac2 --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/lib/Response.php @@ -0,0 +1,40 @@ +error)); + } + + /** + * @param int|string $id + * @param mixed $result + * @param ResponseError $error + */ + public function __construct($id) + { + $this->id = $id; + } +} diff --git a/bundled-libs/felixfbecker/advanced-json-rpc/lib/SuccessResponse.php b/bundled-libs/felixfbecker/advanced-json-rpc/lib/SuccessResponse.php new file mode 100644 index 000000000..222fd46e7 --- /dev/null +++ b/bundled-libs/felixfbecker/advanced-json-rpc/lib/SuccessResponse.php @@ -0,0 +1,40 @@ +result = $result; + } +} diff --git a/bundled-libs/laminas/laminas-db/.laminas-ci.json b/bundled-libs/laminas/laminas-db/.laminas-ci.json new file mode 100644 index 000000000..cd24f8557 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/.laminas-ci.json @@ -0,0 +1,11 @@ +{ + "extensions": [ + "pdo-mysql", + "pdo-pgsql", + "pdo-sqlite", + "mysqli", + "pgsql", + "sqlite3", + "sqlsrv" + ] +} diff --git a/bundled-libs/laminas/laminas-db/.laminas-ci/mysql_permissions.sql b/bundled-libs/laminas/laminas-db/.laminas-ci/mysql_permissions.sql new file mode 100644 index 000000000..59c1c608f --- /dev/null +++ b/bundled-libs/laminas/laminas-db/.laminas-ci/mysql_permissions.sql @@ -0,0 +1,3 @@ +CREATE USER 'gha'@'%' IDENTIFIED WITH mysql_native_password BY 'password'; +GRANT ALL PRIVILEGES ON *.* TO 'gha'@'%'; +FLUSH PRIVILEGES; diff --git a/bundled-libs/laminas/laminas-db/.laminas-ci/phpunit.xml b/bundled-libs/laminas/laminas-db/.laminas-ci/phpunit.xml new file mode 100644 index 000000000..784bfc1ef --- /dev/null +++ b/bundled-libs/laminas/laminas-db/.laminas-ci/phpunit.xml @@ -0,0 +1,64 @@ + + + + + ./src + + + ./src/Sql/Ddl/Column/Float.php + + + + + + ./test/unit + + + ./test/integration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundled-libs/laminas/laminas-db/.laminas-ci/pre-run.sh b/bundled-libs/laminas/laminas-db/.laminas-ci/pre-run.sh new file mode 100755 index 000000000..660082ade --- /dev/null +++ b/bundled-libs/laminas/laminas-db/.laminas-ci/pre-run.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +TEST_USER=$1 +WORKSPACE=$2 +JOB=$3 + +COMMAND=$(echo "${JOB}" | jq -r '.command') + +if [[ ! ${COMMAND} =~ phpunit ]]; then + exit 0 +fi + +PHP_VERSION=$(echo "${JOB}" | jq -r '.php') + +# Install CI version of phpunit config +cp .laminas-ci/phpunit.xml phpunit.xml + +# Install lsof (used in integration tests) +apt update -qq +apt install -yqq lsof diff --git a/bundled-libs/laminas/laminas-db/COPYRIGHT.md b/bundled-libs/laminas/laminas-db/COPYRIGHT.md new file mode 100644 index 000000000..0a8cccc06 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/COPYRIGHT.md @@ -0,0 +1 @@ +Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/) diff --git a/bundled-libs/laminas/laminas-db/LICENSE.md b/bundled-libs/laminas/laminas-db/LICENSE.md new file mode 100644 index 000000000..10b40f142 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/LICENSE.md @@ -0,0 +1,26 @@ +Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +- Neither the name of Laminas Foundation nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/bundled-libs/laminas/laminas-db/README.md b/bundled-libs/laminas/laminas-db/README.md new file mode 100644 index 000000000..dcd250815 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/README.md @@ -0,0 +1,11 @@ +# laminas-db + +[![Build Status](https://github.com/laminas/laminas-config/workflows/Continuous%20Integration/badge.svg)](https://github.com/laminas/laminas-config/actions?query=workflow%3A"Continuous+Integration") + +`Laminas\Db` is a component that abstract the access to a Database using an object +oriented API to build the queries. `Laminas\Db` consumes different storage adapters +to access different database vendors such as MySQL, PostgreSQL, Oracle, IBM DB2, +Microsoft Sql Server, PDO, etc. + +- File issues at https://github.com/laminas/laminas-db/issues +- Documentation is at https://docs.laminas.dev/laminas-db/ diff --git a/bundled-libs/laminas/laminas-db/composer.json b/bundled-libs/laminas/laminas-db/composer.json new file mode 100644 index 000000000..02a0de6e8 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/composer.json @@ -0,0 +1,71 @@ +{ + "name": "laminas/laminas-db", + "description": "Database abstraction layer, SQL abstraction, result set abstraction, and RowDataGateway and TableDataGateway implementations", + "license": "BSD-3-Clause", + "keywords": [ + "laminas", + "db" + ], + "homepage": "https://laminas.dev", + "support": { + "docs": "https://docs.laminas.dev/laminas-db/", + "issues": "https://github.com/laminas/laminas-db/issues", + "source": "https://github.com/laminas/laminas-db", + "rss": "https://github.com/laminas/laminas-db/releases.atom", + "chat": "https://laminas.dev/chat", + "forum": "https://discourse.laminas.dev" + }, + "config": { + "sort-packages": true + }, + "extra": { + "laminas": { + "component": "Laminas\\Db", + "config-provider": "Laminas\\Db\\ConfigProvider" + } + }, + "require": { + "php": "^7.3 || ~8.0.0", + "laminas/laminas-stdlib": "^3.3", + "laminas/laminas-zendframework-bridge": "^1.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-eventmanager": "^3.3", + "laminas/laminas-hydrator": "^3.2 || ^4.0", + "laminas/laminas-servicemanager": "^3.3", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "laminas/laminas-eventmanager": "Laminas\\EventManager component", + "laminas/laminas-hydrator": "(^3.2 || ^4.0) Laminas\\Hydrator component for using HydratingResultSets", + "laminas/laminas-servicemanager": "Laminas\\ServiceManager component" + }, + "autoload": { + "psr-4": { + "Laminas\\Db\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "LaminasTest\\Db\\": "test/unit/", + "LaminasIntegrationTest\\Db\\": "test/integration/" + } + }, + "scripts": { + "check": [ + "@cs-check", + "@test" + ], + "cs-check": "phpcs", + "cs-fix": "phpcbf", + "test": "phpunit --colors=always --testsuite \"unit test\"", + "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", + "test-integration": "phpunit --colors=always --testsuite \"integration test\"", + "upload-coverage": "coveralls -v" + }, + "replace": { + "zendframework/zend-db": "^2.11.0" + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Adapter.php b/bundled-libs/laminas/laminas-db/src/Adapter/Adapter.php new file mode 100644 index 000000000..5f377f82b --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Adapter.php @@ -0,0 +1,435 @@ +createProfiler($parameters); + } + $driver = $this->createDriver($parameters); + } elseif (! $driver instanceof Driver\DriverInterface) { + throw new Exception\InvalidArgumentException( + 'The supplied or instantiated driver object does not implement ' . Driver\DriverInterface::class + ); + } + + $driver->checkEnvironment(); + $this->driver = $driver; + + if ($platform === null) { + $platform = $this->createPlatform($parameters); + } + + $this->platform = $platform; + $this->queryResultSetPrototype = ($queryResultPrototype) ?: new ResultSet\ResultSet(); + + if ($profiler) { + $this->setProfiler($profiler); + } + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->driver instanceof Profiler\ProfilerAwareInterface) { + $this->driver->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * getDriver() + * + * @throws Exception\RuntimeException + * @return Driver\DriverInterface + */ + public function getDriver() + { + if ($this->driver === null) { + throw new Exception\RuntimeException('Driver has not been set or configured for this adapter.'); + } + return $this->driver; + } + + /** + * @return Platform\PlatformInterface + */ + public function getPlatform() + { + return $this->platform; + } + + /** + * @return ResultSet\ResultSetInterface + */ + public function getQueryResultSetPrototype() + { + return $this->queryResultSetPrototype; + } + + public function getCurrentSchema() + { + return $this->driver->getConnection()->getCurrentSchema(); + } + + /** + * query() is a convenience function + * + * @param string $sql + * @param string|array|ParameterContainer $parametersOrQueryMode + * @param \Laminas\Db\ResultSet\ResultSetInterface $resultPrototype + * @throws Exception\InvalidArgumentException + * @return Driver\StatementInterface|ResultSet\ResultSet + */ + public function query( + $sql, + $parametersOrQueryMode = self::QUERY_MODE_PREPARE, + ResultSet\ResultSetInterface $resultPrototype = null + ) { + if (is_string($parametersOrQueryMode) + && in_array($parametersOrQueryMode, [self::QUERY_MODE_PREPARE, self::QUERY_MODE_EXECUTE]) + ) { + $mode = $parametersOrQueryMode; + $parameters = null; + } elseif (is_array($parametersOrQueryMode) || $parametersOrQueryMode instanceof ParameterContainer) { + $mode = self::QUERY_MODE_PREPARE; + $parameters = $parametersOrQueryMode; + } else { + throw new Exception\InvalidArgumentException( + 'Parameter 2 to this method must be a flag, an array, or ParameterContainer' + ); + } + + if ($mode == self::QUERY_MODE_PREPARE) { + $this->lastPreparedStatement = null; + $this->lastPreparedStatement = $this->driver->createStatement($sql); + $this->lastPreparedStatement->prepare(); + if (is_array($parameters) || $parameters instanceof ParameterContainer) { + if (is_array($parameters)) { + $this->lastPreparedStatement->setParameterContainer(new ParameterContainer($parameters)); + } else { + $this->lastPreparedStatement->setParameterContainer($parameters); + } + $result = $this->lastPreparedStatement->execute(); + } else { + return $this->lastPreparedStatement; + } + } else { + $result = $this->driver->getConnection()->execute($sql); + } + + if ($result instanceof Driver\ResultInterface && $result->isQueryResult()) { + $resultSet = clone ($resultPrototype ?: $this->queryResultSetPrototype); + $resultSet->initialize($result); + return $resultSet; + } + + return $result; + } + + /** + * Create statement + * + * @param string $initialSql + * @param ParameterContainer $initialParameters + * @return Driver\StatementInterface + */ + public function createStatement($initialSql = null, $initialParameters = null) + { + $statement = $this->driver->createStatement($initialSql); + if ($initialParameters === null + || ! $initialParameters instanceof ParameterContainer + && is_array($initialParameters) + ) { + $initialParameters = new ParameterContainer((is_array($initialParameters) ? $initialParameters : [])); + } + $statement->setParameterContainer($initialParameters); + return $statement; + } + + public function getHelpers() + { + $functions = []; + $platform = $this->platform; + foreach (func_get_args() as $arg) { + switch ($arg) { + case self::FUNCTION_QUOTE_IDENTIFIER: + $functions[] = function ($value) use ($platform) { + return $platform->quoteIdentifier($value); + }; + break; + case self::FUNCTION_QUOTE_VALUE: + $functions[] = function ($value) use ($platform) { + return $platform->quoteValue($value); + }; + break; + } + } + } + + /** + * @param $name + * @throws Exception\InvalidArgumentException + * @return Driver\DriverInterface|Platform\PlatformInterface + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'driver': + return $this->driver; + case 'platform': + return $this->platform; + default: + throw new Exception\InvalidArgumentException('Invalid magic property on adapter'); + } + } + + /** + * @param array $parameters + * @return Driver\DriverInterface + * @throws \InvalidArgumentException + * @throws Exception\InvalidArgumentException + */ + protected function createDriver($parameters) + { + if (! isset($parameters['driver'])) { + throw new Exception\InvalidArgumentException( + __FUNCTION__ . ' expects a "driver" key to be present inside the parameters' + ); + } + + if ($parameters['driver'] instanceof Driver\DriverInterface) { + return $parameters['driver']; + } + + if (! is_string($parameters['driver'])) { + throw new Exception\InvalidArgumentException( + __FUNCTION__ . ' expects a "driver" to be a string or instance of DriverInterface' + ); + } + + $options = []; + if (isset($parameters['options'])) { + $options = (array) $parameters['options']; + unset($parameters['options']); + } + + $driverName = strtolower($parameters['driver']); + switch ($driverName) { + case 'mysqli': + $driver = new Driver\Mysqli\Mysqli($parameters, null, null, $options); + break; + case 'sqlsrv': + $driver = new Driver\Sqlsrv\Sqlsrv($parameters); + break; + case 'oci8': + $driver = new Driver\Oci8\Oci8($parameters); + break; + case 'pgsql': + $driver = new Driver\Pgsql\Pgsql($parameters); + break; + case 'ibmdb2': + $driver = new Driver\IbmDb2\IbmDb2($parameters); + break; + case 'pdo': + default: + if ($driverName == 'pdo' || strpos($driverName, 'pdo') === 0) { + $driver = new Driver\Pdo\Pdo($parameters); + } + } + + if (! isset($driver) || ! $driver instanceof Driver\DriverInterface) { + throw new Exception\InvalidArgumentException('DriverInterface expected', null, null); + } + + return $driver; + } + + /** + * @param array $parameters + * @return Platform\PlatformInterface + */ + protected function createPlatform(array $parameters) + { + if (isset($parameters['platform'])) { + $platformName = $parameters['platform']; + } elseif ($this->driver instanceof Driver\DriverInterface) { + $platformName = $this->driver->getDatabasePlatformName(Driver\DriverInterface::NAME_FORMAT_CAMELCASE); + } else { + throw new Exception\InvalidArgumentException( + 'A platform could not be determined from the provided configuration' + ); + } + + // currently only supported by the IbmDb2 & Oracle concrete implementations + $options = (isset($parameters['platform_options'])) ? $parameters['platform_options'] : []; + + switch ($platformName) { + case 'Mysql': + // mysqli or pdo_mysql driver + if ($this->driver instanceof Driver\Mysqli\Mysqli || $this->driver instanceof Driver\Pdo\Pdo) { + $driver = $this->driver; + } else { + $driver = null; + } + return new Platform\Mysql($driver); + case 'SqlServer': + // PDO is only supported driver for quoting values in this platform + return new Platform\SqlServer(($this->driver instanceof Driver\Pdo\Pdo) ? $this->driver : null); + case 'Oracle': + if ($this->driver instanceof Driver\Oci8\Oci8 || $this->driver instanceof Driver\Pdo\Pdo) { + $driver = $this->driver; + } else { + $driver = null; + } + return new Platform\Oracle($options, $driver); + case 'Sqlite': + // PDO is only supported driver for quoting values in this platform + if ($this->driver instanceof Driver\Pdo\Pdo) { + return new Platform\Sqlite($this->driver); + } + return new Platform\Sqlite(null); + case 'Postgresql': + // pgsql or pdo postgres driver + if ($this->driver instanceof Driver\Pgsql\Pgsql || $this->driver instanceof Driver\Pdo\Pdo) { + $driver = $this->driver; + } else { + $driver = null; + } + return new Platform\Postgresql($driver); + case 'IbmDb2': + // ibm_db2 driver escaping does not need an action connection + return new Platform\IbmDb2($options); + default: + return new Platform\Sql92(); + } + } + + /** + * + * @param array $parameters + * @return Profiler\ProfilerInterface + * @throws Exception\InvalidArgumentException + */ + protected function createProfiler($parameters) + { + if ($parameters['profiler'] instanceof Profiler\ProfilerInterface) { + $profiler = $parameters['profiler']; + } elseif (is_bool($parameters['profiler'])) { + $profiler = ($parameters['profiler'] == true) ? new Profiler\Profiler : null; + } else { + throw new Exception\InvalidArgumentException( + '"profiler" parameter must be an instance of ProfilerInterface or a boolean' + ); + } + return $profiler; + } + + /** + * @param array $parameters + * @return Driver\DriverInterface + * @throws \InvalidArgumentException + * @throws Exception\InvalidArgumentException + * @deprecated + */ + protected function createDriverFromParameters(array $parameters) + { + return $this->createDriver($parameters); + } + + /** + * @param Driver\DriverInterface $driver + * @return Platform\PlatformInterface + * @deprecated + */ + protected function createPlatformFromDriver(Driver\DriverInterface $driver) + { + return $this->createPlatform($driver); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/AdapterAbstractServiceFactory.php b/bundled-libs/laminas/laminas-db/src/Adapter/AdapterAbstractServiceFactory.php new file mode 100644 index 000000000..669c138d0 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/AdapterAbstractServiceFactory.php @@ -0,0 +1,124 @@ +getConfig($container); + if (empty($config)) { + return false; + } + + return ( + isset($config[$requestedName]) + && is_array($config[$requestedName]) + && ! empty($config[$requestedName]) + ); + } + + /** + * Determine if we can create a service with name (SM v2 compatibility) + * + * @param ServiceLocatorInterface $serviceLocator + * @param string $name + * @param string $requestedName + * @return bool + */ + public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName) + { + return $this->canCreate($serviceLocator, $requestedName); + } + + /** + * Create a DB adapter + * + * @param ContainerInterface $container + * @param string $requestedName + * @param array $options + * @return Adapter + */ + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + $config = $this->getConfig($container); + return new Adapter($config[$requestedName]); + } + + /** + * Create service with name + * + * @param ServiceLocatorInterface $serviceLocator + * @param string $name + * @param string $requestedName + * @return Adapter + */ + public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName) + { + return $this($serviceLocator, $requestedName); + } + + /** + * Get db configuration, if any + * + * @param ContainerInterface $container + * @return array + */ + protected function getConfig(ContainerInterface $container) + { + if ($this->config !== null) { + return $this->config; + } + + if (! $container->has('config')) { + $this->config = []; + return $this->config; + } + + $config = $container->get('config'); + if (! isset($config['db']) + || ! is_array($config['db']) + ) { + $this->config = []; + return $this->config; + } + + $config = $config['db']; + if (! isset($config['adapters']) + || ! is_array($config['adapters']) + ) { + $this->config = []; + return $this->config; + } + + $this->config = $config['adapters']; + return $this->config; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/AdapterAwareInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/AdapterAwareInterface.php new file mode 100644 index 000000000..1678a6010 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/AdapterAwareInterface.php @@ -0,0 +1,20 @@ +adapter = $adapter; + + return $this; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/AdapterInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/AdapterInterface.php new file mode 100644 index 000000000..389eb2a40 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/AdapterInterface.php @@ -0,0 +1,27 @@ +adapterName = $adapterName; + } + + public static function __set_state(array $state) : self + { + return new self($state['adapterName'] ?? AdapterInterface::class); + } + + public function __invoke( + ContainerInterface $container, + string $name, + callable $callback, + array $options = null + ) { + $instance = $callback(); + + if (! $instance instanceof AdapterAwareInterface) { + return $instance; + } + + if (! $container->has($this->adapterName)) { + return $instance; + } + + $databaseAdapter = $container->get($this->adapterName); + + if (! $databaseAdapter instanceof Adapter) { + return $instance; + } + + $instance->setDbAdapter($databaseAdapter); + + return $instance; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/AdapterServiceFactory.php b/bundled-libs/laminas/laminas-db/src/Adapter/AdapterServiceFactory.php new file mode 100644 index 000000000..9cdcefcf2 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/AdapterServiceFactory.php @@ -0,0 +1,41 @@ +get('config'); + return new Adapter($config['db']); + } + + /** + * Create db adapter service (v2) + * + * @param ServiceLocatorInterface $container + * @return Adapter + */ + public function createService(ServiceLocatorInterface $container) + { + return $this($container, Adapter::class); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/AbstractConnection.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/AbstractConnection.php new file mode 100644 index 000000000..9e3eee0b4 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/AbstractConnection.php @@ -0,0 +1,134 @@ +isConnected()) { + $this->resource = null; + } + + return $this; + } + + /** + * Get connection parameters + * + * @return array + */ + public function getConnectionParameters() + { + return $this->connectionParameters; + } + + /** + * Get driver name + * + * @return null|string + */ + public function getDriverName() + { + return $this->driverName; + } + + /** + * @return null|ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * {@inheritDoc} + * + * @return resource + */ + public function getResource() + { + if (! $this->isConnected()) { + $this->connect(); + } + + return $this->resource; + } + + /** + * Checks whether the connection is in transaction state. + * + * @return boolean + */ + public function inTransaction() + { + return $this->inTransaction; + } + + /** + * @param array $connectionParameters + * @return self Provides a fluent interface + */ + public function setConnectionParameters(array $connectionParameters) + { + $this->connectionParameters = $connectionParameters; + + return $this; + } + + /** + * {@inheritDoc} + * + * @return self Provides a fluent interface + */ + public function setProfiler(ProfilerInterface $profiler) + { + $this->profiler = $profiler; + + return $this; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/ConnectionInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/ConnectionInterface.php new file mode 100644 index 000000000..6133486af --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/ConnectionInterface.php @@ -0,0 +1,84 @@ +driver = $driver; + } + + /** + * Get name + * + * @return string + */ + abstract public function getName(); +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Feature/DriverFeatureInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Feature/DriverFeatureInterface.php new file mode 100644 index 000000000..e516771f3 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Feature/DriverFeatureInterface.php @@ -0,0 +1,36 @@ +setConnectionParameters($connectionParameters); + } elseif (is_resource($connectionParameters)) { + $this->setResource($connectionParameters); + } elseif (null !== $connectionParameters) { + throw new Exception\InvalidArgumentException( + '$connection must be an array of parameters, a db2 connection resource or null' + ); + } + } + + /** + * Set driver + * + * @param IbmDb2 $driver + * @return self Provides a fluent interface + */ + public function setDriver(IbmDb2 $driver) + { + $this->driver = $driver; + + return $this; + } + + /** + * @param resource $resource DB2 resource + * @return self Provides a fluent interface + */ + public function setResource($resource) + { + if (! is_resource($resource) || get_resource_type($resource) !== 'DB2 Connection') { + throw new Exception\InvalidArgumentException('The resource provided must be of type "DB2 Connection"'); + } + $this->resource = $resource; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getCurrentSchema() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $info = db2_server_info($this->resource); + + return (isset($info->DB_NAME) ? $info->DB_NAME : ''); + } + + /** + * {@inheritDoc} + */ + public function connect() + { + if (is_resource($this->resource)) { + return $this; + } + + // localize + $p = $this->connectionParameters; + + // given a list of key names, test for existence in $p + $findParameterValue = function (array $names) use ($p) { + foreach ($names as $name) { + if (isset($p[$name])) { + return $p[$name]; + } + } + + return; + }; + + $database = $findParameterValue(['database', 'db']); + $username = $findParameterValue(['username', 'uid', 'UID']); + $password = $findParameterValue(['password', 'pwd', 'PWD']); + $isPersistent = $findParameterValue(['persistent', 'PERSISTENT', 'Persistent']); + $options = (isset($p['driver_options']) ? $p['driver_options'] : []); + $connect = ((bool) $isPersistent) ? 'db2_pconnect' : 'db2_connect'; + + $this->resource = $connect($database, $username, $password, $options); + + if ($this->resource === false) { + throw new Exception\RuntimeException(sprintf( + '%s: Unable to connect to database', + __METHOD__ + )); + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isConnected() + { + return ($this->resource !== null); + } + + /** + * {@inheritDoc} + */ + public function disconnect() + { + if ($this->resource) { + db2_close($this->resource); + $this->resource = null; + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + if ($this->isI5() && ! ini_get('ibm_db2.i5_allow_commit')) { + throw new Exception\RuntimeException( + 'DB2 transactions are not enabled, you need to set the ibm_db2.i5_allow_commit=1 in your php.ini' + ); + } + + if (! $this->isConnected()) { + $this->connect(); + } + + $this->prevAutocommit = db2_autocommit($this->resource); + db2_autocommit($this->resource, DB2_AUTOCOMMIT_OFF); + $this->inTransaction = true; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if (! $this->isConnected()) { + $this->connect(); + } + + if (! db2_commit($this->resource)) { + throw new Exception\RuntimeException("The commit has not been successful"); + } + + if ($this->prevAutocommit) { + db2_autocommit($this->resource, $this->prevAutocommit); + } + + $this->inTransaction = false; + + return $this; + } + + /** + * Rollback + * + * @return self Provides a fluent interface + * @throws Exception\RuntimeException + */ + public function rollback() + { + if (! $this->isConnected()) { + throw new Exception\RuntimeException('Must be connected before you can rollback.'); + } + + if (! $this->inTransaction()) { + throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback.'); + } + + if (! db2_rollback($this->resource)) { + throw new Exception\RuntimeException('The rollback has not been successful'); + } + + if ($this->prevAutocommit) { + db2_autocommit($this->resource, $this->prevAutocommit); + } + + $this->inTransaction = false; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function execute($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + set_error_handler(function () { + }, E_WARNING); // suppress warnings + $resultResource = db2_exec($this->resource, $sql); + restore_error_handler(); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + // if the returnValue is something other than a pg result resource, bypass wrapping it + if ($resultResource === false) { + throw new Exception\InvalidQueryException(db2_stmt_errormsg()); + } + + return $this->driver->createResult(($resultResource === true) ? $this->resource : $resultResource); + } + + /** + * {@inheritDoc} + */ + public function getLastGeneratedValue($name = null) + { + return db2_last_insert_id($this->resource); + } + + /** + * Determine if the OS is OS400 (AS400, IBM i) + * + * @return bool + */ + protected function isI5() + { + if (isset($this->i5)) { + return $this->i5; + } + + $this->i5 = (php_uname('s') == 'OS400'); + + return $this->i5; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/IbmDb2/IbmDb2.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/IbmDb2/IbmDb2.php new file mode 100644 index 000000000..4806e7406 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/IbmDb2/IbmDb2.php @@ -0,0 +1,221 @@ +registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * @param Connection $connection + * @return self Provides a fluent interface + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); + return $this; + } + + /** + * @param Statement $statementPrototype + * @return self Provides a fluent interface + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); + return $this; + } + + /** + * @param Result $resultPrototype + * @return self Provides a fluent interface + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + return $this; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + return 'IbmDb2'; + } else { + return 'IBM DB2'; + } + } + + /** + * Check environment + * + * @return bool + */ + public function checkEnvironment() + { + if (! extension_loaded('ibm_db2')) { + throw new Exception\RuntimeException('The ibm_db2 extension is required by this driver.'); + } + } + + /** + * Get connection + * + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Create statement + * + * @param string|resource $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + if (is_resource($sqlOrResource) && get_resource_type($sqlOrResource) == 'DB2 Statement') { + $statement->setResource($sqlOrResource); + } else { + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } elseif ($sqlOrResource !== null) { + throw new Exception\InvalidArgumentException( + __FUNCTION__ . ' only accepts an SQL string or an ibm_db2 resource' + ); + } + if (! $this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + } + return $statement; + } + + /** + * Create result + * + * @param resource $resource + * @return Result + */ + public function createResult($resource) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue()); + return $result; + } + + /** + * @return Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * Get prepare type + * + * @return string + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_POSITIONAL; + } + + /** + * Format parameter name + * + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return '?'; + } + + /** + * Get last generated value + * + * @return mixed + */ + public function getLastGeneratedValue() + { + return $this->connection->getLastGeneratedValue(); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Result.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Result.php new file mode 100644 index 000000000..08a06ae41 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Result.php @@ -0,0 +1,191 @@ +resource = $resource; + $this->generatedValue = $generatedValue; + return $this; + } + + /** + * (PHP 5 >= 5.0.0)
+ * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + */ + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + $this->currentData = db2_fetch_assoc($this->resource); + return $this->currentData; + } + + /** + * @return mixed + */ + public function next() + { + $this->currentData = db2_fetch_assoc($this->resource); + $this->currentComplete = true; + $this->position++; + return $this->currentData; + } + + /** + * @return int|string + */ + public function key() + { + return $this->position; + } + + /** + * @return bool + */ + public function valid() + { + return ($this->currentData !== false); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + */ + public function rewind() + { + if ($this->position > 0) { + throw new Exception\RuntimeException( + 'This result is a forward only result set, calling rewind() after moving forward is not supported' + ); + } + $this->currentData = db2_fetch_assoc($this->resource); + $this->currentComplete = true; + $this->position = 1; + } + + /** + * Force buffering + * + * @return void + */ + public function buffer() + { + return; + } + + /** + * Check if is buffered + * + * @return bool|null + */ + public function isBuffered() + { + return false; + } + + /** + * Is query result? + * + * @return bool + */ + public function isQueryResult() + { + return (db2_num_fields($this->resource) > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + return db2_num_rows($this->resource); + } + + /** + * Get generated value + * + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } + + /** + * Get the resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Get field count + * + * @return int + */ + public function getFieldCount() + { + return db2_num_fields($this->resource); + } + + /** + * @return null|int + */ + public function count() + { + return; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Statement.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Statement.php new file mode 100644 index 000000000..ee2ece168 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/IbmDb2/Statement.php @@ -0,0 +1,276 @@ +db2 = $resource; + return $this; + } + + /** + * @param IbmDb2 $driver + * @return self Provides a fluent interface + */ + public function setDriver(IbmDb2 $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Set sql + * + * @param $sql + * @return self Provides a fluent interface + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Get sql + * + * @return mixed + */ + public function getSql() + { + return $this->sql; + } + + /** + * Set parameter container + * + * @param ParameterContainer $parameterContainer + * @return self Provides a fluent interface + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * Get parameter container + * + * @return mixed + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * @param $resource + * @throws \Laminas\Db\Adapter\Exception\InvalidArgumentException + */ + public function setResource($resource) + { + if (get_resource_type($resource) !== 'DB2 Statement') { + throw new Exception\InvalidArgumentException('Resource must be of type DB2 Statement'); + } + $this->resource = $resource; + } + + /** + * Get resource + * + * @return resource + */ + public function getResource() + { + return $this->resource; + } + + /** + * Prepare sql + * + * @param string|null $sql + * @return self Provides a fluent interface + * @throws Exception\RuntimeException + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has been prepared already'); + } + + if ($sql === null) { + $sql = $this->sql; + } + + try { + set_error_handler($this->createErrorHandler()); + $this->resource = db2_prepare($this->db2, $sql); + } catch (ErrorException $e) { + throw new Exception\RuntimeException($e->getMessage() . '. ' . db2_stmt_errormsg(), db2_stmt_error(), $e); + } finally { + restore_error_handler(); + } + + if ($this->resource === false) { + throw new Exception\RuntimeException(db2_stmt_errormsg(), db2_stmt_error()); + } + + $this->isPrepared = true; + return $this; + } + + /** + * Check if is prepared + * + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * Execute + * + * @param null|array|ParameterContainer $parameters + * @return Result + */ + public function execute($parameters = null) + { + if (! $this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (! $this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + set_error_handler(function () { + }, E_WARNING); // suppress warnings + $response = db2_execute($this->resource, $this->parameterContainer->getPositionalArray()); + restore_error_handler(); + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($response === false) { + throw new Exception\RuntimeException(db2_stmt_errormsg($this->resource)); + } + + $result = $this->driver->createResult($this->resource); + return $result; + } + + /** + * Creates and returns a callable error handler that raises exceptions. + * + * Only raises exceptions for errors that are within the error_reporting mask. + * + * @return callable + */ + private function createErrorHandler() + { + /** + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + * @return void + * @throws ErrorException if error is not within the error_reporting mask. + */ + return function ($errno, $errstr, $errfile, $errline) { + if (! (error_reporting() & $errno)) { + // error_reporting does not include this error + return; + } + + throw new ErrorException($errstr, 0, $errno, $errfile, $errline); + }; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Connection.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Connection.php new file mode 100644 index 000000000..1ff2ede9d --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Connection.php @@ -0,0 +1,299 @@ +setConnectionParameters($connectionInfo); + } elseif ($connectionInfo instanceof \mysqli) { + $this->setResource($connectionInfo); + } elseif (null !== $connectionInfo) { + throw new Exception\InvalidArgumentException( + '$connection must be an array of parameters, a mysqli object or null' + ); + } + } + + /** + * @param Mysqli $driver + * @return self Provides a fluent interface + */ + public function setDriver(Mysqli $driver) + { + $this->driver = $driver; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getCurrentSchema() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $result = $this->resource->query('SELECT DATABASE()'); + $r = $result->fetch_row(); + + return $r[0]; + } + + /** + * Set resource + * + * @param \mysqli $resource + * @return self Provides a fluent interface + */ + public function setResource(\mysqli $resource) + { + $this->resource = $resource; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function connect() + { + if ($this->resource instanceof \mysqli) { + return $this; + } + + // localize + $p = $this->connectionParameters; + + // given a list of key names, test for existence in $p + $findParameterValue = function (array $names) use ($p) { + foreach ($names as $name) { + if (isset($p[$name])) { + return $p[$name]; + } + } + + return; + }; + + $hostname = $findParameterValue(['hostname', 'host']); + $username = $findParameterValue(['username', 'user']); + $password = $findParameterValue(['password', 'passwd', 'pw']); + $database = $findParameterValue(['database', 'dbname', 'db', 'schema']); + $port = (isset($p['port'])) ? (int) $p['port'] : null; + $socket = (isset($p['socket'])) ? $p['socket'] : null; + + $useSSL = (isset($p['use_ssl'])) ? $p['use_ssl'] : 0; + $clientKey = (isset($p['client_key'])) ? $p['client_key'] : ''; + $clientCert = (isset($p['client_cert'])) ? $p['client_cert'] : ''; + $caCert = (isset($p['ca_cert'])) ? $p['ca_cert'] : ''; + $caPath = (isset($p['ca_path'])) ? $p['ca_path'] : ''; + $cipher = (isset($p['cipher'])) ? $p['cipher'] : ''; + + $this->resource = $this->createResource(); + $this->resource->init(); + + if (! empty($p['driver_options'])) { + foreach ($p['driver_options'] as $option => $value) { + if (is_string($option)) { + $option = strtoupper($option); + if (! defined($option)) { + continue; + } + $option = constant($option); + } + $this->resource->options($option, $value); + } + } + + $flags = null; + + if ($useSSL && ! $socket) { + // Even though mysqli docs are not quite clear on this, MYSQLI_CLIENT_SSL + // needs to be set to make sure SSL is used. ssl_set can also cause it to + // be implicitly set, but only when any of the parameters is non-empty. + $flags = MYSQLI_CLIENT_SSL; + $this->resource->ssl_set($clientKey, $clientCert, $caCert, $caPath, $cipher); + //MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT is not valid option, needs to be set as flag + if (isset($p['driver_options']) + && isset($p['driver_options'][MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT]) + ) { + $flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; + } + } + + try { + $flags === null + ? $this->resource->real_connect($hostname, $username, $password, $database, $port, $socket) + : $this->resource->real_connect($hostname, $username, $password, $database, $port, $socket, $flags); + } catch (GenericException $e) { + throw new Exception\RuntimeException( + 'Connection error', + null, + new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno) + ); + } + + if ($this->resource->connect_error) { + throw new Exception\RuntimeException( + 'Connection error', + null, + new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno) + ); + } + + if (! empty($p['charset'])) { + $this->resource->set_charset($p['charset']); + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isConnected() + { + return ($this->resource instanceof \mysqli); + } + + /** + * {@inheritDoc} + */ + public function disconnect() + { + if ($this->resource instanceof \mysqli) { + $this->resource->close(); + } + $this->resource = null; + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $this->resource->autocommit(false); + $this->inTransaction = true; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $this->resource->commit(); + $this->inTransaction = false; + $this->resource->autocommit(true); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function rollback() + { + if (! $this->isConnected()) { + throw new Exception\RuntimeException('Must be connected before you can rollback.'); + } + + if (! $this->inTransaction) { + throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback.'); + } + + $this->resource->rollback(); + $this->resource->autocommit(true); + $this->inTransaction = false; + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\InvalidQueryException + */ + public function execute($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $resultResource = $this->resource->query($sql); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + // if the returnValue is something other than a mysqli_result, bypass wrapping it + if ($resultResource === false) { + throw new Exception\InvalidQueryException($this->resource->error); + } + + $resultPrototype = $this->driver->createResult(($resultResource === true) ? $this->resource : $resultResource); + + return $resultPrototype; + } + + /** + * {@inheritDoc} + */ + public function getLastGeneratedValue($name = null) + { + return $this->resource->insert_id; + } + + /** + * Create a new mysqli resource + * + * @return \mysqli + */ + protected function createResource() + { + return new \mysqli(); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Mysqli.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Mysqli.php new file mode 100644 index 000000000..513cf4548 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Mysqli.php @@ -0,0 +1,261 @@ + false + ]; + + /** + * Constructor + * + * @param array|Connection|\mysqli $connection + * @param null|Statement $statementPrototype + * @param null|Result $resultPrototype + * @param array $options + */ + public function __construct( + $connection, + Statement $statementPrototype = null, + Result $resultPrototype = null, + array $options = [] + ) { + if (! $connection instanceof Connection) { + $connection = new Connection($connection); + } + + $options = array_intersect_key(array_merge($this->options, $options), $this->options); + + $this->registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement($options['buffer_results'])); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return self Provides a fluent interface + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); // needs access to driver to createStatement() + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statementPrototype + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); // needs access to driver to createResult() + } + + /** + * Get statement prototype + * + * @return null|Statement + */ + public function getStatementPrototype() + { + return $this->statementPrototype; + } + + /** + * Register result prototype + * + * @param Result $resultPrototype + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + } + + /** + * @return null|Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + return 'Mysql'; + } + + return 'MySQL'; + } + + /** + * Check environment + * + * @throws Exception\RuntimeException + * @return void + */ + public function checkEnvironment() + { + if (! extension_loaded('mysqli')) { + throw new Exception\RuntimeException( + 'The Mysqli extension is required for this adapter but the extension is not loaded' + ); + } + } + + /** + * Get connection + * + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Create statement + * + * @param string $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + /** + * @todo Resource tracking + if (is_resource($sqlOrResource) && !in_array($sqlOrResource, $this->resources, true)) { + $this->resources[] = $sqlOrResource; + } + */ + + $statement = clone $this->statementPrototype; + if ($sqlOrResource instanceof mysqli_stmt) { + $statement->setResource($sqlOrResource); + } else { + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } + if (! $this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + } + return $statement; + } + + /** + * Create result + * + * @param resource $resource + * @param null|bool $isBuffered + * @return Result + */ + public function createResult($resource, $isBuffered = null) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue(), $isBuffered); + return $result; + } + + /** + * Get prepare type + * + * @return string + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_POSITIONAL; + } + + /** + * Format parameter name + * + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return '?'; + } + + /** + * Get last generated value + * + * @return mixed + */ + public function getLastGeneratedValue() + { + return $this->getConnection()->getLastGeneratedValue(); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Result.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Result.php new file mode 100644 index 000000000..0170b1809 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Result.php @@ -0,0 +1,341 @@ + null, 'values' => []]; + + /** + * @var mixed + */ + protected $generatedValue = null; + + /** + * Initialize + * + * @param mixed $resource + * @param mixed $generatedValue + * @param bool|null $isBuffered + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function initialize($resource, $generatedValue, $isBuffered = null) + { + if (! $resource instanceof \mysqli + && ! $resource instanceof \mysqli_result + && ! $resource instanceof \mysqli_stmt + ) { + throw new Exception\InvalidArgumentException('Invalid resource provided.'); + } + + if ($isBuffered !== null) { + $this->isBuffered = $isBuffered; + } else { + if ($resource instanceof \mysqli || $resource instanceof \mysqli_result + || $resource instanceof \mysqli_stmt && $resource->num_rows != 0) { + $this->isBuffered = true; + } + } + + $this->resource = $resource; + $this->generatedValue = $generatedValue; + return $this; + } + + /** + * Force buffering + * + * @throws Exception\RuntimeException + */ + public function buffer() + { + if ($this->resource instanceof \mysqli_stmt && $this->isBuffered !== true) { + if ($this->position > 0) { + throw new Exception\RuntimeException('Cannot buffer a result set that has started iteration.'); + } + $this->resource->store_result(); + $this->isBuffered = true; + } + } + + /** + * Check if is buffered + * + * @return bool|null + */ + public function isBuffered() + { + return $this->isBuffered; + } + + /** + * Return the resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Is query result? + * + * @return bool + */ + public function isQueryResult() + { + return ($this->resource->field_count > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + if ($this->resource instanceof \mysqli || $this->resource instanceof \mysqli_stmt) { + return $this->resource->affected_rows; + } + + return $this->resource->num_rows; + } + + /** + * Current + * + * @return mixed + */ + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + if ($this->resource instanceof \mysqli_stmt) { + $this->loadDataFromMysqliStatement(); + return $this->currentData; + } else { + $this->loadFromMysqliResult(); + return $this->currentData; + } + } + + /** + * Mysqli's binding and returning of statement values + * + * Mysqli requires you to bind variables to the extension in order to + * get data out. These values have to be references: + * @see http://php.net/manual/en/mysqli-stmt.bind-result.php + * + * @throws Exception\RuntimeException + * @return bool + */ + protected function loadDataFromMysqliStatement() + { + // build the default reference based bind structure, if it does not already exist + if ($this->statementBindValues['keys'] === null) { + $this->statementBindValues['keys'] = []; + $resultResource = $this->resource->result_metadata(); + foreach ($resultResource->fetch_fields() as $col) { + $this->statementBindValues['keys'][] = $col->name; + } + $this->statementBindValues['values'] = array_fill(0, count($this->statementBindValues['keys']), null); + $refs = []; + foreach ($this->statementBindValues['values'] as $i => &$f) { + $refs[$i] = &$f; + } + call_user_func_array([$this->resource, 'bind_result'], $this->statementBindValues['values']); + } + + if (($r = $this->resource->fetch()) === null) { + if (! $this->isBuffered) { + $this->resource->close(); + } + return false; + } elseif ($r === false) { + throw new Exception\RuntimeException($this->resource->error); + } + + // dereference + for ($i = 0, $count = count($this->statementBindValues['keys']); $i < $count; $i++) { + $this->currentData[$this->statementBindValues['keys'][$i]] = $this->statementBindValues['values'][$i]; + } + $this->currentComplete = true; + $this->nextComplete = true; + $this->position++; + return true; + } + + /** + * Load from mysqli result + * + * @return bool + */ + protected function loadFromMysqliResult() + { + $this->currentData = null; + + if (($data = $this->resource->fetch_assoc()) === null) { + return false; + } + + $this->position++; + $this->currentData = $data; + $this->currentComplete = true; + $this->nextComplete = true; + $this->position++; + return true; + } + + /** + * Next + * + * @return void + */ + public function next() + { + $this->currentComplete = false; + + if ($this->nextComplete == false) { + $this->position++; + } + + $this->nextComplete = false; + } + + /** + * Key + * + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * Rewind + * + * @throws Exception\RuntimeException + * @return void + */ + public function rewind() + { + if (0 !== $this->position && false === $this->isBuffered) { + throw new Exception\RuntimeException('Unbuffered results cannot be rewound for multiple iterations'); + } + + $this->resource->data_seek(0); // works for both mysqli_result & mysqli_stmt + $this->currentComplete = false; + $this->position = 0; + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + if ($this->currentComplete) { + return true; + } + + if ($this->resource instanceof \mysqli_stmt) { + return $this->loadDataFromMysqliStatement(); + } + + return $this->loadFromMysqliResult(); + } + + /** + * Count + * + * @throws Exception\RuntimeException + * @return int + */ + public function count() + { + if ($this->isBuffered === false) { + throw new Exception\RuntimeException('Row count is not available in unbuffered result sets.'); + } + return $this->resource->num_rows; + } + + /** + * Get field count + * + * @return int + */ + public function getFieldCount() + { + return $this->resource->field_count; + } + + /** + * Get generated value + * + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Statement.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Statement.php new file mode 100644 index 000000000..e3004820a --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Mysqli/Statement.php @@ -0,0 +1,314 @@ +bufferResults = (bool) $bufferResults; + } + + /** + * Set driver + * + * @param Mysqli $driver + * @return self Provides a fluent interface + */ + public function setDriver(Mysqli $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Initialize + * + * @param \mysqli $mysqli + * @return self Provides a fluent interface + */ + public function initialize(\mysqli $mysqli) + { + $this->mysqli = $mysqli; + return $this; + } + + /** + * Set sql + * + * @param string $sql + * @return self Provides a fluent interface + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Set Parameter container + * + * @param ParameterContainer $parameterContainer + * @return self Provides a fluent interface + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Set resource + * + * @param \mysqli_stmt $mysqliStatement + * @return self Provides a fluent interface + */ + public function setResource(\mysqli_stmt $mysqliStatement) + { + $this->resource = $mysqliStatement; + $this->isPrepared = true; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * Get parameter count + * + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * Is prepared + * + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * Prepare + * + * @param string $sql + * @return self Provides a fluent interface + * @throws Exception\InvalidQueryException + * @throws Exception\RuntimeException + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has already been prepared'); + } + + $sql = ($sql) ?: $this->sql; + + $this->resource = $this->mysqli->prepare($sql); + if (! $this->resource instanceof \mysqli_stmt) { + throw new Exception\InvalidQueryException( + 'Statement couldn\'t be produced with sql: ' . $sql, + null, + new Exception\ErrorException($this->mysqli->error, $this->mysqli->errno) + ); + } + + $this->isPrepared = true; + return $this; + } + + /** + * Execute + * + * @param null|array|ParameterContainer $parameters + * @throws Exception\RuntimeException + * @return mixed + */ + public function execute($parameters = null) + { + if (! $this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (! $this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + $return = $this->resource->execute(); + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($return === false) { + throw new Exception\RuntimeException($this->resource->error); + } + + if ($this->bufferResults === true) { + $this->resource->store_result(); + $this->isPrepared = false; + $buffered = true; + } else { + $buffered = false; + } + + $result = $this->driver->createResult($this->resource, $buffered); + return $result; + } + + /** + * Bind parameters from container + * + * @return void + */ + protected function bindParametersFromContainer() + { + $parameters = $this->parameterContainer->getNamedArray(); + $type = ''; + $args = []; + + foreach ($parameters as $name => &$value) { + if ($this->parameterContainer->offsetHasErrata($name)) { + switch ($this->parameterContainer->offsetGetErrata($name)) { + case ParameterContainer::TYPE_DOUBLE: + $type .= 'd'; + break; + case ParameterContainer::TYPE_NULL: + $value = null; // as per @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php#96148 + case ParameterContainer::TYPE_INTEGER: + $type .= 'i'; + break; + case ParameterContainer::TYPE_STRING: + default: + $type .= 's'; + break; + } + } else { + $type .= 's'; + } + $args[] = &$value; + } + + if ($args) { + array_unshift($args, $type); + call_user_func_array([$this->resource, 'bind_param'], $args); + } + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Connection.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Connection.php new file mode 100644 index 000000000..f6a618dd9 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Connection.php @@ -0,0 +1,267 @@ +setConnectionParameters($connectionInfo); + } elseif ($connectionInfo instanceof \oci8) { + $this->setResource($connectionInfo); + } elseif (null !== $connectionInfo) { + throw new Exception\InvalidArgumentException( + '$connection must be an array of parameters, an oci8 resource or null' + ); + } + } + + /** + * @param Oci8 $driver + * @return self Provides a fluent interface + */ + public function setDriver(Oci8 $driver) + { + $this->driver = $driver; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getCurrentSchema() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $query = "SELECT sys_context('USERENV', 'CURRENT_SCHEMA') as \"current_schema\" FROM DUAL"; + $stmt = oci_parse($this->resource, $query); + oci_execute($stmt); + $dbNameArray = oci_fetch_array($stmt, OCI_ASSOC); + + return $dbNameArray['current_schema']; + } + + /** + * Set resource + * + * @param resource $resource + * @return self Provides a fluent interface + */ + public function setResource($resource) + { + if (! is_resource($resource) || get_resource_type($resource) !== 'oci8 connection') { + throw new Exception\InvalidArgumentException('A resource of type "oci8 connection" was expected'); + } + $this->resource = $resource; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function connect() + { + if (is_resource($this->resource)) { + return $this; + } + + // localize + $p = $this->connectionParameters; + + // given a list of key names, test for existence in $p + $findParameterValue = function (array $names) use ($p) { + foreach ($names as $name) { + if (isset($p[$name])) { + return $p[$name]; + } + } + + return; + }; + + // http://www.php.net/manual/en/function.oci-connect.php + $username = $findParameterValue(['username']); + $password = $findParameterValue(['password']); + $connectionString = $findParameterValue([ + 'connection_string', + 'connectionstring', + 'connection', + 'hostname', + 'instance' + ]); + $characterSet = $findParameterValue(['character_set', 'charset', 'encoding']); + $sessionMode = $findParameterValue(['session_mode']); + + // connection modifiers + $isUnique = $findParameterValue(['unique']); + $isPersistent = $findParameterValue(['persistent']); + + if ($isUnique == true) { + $this->resource = oci_new_connect($username, $password, $connectionString, $characterSet, $sessionMode); + } elseif ($isPersistent == true) { + $this->resource = oci_pconnect($username, $password, $connectionString, $characterSet, $sessionMode); + } else { + $this->resource = oci_connect($username, $password, $connectionString, $characterSet, $sessionMode); + } + + if (! $this->resource) { + $e = oci_error(); + throw new Exception\RuntimeException( + 'Connection error', + null, + new Exception\ErrorException($e['message'], $e['code']) + ); + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isConnected() + { + return (is_resource($this->resource)); + } + + /** + * {@inheritDoc} + */ + public function disconnect() + { + if (is_resource($this->resource)) { + oci_close($this->resource); + } + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + if (! $this->isConnected()) { + $this->connect(); + } + + // A transaction begins when the first SQL statement that changes data is executed with oci_execute() using + // the OCI_NO_AUTO_COMMIT flag. + $this->inTransaction = true; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if (! $this->isConnected()) { + $this->connect(); + } + + if ($this->inTransaction()) { + $valid = oci_commit($this->resource); + if ($valid === false) { + $e = oci_error($this->resource); + throw new Exception\InvalidQueryException($e['message'], $e['code']); + } + + $this->inTransaction = false; + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function rollback() + { + if (! $this->isConnected()) { + throw new Exception\RuntimeException('Must be connected before you can rollback.'); + } + + if (! $this->inTransaction()) { + throw new Exception\RuntimeException('Must call commit() before you can rollback.'); + } + + $valid = oci_rollback($this->resource); + if ($valid === false) { + $e = oci_error($this->resource); + throw new Exception\InvalidQueryException($e['message'], $e['code']); + } + + $this->inTransaction = false; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function execute($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $ociStmt = oci_parse($this->resource, $sql); + + if ($this->inTransaction) { + $valid = @oci_execute($ociStmt, OCI_NO_AUTO_COMMIT); + } else { + $valid = @oci_execute($ociStmt, OCI_COMMIT_ON_SUCCESS); + } + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + if ($valid === false) { + $e = oci_error($ociStmt); + throw new Exception\InvalidQueryException($e['message'], $e['code']); + } + + $resultPrototype = $this->driver->createResult($ociStmt); + + return $resultPrototype; + } + + /** + * {@inheritDoc} + */ + public function getLastGeneratedValue($name = null) + { + // @todo Get Last Generated Value in Connection (this might not apply) + return; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Feature/RowCounter.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Feature/RowCounter.php new file mode 100644 index 000000000..ae9070567 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Feature/RowCounter.php @@ -0,0 +1,74 @@ +getSql(); + if ($sql == '' || stripos(strtolower($sql), 'select') === false) { + return; + } + $countSql = 'SELECT COUNT(*) as "count" FROM (' . $sql . ')'; + $countStmt->prepare($countSql); + $result = $countStmt->execute(); + $countRow = $result->current(); + return $countRow['count']; + } + + /** + * @param string $sql + * @return null|int + */ + public function getCountForSql($sql) + { + if (stripos(strtolower($sql), 'select') === false) { + return; + } + $countSql = 'SELECT COUNT(*) as "count" FROM (' . $sql . ')'; + $result = $this->driver->getConnection()->execute($countSql); + $countRow = $result->current(); + return $countRow['count']; + } + + /** + * @param \Laminas\Db\Adapter\Driver\Oci8\Statement|string $context + * @return callable + */ + public function getRowCountClosure($context) + { + $rowCounter = $this; + return function () use ($rowCounter, $context) { + /** @var $rowCounter RowCounter */ + return ($context instanceof Statement) + ? $rowCounter->getCountForStatement($context) + : $rowCounter->getCountForSql($context); + }; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Oci8.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Oci8.php new file mode 100644 index 000000000..820de0e88 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Oci8.php @@ -0,0 +1,301 @@ +options, $options), $this->options); + $this->registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + if (is_array($features)) { + foreach ($features as $name => $feature) { + $this->addFeature($name, $feature); + } + } elseif ($features instanceof AbstractFeature) { + $this->addFeature($features->getName(), $features); + } elseif ($features === self::FEATURES_DEFAULT) { + $this->setupDefaultFeatures(); + } + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return self Provides a fluent interface + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); // needs access to driver to createStatement() + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statementPrototype + * @return self Provides a fluent interface + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); // needs access to driver to createResult() + return $this; + } + + /** + * @return null|Statement + */ + public function getStatementPrototype() + { + return $this->statementPrototype; + } + + /** + * Register result prototype + * + * @param Result $resultPrototype + * @return self Provides a fluent interface + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + return $this; + } + + /** + * @return null|Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * Add feature + * + * @param string $name + * @param AbstractFeature $feature + * @return self Provides a fluent interface + */ + public function addFeature($name, $feature) + { + if ($feature instanceof AbstractFeature) { + $name = $feature->getName(); // overwrite the name, just in case + $feature->setDriver($this); + } + $this->features[$name] = $feature; + return $this; + } + + /** + * Setup the default features for Pdo + * + * @return self Provides a fluent interface + */ + public function setupDefaultFeatures() + { + $this->addFeature(null, new Feature\RowCounter()); + return $this; + } + + /** + * Get feature + * + * @param string $name + * @return AbstractFeature|false + */ + public function getFeature($name) + { + if (isset($this->features[$name])) { + return $this->features[$name]; + } + return false; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + return 'Oracle'; + } + + /** + * Check environment + */ + public function checkEnvironment() + { + if (! extension_loaded('oci8')) { + throw new Exception\RuntimeException( + 'The Oci8 extension is required for this adapter but the extension is not loaded' + ); + } + } + + /** + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * @param string $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + if (is_resource($sqlOrResource) && get_resource_type($sqlOrResource) == 'oci8 statement') { + $statement->setResource($sqlOrResource); + } else { + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } elseif ($sqlOrResource !== null) { + throw new Exception\InvalidArgumentException( + 'Oci8 only accepts an SQL string or an oci8 resource in ' . __FUNCTION__ + ); + } + if (! $this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + } + return $statement; + } + + /** + * @param resource $resource + * @param null $context + * @return Result + */ + public function createResult($resource, $context = null) + { + $result = clone $this->resultPrototype; + $rowCount = null; + // special feature, oracle Oci counter + if ($context && ($rowCounter = $this->getFeature('RowCounter')) && oci_num_fields($resource) > 0) { + $rowCount = $rowCounter->getRowCountClosure($context); + } + $result->initialize($resource, null, $rowCount); + return $result; + } + + /** + * @return string + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_NAMED; + } + + /** + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return ':' . $name; + } + + /** + * @return mixed + */ + public function getLastGeneratedValue() + { + return $this->getConnection()->getLastGeneratedValue(); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Result.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Result.php new file mode 100644 index 000000000..0b5df2d73 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Result.php @@ -0,0 +1,230 @@ + null, 'values' => []]; + + /** + * @var mixed + */ + protected $generatedValue = null; + + /** + * Initialize + * @param resource $resource + * @param null|int $generatedValue + * @param null|int $rowCount + * @return self Provides a fluent interface + */ + public function initialize($resource, $generatedValue = null, $rowCount = null) + { + if (! is_resource($resource) && get_resource_type($resource) !== 'oci8 statement') { + throw new Exception\InvalidArgumentException('Invalid resource provided.'); + } + $this->resource = $resource; + $this->generatedValue = $generatedValue; + $this->rowCount = $rowCount; + return $this; + } + + /** + * Force buffering at driver level + * + * Oracle does not support this, to my knowledge (@ralphschindler) + * + * @throws Exception\RuntimeException + */ + public function buffer() + { + return; + } + + /** + * Is the result buffered? + * + * @return bool + */ + public function isBuffered() + { + return false; + } + + /** + * Return the resource + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Is query result? + * + * @return bool + */ + public function isQueryResult() + { + return (oci_num_fields($this->resource) > 0); + } + + /** + * Get affected rows + * @return int + */ + public function getAffectedRows() + { + return oci_num_rows($this->resource); + } + + /** + * Current + * @return mixed + */ + public function current() + { + if ($this->currentComplete == false) { + if ($this->loadData() === false) { + return false; + } + } + return $this->currentData; + } + + /** + * Load from oci8 result + * + * @return bool + */ + protected function loadData() + { + $this->currentComplete = true; + $this->currentData = oci_fetch_assoc($this->resource); + if ($this->currentData !== false) { + $this->position++; + return true; + } + return false; + } + + /** + * Next + */ + public function next() + { + return $this->loadData(); + } + + /** + * Key + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * Rewind + */ + public function rewind() + { + if ($this->position > 0) { + throw new Exception\RuntimeException('Oci8 results cannot be rewound for multiple iterations'); + } + } + + /** + * Valid + * @return bool + */ + public function valid() + { + if ($this->currentComplete) { + return ($this->currentData !== false); + } + return $this->loadData(); + } + + /** + * Count + * @return null|int + */ + public function count() + { + if (is_int($this->rowCount)) { + return $this->rowCount; + } + if (is_callable($this->rowCount)) { + $this->rowCount = (int) call_user_func($this->rowCount); + return $this->rowCount; + } + return; + } + + /** + * @return int + */ + public function getFieldCount() + { + return oci_num_fields($this->resource); + } + + /** + * @return null + */ + public function getGeneratedValue() + { + // @todo OCI8 generated value in Driver Result + return; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Statement.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Statement.php new file mode 100644 index 000000000..574e51467 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Oci8/Statement.php @@ -0,0 +1,326 @@ +driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Initialize + * + * @param resource $oci8 + * @return self Provides a fluent interface + */ + public function initialize($oci8) + { + $this->oci8 = $oci8; + return $this; + } + + /** + * Set sql + * + * @param string $sql + * @return self Provides a fluent interface + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Set Parameter container + * + * @param ParameterContainer $parameterContainer + * @return self Provides a fluent interface + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Set resource + * + * @param resource $oci8Statement + * @return self Provides a fluent interface + */ + public function setResource($oci8Statement) + { + $type = oci_statement_type($oci8Statement); + if (false === $type || 'UNKNOWN' == $type) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid statement provided to %s', + __METHOD__ + )); + } + $this->resource = $oci8Statement; + $this->isPrepared = true; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * @param string $sql + * @return self Provides a fluent interface + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has already been prepared'); + } + + $sql = ($sql) ?: $this->sql; + + // get oci8 statement resource + $this->resource = oci_parse($this->oci8, $sql); + + if (! $this->resource) { + $e = oci_error($this->oci8); + throw new Exception\InvalidQueryException( + 'Statement couldn\'t be produced with sql: ' . $sql, + null, + new Exception\ErrorException($e['message'], $e['code']) + ); + } + + $this->isPrepared = true; + return $this; + } + + /** + * Execute + * + * @param null|array|ParameterContainer $parameters + * @return mixed + */ + public function execute($parameters = null) + { + if (! $this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (! $this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + if ($this->driver->getConnection()->inTransaction()) { + $ret = @oci_execute($this->resource, OCI_NO_AUTO_COMMIT); + } else { + $ret = @oci_execute($this->resource, OCI_COMMIT_ON_SUCCESS); + } + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($ret === false) { + $e = oci_error($this->resource); + throw new Exception\RuntimeException($e['message'], $e['code']); + } + + $result = $this->driver->createResult($this->resource, $this); + return $result; + } + + /** + * Bind parameters from container + */ + protected function bindParametersFromContainer() + { + $parameters = $this->parameterContainer->getNamedArray(); + + foreach ($parameters as $name => &$value) { + if ($this->parameterContainer->offsetHasErrata($name)) { + switch ($this->parameterContainer->offsetGetErrata($name)) { + case ParameterContainer::TYPE_NULL: + $type = null; + $value = null; + break; + case ParameterContainer::TYPE_DOUBLE: + case ParameterContainer::TYPE_INTEGER: + $type = SQLT_INT; + if (is_string($value)) { + $value = (int) $value; + } + break; + case ParameterContainer::TYPE_BINARY: + $type = SQLT_BIN; + break; + case ParameterContainer::TYPE_LOB: + $type = OCI_B_CLOB; + $clob = oci_new_descriptor($this->driver->getConnection()->getResource(), OCI_DTYPE_LOB); + $clob->writetemporary($value, OCI_TEMP_CLOB); + $value = $clob; + break; + case ParameterContainer::TYPE_STRING: + default: + $type = SQLT_CHR; + break; + } + } else { + $type = SQLT_CHR; + } + + $maxLength = -1; + if ($this->parameterContainer->offsetHasMaxLength($name)) { + $maxLength = $this->parameterContainer->offsetGetMaxLength($name); + } + + oci_bind_by_name($this->resource, $name, $value, $maxLength, $type); + } + } + + /** + * Perform a deep clone + */ + public function __clone() + { + $this->isPrepared = false; + $this->parametersBound = false; + $this->resource = null; + if ($this->parameterContainer) { + $this->parameterContainer = clone $this->parameterContainer; + } + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Connection.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Connection.php new file mode 100644 index 000000000..54165b80b --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Connection.php @@ -0,0 +1,431 @@ +setConnectionParameters($connectionParameters); + } elseif ($connectionParameters instanceof \PDO) { + $this->setResource($connectionParameters); + } elseif (null !== $connectionParameters) { + throw new Exception\InvalidArgumentException( + '$connection must be an array of parameters, a PDO object or null' + ); + } + } + + /** + * Set driver + * + * @param Pdo $driver + * @return self Provides a fluent interface + */ + public function setDriver(Pdo $driver) + { + $this->driver = $driver; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function setConnectionParameters(array $connectionParameters) + { + $this->connectionParameters = $connectionParameters; + if (isset($connectionParameters['dsn'])) { + $this->driverName = substr( + $connectionParameters['dsn'], + 0, + strpos($connectionParameters['dsn'], ':') + ); + } elseif (isset($connectionParameters['pdodriver'])) { + $this->driverName = strtolower($connectionParameters['pdodriver']); + } elseif (isset($connectionParameters['driver'])) { + $this->driverName = strtolower(substr( + str_replace(['-', '_', ' '], '', $connectionParameters['driver']), + 3 + )); + } + } + + /** + * Get the dsn string for this connection + * @throws \Laminas\Db\Adapter\Exception\RunTimeException + * @return string + */ + public function getDsn() + { + if (! $this->dsn) { + throw new Exception\RuntimeException( + 'The DSN has not been set or constructed from parameters in connect() for this Connection' + ); + } + + return $this->dsn; + } + + /** + * {@inheritDoc} + */ + public function getCurrentSchema() + { + if (! $this->isConnected()) { + $this->connect(); + } + + switch ($this->driverName) { + case 'mysql': + $sql = 'SELECT DATABASE()'; + break; + case 'sqlite': + return 'main'; + case 'sqlsrv': + case 'dblib': + $sql = 'SELECT SCHEMA_NAME()'; + break; + case 'pgsql': + default: + $sql = 'SELECT CURRENT_SCHEMA'; + break; + } + + /** @var $result \PDOStatement */ + $result = $this->resource->query($sql); + if ($result instanceof \PDOStatement) { + return $result->fetchColumn(); + } + + return false; + } + + /** + * Set resource + * + * @param \PDO $resource + * @return self Provides a fluent interface + */ + public function setResource(\PDO $resource) + { + $this->resource = $resource; + $this->driverName = strtolower($this->resource->getAttribute(\PDO::ATTR_DRIVER_NAME)); + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\InvalidConnectionParametersException + * @throws Exception\RuntimeException + */ + public function connect() + { + if ($this->resource) { + return $this; + } + + $dsn = $username = $password = $hostname = $database = null; + $options = []; + foreach ($this->connectionParameters as $key => $value) { + switch (strtolower($key)) { + case 'dsn': + $dsn = $value; + break; + case 'driver': + $value = strtolower((string) $value); + if (strpos($value, 'pdo') === 0) { + $pdoDriver = str_replace(['-', '_', ' '], '', $value); + $pdoDriver = substr($pdoDriver, 3) ?: ''; + } + break; + case 'pdodriver': + $pdoDriver = (string) $value; + break; + case 'user': + case 'username': + $username = (string) $value; + break; + case 'pass': + case 'password': + $password = (string) $value; + break; + case 'host': + case 'hostname': + $hostname = (string) $value; + break; + case 'port': + $port = (int) $value; + break; + case 'database': + case 'dbname': + $database = (string) $value; + break; + case 'charset': + $charset = (string) $value; + break; + case 'unix_socket': + $unix_socket = (string) $value; + break; + case 'version': + $version = (string) $value; + break; + case 'driver_options': + case 'options': + $value = (array) $value; + $options = array_diff_key($options, $value) + $value; + break; + default: + $options[$key] = $value; + break; + } + } + + if (isset($hostname) && isset($unix_socket)) { + throw new Exception\InvalidConnectionParametersException( + 'Ambiguous connection parameters, both hostname and unix_socket parameters were set', + $this->connectionParameters + ); + } + + if (! isset($dsn) && isset($pdoDriver)) { + $dsn = []; + switch ($pdoDriver) { + case 'sqlite': + $dsn[] = $database; + break; + case 'sqlsrv': + if (isset($database)) { + $dsn[] = "database={$database}"; + } + if (isset($hostname)) { + $dsn[] = "server={$hostname}"; + } + break; + default: + if (isset($database)) { + $dsn[] = "dbname={$database}"; + } + if (isset($hostname)) { + $dsn[] = "host={$hostname}"; + } + if (isset($port)) { + $dsn[] = "port={$port}"; + } + if (isset($charset) && $pdoDriver != 'pgsql') { + $dsn[] = "charset={$charset}"; + } + if (isset($unix_socket)) { + $dsn[] = "unix_socket={$unix_socket}"; + } + if (isset($version)) { + $dsn[] = "version={$version}"; + } + break; + } + $dsn = $pdoDriver . ':' . implode(';', $dsn); + } elseif (! isset($dsn)) { + throw new Exception\InvalidConnectionParametersException( + 'A dsn was not provided or could not be constructed from your parameters', + $this->connectionParameters + ); + } + + $this->dsn = $dsn; + + try { + $this->resource = new \PDO($dsn, $username, $password, $options); + $this->resource->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + if (isset($charset) && $pdoDriver == 'pgsql') { + $this->resource->exec('SET NAMES ' . $this->resource->quote($charset)); + } + $this->driverName = strtolower($this->resource->getAttribute(\PDO::ATTR_DRIVER_NAME)); + } catch (\PDOException $e) { + $code = $e->getCode(); + if (! is_long($code)) { + $code = null; + } + throw new Exception\RuntimeException('Connect Error: ' . $e->getMessage(), $code, $e); + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isConnected() + { + return ($this->resource instanceof \PDO); + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + if (! $this->isConnected()) { + $this->connect(); + } + + if (0 === $this->nestedTransactionsCount) { + $this->resource->beginTransaction(); + $this->inTransaction = true; + } + + $this->nestedTransactionsCount ++; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if (! $this->isConnected()) { + $this->connect(); + } + + if ($this->inTransaction) { + $this->nestedTransactionsCount -= 1; + } + + /* + * This shouldn't check for being in a transaction since + * after issuing a SET autocommit=0; we have to commit too. + */ + if (0 === $this->nestedTransactionsCount) { + $this->resource->commit(); + $this->inTransaction = false; + } + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + public function rollback() + { + if (! $this->isConnected()) { + throw new Exception\RuntimeException('Must be connected before you can rollback'); + } + + if (! $this->inTransaction()) { + throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback'); + } + + $this->resource->rollBack(); + + $this->inTransaction = false; + $this->nestedTransactionsCount = 0; + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\InvalidQueryException + */ + public function execute($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $resultResource = $this->resource->query($sql); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + if ($resultResource === false) { + $errorInfo = $this->resource->errorInfo(); + throw new Exception\InvalidQueryException($errorInfo[2]); + } + + $result = $this->driver->createResult($resultResource, $sql); + + return $result; + } + + /** + * Prepare + * + * @param string $sql + * @return Statement + */ + public function prepare($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + $statement = $this->driver->createStatement($sql); + + return $statement; + } + + /** + * {@inheritDoc} + * + * @param string $name + * @return string|null|false + */ + public function getLastGeneratedValue($name = null) + { + if ($name === null + && ($this->driverName == 'pgsql' || $this->driverName == 'firebird')) { + return; + } + + try { + return $this->resource->lastInsertId($name); + } catch (\Exception $e) { + // do nothing + } + + return false; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/OracleRowCounter.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/OracleRowCounter.php new file mode 100644 index 000000000..781aca3d6 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/OracleRowCounter.php @@ -0,0 +1,75 @@ +getSql(); + if ($sql == '' || stripos($sql, 'select') === false) { + return; + } + $countSql = 'SELECT COUNT(*) as "count" FROM (' . $sql . ')'; + $countStmt->prepare($countSql); + $result = $countStmt->execute(); + $countRow = $result->getResource()->fetch(\PDO::FETCH_ASSOC); + unset($statement, $result); + return $countRow['count']; + } + + /** + * @param $sql + * @return null|int + */ + public function getCountForSql($sql) + { + if (stripos($sql, 'select') === false) { + return; + } + $countSql = 'SELECT COUNT(*) as count FROM (' . $sql . ')'; + /** @var $pdo \PDO */ + $pdo = $this->driver->getConnection()->getResource(); + $result = $pdo->query($countSql); + $countRow = $result->fetch(\PDO::FETCH_ASSOC); + return $countRow['count']; + } + + /** + * @param $context + * @return \Closure + */ + public function getRowCountClosure($context) + { + return function () use ($context) { + return ($context instanceof Pdo\Statement) + ? $this->getCountForStatement($context) + : $this->getCountForSql($context); + }; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php new file mode 100644 index 000000000..eb922f5c9 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php @@ -0,0 +1,75 @@ +getSql(); + if ($sql == '' || stripos($sql, 'select') === false) { + return; + } + $countSql = 'SELECT COUNT(*) as "count" FROM (' . $sql . ')'; + $countStmt->prepare($countSql); + $result = $countStmt->execute(); + $countRow = $result->getResource()->fetch(\PDO::FETCH_ASSOC); + unset($statement, $result); + return $countRow['count']; + } + + /** + * @param $sql + * @return null|int + */ + public function getCountForSql($sql) + { + if (stripos($sql, 'select') === false) { + return; + } + $countSql = 'SELECT COUNT(*) as count FROM (' . $sql . ')'; + /** @var $pdo \PDO */ + $pdo = $this->driver->getConnection()->getResource(); + $result = $pdo->query($countSql); + $countRow = $result->fetch(\PDO::FETCH_ASSOC); + return $countRow['count']; + } + + /** + * @param $context + * @return \Closure + */ + public function getRowCountClosure($context) + { + return function () use ($context) { + return ($context instanceof Pdo\Statement) + ? $this->getCountForStatement($context) + : $this->getCountForSql($context); + }; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Pdo.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Pdo.php new file mode 100644 index 000000000..3e8bf1ef7 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Pdo.php @@ -0,0 +1,338 @@ +registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + if (is_array($features)) { + foreach ($features as $name => $feature) { + $this->addFeature($name, $feature); + } + } elseif ($features instanceof AbstractFeature) { + $this->addFeature($features->getName(), $features); + } elseif ($features === self::FEATURES_DEFAULT) { + $this->setupDefaultFeatures(); + } + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return self Provides a fluent interface + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statementPrototype + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); + } + + /** + * Register result prototype + * + * @param Result $resultPrototype + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + } + + /** + * Add feature + * + * @param string $name + * @param AbstractFeature $feature + * @return self Provides a fluent interface + */ + public function addFeature($name, $feature) + { + if ($feature instanceof AbstractFeature) { + $name = $feature->getName(); // overwrite the name, just in case + $feature->setDriver($this); + } + $this->features[$name] = $feature; + return $this; + } + + /** + * Setup the default features for Pdo + * + * @return self Provides a fluent interface + */ + public function setupDefaultFeatures() + { + $driverName = $this->connection->getDriverName(); + if ($driverName == 'sqlite') { + $this->addFeature(null, new Feature\SqliteRowCounter); + } elseif ($driverName == 'oci') { + $this->addFeature(null, new Feature\OracleRowCounter); + } + return $this; + } + + /** + * Get feature + * + * @param $name + * @return AbstractFeature|false + */ + public function getFeature($name) + { + if (isset($this->features[$name])) { + return $this->features[$name]; + } + return false; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + $name = $this->getConnection()->getDriverName(); + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + switch ($name) { + case 'pgsql': + return 'Postgresql'; + case 'oci': + return 'Oracle'; + case 'dblib': + case 'sqlsrv': + return 'SqlServer'; + default: + return ucfirst($name); + } + } else { + switch ($name) { + case 'sqlite': + return 'SQLite'; + case 'mysql': + return 'MySQL'; + case 'pgsql': + return 'PostgreSQL'; + case 'oci': + return 'Oracle'; + case 'dblib': + case 'sqlsrv': + return 'SQLServer'; + default: + return ucfirst($name); + } + } + } + + /** + * Check environment + */ + public function checkEnvironment() + { + if (! extension_loaded('PDO')) { + throw new Exception\RuntimeException( + 'The PDO extension is required for this adapter but the extension is not loaded' + ); + } + } + + /** + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * @param string|PDOStatement $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + if ($sqlOrResource instanceof PDOStatement) { + $statement->setResource($sqlOrResource); + } else { + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } + if (! $this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + } + return $statement; + } + + /** + * @param resource $resource + * @param mixed $context + * @return Result + */ + public function createResult($resource, $context = null) + { + $result = clone $this->resultPrototype; + $rowCount = null; + + // special feature, sqlite PDO counter + if ($this->connection->getDriverName() == 'sqlite' + && ($sqliteRowCounter = $this->getFeature('SqliteRowCounter')) + && $resource->columnCount() > 0) { + $rowCount = $sqliteRowCounter->getRowCountClosure($context); + } + + // special feature, oracle PDO counter + if ($this->connection->getDriverName() == 'oci' + && ($oracleRowCounter = $this->getFeature('OracleRowCounter')) + && $resource->columnCount() > 0) { + $rowCount = $oracleRowCounter->getRowCountClosure($context); + } + + + $result->initialize($resource, $this->connection->getLastGeneratedValue(), $rowCount); + return $result; + } + + /** + * @return Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * @return string + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_NAMED; + } + + /** + * @param string $name + * @param string|null $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + if ($type === null && ! is_numeric($name) || $type == self::PARAMETERIZATION_NAMED) { + $name = ltrim($name, ':'); + // @see https://bugs.php.net/bug.php?id=43130 + if (preg_match('/[^a-zA-Z0-9_]/', $name)) { + throw new Exception\RuntimeException(sprintf( + 'The PDO param %s contains invalid characters.' + . ' Only alphabetic characters, digits, and underscores (_)' + . ' are allowed.', + $name + )); + } + return ':' . $name; + } + + return '?'; + } + + /** + * @param string|null $name + * @return string|null|false + */ + public function getLastGeneratedValue($name = null) + { + return $this->connection->getLastGeneratedValue($name); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Result.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Result.php new file mode 100644 index 000000000..be69af295 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Result.php @@ -0,0 +1,277 @@ +resource = $resource; + $this->generatedValue = $generatedValue; + $this->rowCount = $rowCount; + + return $this; + } + + /** + * @return null + */ + public function buffer() + { + return; + } + + /** + * @return bool|null + */ + public function isBuffered() + { + return false; + } + + /** + * @param int $fetchMode + * @throws Exception\InvalidArgumentException on invalid fetch mode + */ + public function setFetchMode($fetchMode) + { + if (! in_array($fetchMode, self::VALID_FETCH_MODES, true)) { + throw new Exception\InvalidArgumentException( + 'The fetch mode must be one of the PDO::FETCH_* constants.' + ); + } + + $this->fetchMode = (int) $fetchMode; + } + + /** + * @return int + */ + public function getFetchMode() + { + return $this->fetchMode; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Get the data + * @return mixed + */ + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + $this->currentData = $this->resource->fetch($this->fetchMode); + $this->currentComplete = true; + return $this->currentData; + } + + /** + * Next + * + * @return mixed + */ + public function next() + { + $this->currentData = $this->resource->fetch($this->fetchMode); + $this->currentComplete = true; + $this->position++; + return $this->currentData; + } + + /** + * Key + * + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * @throws Exception\RuntimeException + * @return void + */ + public function rewind() + { + if ($this->statementMode == self::STATEMENT_MODE_FORWARD && $this->position > 0) { + throw new Exception\RuntimeException( + 'This result is a forward only result set, calling rewind() after moving forward is not supported' + ); + } + $this->currentData = $this->resource->fetch($this->fetchMode); + $this->currentComplete = true; + $this->position = 0; + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + return ($this->currentData !== false); + } + + /** + * Count + * + * @return int + */ + public function count() + { + if (is_int($this->rowCount)) { + return $this->rowCount; + } + if ($this->rowCount instanceof \Closure) { + $this->rowCount = (int) call_user_func($this->rowCount); + } else { + $this->rowCount = (int) $this->resource->rowCount(); + } + return $this->rowCount; + } + + /** + * @return int + */ + public function getFieldCount() + { + return $this->resource->columnCount(); + } + + /** + * Is query result + * + * @return bool + */ + public function isQueryResult() + { + return ($this->resource->columnCount() > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + return $this->resource->rowCount(); + } + + /** + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Statement.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Statement.php new file mode 100644 index 000000000..23c7fd031 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pdo/Statement.php @@ -0,0 +1,309 @@ +driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Initialize + * + * @param \PDO $connectionResource + * @return self Provides a fluent interface + */ + public function initialize(\PDO $connectionResource) + { + $this->pdo = $connectionResource; + return $this; + } + + /** + * Set resource + * + * @param \PDOStatement $pdoStatement + * @return self Provides a fluent interface + */ + public function setResource(\PDOStatement $pdoStatement) + { + $this->resource = $pdoStatement; + return $this; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Set sql + * + * @param string $sql + * @return self Provides a fluent interface + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @param ParameterContainer $parameterContainer + * @return self Provides a fluent interface + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * @param string $sql + * @throws Exception\RuntimeException + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has been prepared already'); + } + + if ($sql === null) { + $sql = $this->sql; + } + + $this->resource = $this->pdo->prepare($sql); + + if ($this->resource === false) { + $error = $this->pdo->errorInfo(); + throw new Exception\RuntimeException($error[2]); + } + + $this->isPrepared = true; + } + + /** + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * @param null|array|ParameterContainer $parameters + * @throws Exception\InvalidQueryException + * @return Result + */ + public function execute($parameters = null) + { + if (! $this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (! $this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + try { + $this->resource->execute(); + } catch (\PDOException $e) { + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + throw new Exception\InvalidQueryException( + 'Statement could not be executed (' . implode(' - ', $this->resource->errorInfo()) . ')', + null, + $e + ); + } + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + $result = $this->driver->createResult($this->resource, $this); + return $result; + } + + /** + * Bind parameters from container + */ + protected function bindParametersFromContainer() + { + if ($this->parametersBound) { + return; + } + + $parameters = $this->parameterContainer->getNamedArray(); + foreach ($parameters as $name => &$value) { + if (is_bool($value)) { + $type = \PDO::PARAM_BOOL; + } elseif (is_int($value)) { + $type = \PDO::PARAM_INT; + } else { + $type = \PDO::PARAM_STR; + } + if ($this->parameterContainer->offsetHasErrata($name)) { + switch ($this->parameterContainer->offsetGetErrata($name)) { + case ParameterContainer::TYPE_INTEGER: + $type = \PDO::PARAM_INT; + break; + case ParameterContainer::TYPE_NULL: + $type = \PDO::PARAM_NULL; + break; + case ParameterContainer::TYPE_LOB: + $type = \PDO::PARAM_LOB; + break; + } + } + + // parameter is named or positional, value is reference + $parameter = is_int($name) ? ($name + 1) : $this->driver->formatParameterName($name); + $this->resource->bindParam($parameter, $value, $type); + } + } + + /** + * Perform a deep clone + * @return Statement A cloned statement + */ + public function __clone() + { + $this->isPrepared = false; + $this->parametersBound = false; + $this->resource = null; + if ($this->parameterContainer) { + $this->parameterContainer = clone $this->parameterContainer; + } + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Connection.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Connection.php new file mode 100644 index 000000000..df1234416 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Connection.php @@ -0,0 +1,315 @@ +setConnectionParameters($connectionInfo); + } elseif (is_resource($connectionInfo)) { + $this->setResource($connectionInfo); + } + } + + /** + * Set resource + * + * @param resource $resource + * @return self Provides a fluent interface + */ + public function setResource($resource) + { + $this->resource = $resource; + + return $this; + } + + + /** + * Set driver + * + * @param Pgsql $driver + * @return self Provides a fluent interface + */ + public function setDriver(Pgsql $driver) + { + $this->driver = $driver; + + return $this; + } + + /** + * @param int|null $type + * @return self Provides a fluent interface + */ + public function setType($type) + { + $invalidConectionType = ($type !== PGSQL_CONNECT_FORCE_NEW); + + // Compatibility with PHP < 5.6 + if ($invalidConectionType && defined('PGSQL_CONNECT_ASYNC')) { + $invalidConectionType = ($type !== PGSQL_CONNECT_ASYNC); + } + + if ($invalidConectionType) { + throw new Exception\InvalidArgumentException( + 'Connection type is not valid. (See: http://php.net/manual/en/function.pg-connect.php)' + ); + } + $this->type = $type; + return $this; + } + + /** + * {@inheritDoc} + * + * @return null|string + */ + public function getCurrentSchema() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $result = pg_query($this->resource, 'SELECT CURRENT_SCHEMA AS "currentschema"'); + if ($result == false) { + return; + } + + return pg_fetch_result($result, 0, 'currentschema'); + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException on failure + */ + public function connect() + { + if (is_resource($this->resource)) { + return $this; + } + + $connection = $this->getConnectionString(); + set_error_handler(function ($number, $string) { + throw new Exception\RuntimeException( + __CLASS__ . '::connect: Unable to connect to database', + null, + new Exception\ErrorException($string, $number) + ); + }); + try { + $this->resource = pg_connect($connection); + } finally { + restore_error_handler(); + } + + if ($this->resource === false) { + throw new Exception\RuntimeException(sprintf( + '%s: Unable to connect to database', + __METHOD__ + )); + } + + $p = $this->connectionParameters; + + if (! empty($p['charset'])) { + if (-1 === pg_set_client_encoding($this->resource, $p['charset'])) { + throw new Exception\RuntimeException(sprintf( + "%s: Unable to set client encoding '%s'", + __METHOD__, + $p['charset'] + )); + } + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isConnected() + { + return (is_resource($this->resource)); + } + + /** + * {@inheritDoc} + */ + public function disconnect() + { + pg_close($this->resource); + return $this; + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + if ($this->inTransaction()) { + throw new Exception\RuntimeException('Nested transactions are not supported'); + } + + if (! $this->isConnected()) { + $this->connect(); + } + + pg_query($this->resource, 'BEGIN'); + $this->inTransaction = true; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if (! $this->isConnected()) { + $this->connect(); + } + + if (! $this->inTransaction()) { + return; // We ignore attempts to commit non-existing transaction + } + + pg_query($this->resource, 'COMMIT'); + $this->inTransaction = false; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function rollback() + { + if (! $this->isConnected()) { + throw new Exception\RuntimeException('Must be connected before you can rollback'); + } + + if (! $this->inTransaction()) { + throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback'); + } + + pg_query($this->resource, 'ROLLBACK'); + $this->inTransaction = false; + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\InvalidQueryException + * @return resource|\Laminas\Db\ResultSet\ResultSetInterface + */ + public function execute($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $resultResource = pg_query($this->resource, $sql); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + // if the returnValue is something other than a pg result resource, bypass wrapping it + if ($resultResource === false) { + throw new Exception\InvalidQueryException(pg_errormessage()); + } + + $resultPrototype = $this->driver->createResult(($resultResource === true) ? $this->resource : $resultResource); + + return $resultPrototype; + } + + /** + * {@inheritDoc} + * + * @return string + */ + public function getLastGeneratedValue($name = null) + { + if ($name === null) { + return; + } + $result = pg_query( + $this->resource, + 'SELECT CURRVAL(\'' . str_replace('\'', '\\\'', $name) . '\') as "currval"' + ); + + return pg_fetch_result($result, 0, 'currval'); + } + + /** + * Get Connection String + * + * @return string + */ + private function getConnectionString() + { + // localize + $p = $this->connectionParameters; + + // given a list of key names, test for existence in $p + $findParameterValue = function (array $names) use ($p) { + foreach ($names as $name) { + if (isset($p[$name])) { + return $p[$name]; + } + } + return; + }; + + $connectionParameters = [ + 'host' => $findParameterValue(['hostname', 'host']), + 'user' => $findParameterValue(['username', 'user']), + 'password' => $findParameterValue(['password', 'passwd', 'pw']), + 'dbname' => $findParameterValue(['database', 'dbname', 'db', 'schema']), + 'port' => isset($p['port']) ? (int) $p['port'] : null, + 'socket' => isset($p['socket']) ? $p['socket'] : null, + ]; + + return urldecode(http_build_query(array_filter($connectionParameters), null, ' ')); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Pgsql.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Pgsql.php new file mode 100644 index 000000000..dd6c17e1f --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Pgsql.php @@ -0,0 +1,244 @@ + false + ]; + + /** + * Constructor + * + * @param array|Connection|resource $connection + * @param null|Statement $statementPrototype + * @param null|Result $resultPrototype + * @param array $options + */ + public function __construct( + $connection, + Statement $statementPrototype = null, + Result $resultPrototype = null, + $options = null + ) { + if (! $connection instanceof Connection) { + $connection = new Connection($connection); + } + + $this->registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return self Provides a fluent interface + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statement + * @return self Provides a fluent interface + */ + public function registerStatementPrototype(Statement $statement) + { + $this->statementPrototype = $statement; + $this->statementPrototype->setDriver($this); // needs access to driver to createResult() + return $this; + } + + /** + * Register result prototype + * + * @param Result $result + * @return self Provides a fluent interface + */ + public function registerResultPrototype(Result $result) + { + $this->resultPrototype = $result; + return $this; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + return 'Postgresql'; + } + + return 'PostgreSQL'; + } + + /** + * Check environment + * + * @throws Exception\RuntimeException + * @return bool + */ + public function checkEnvironment() + { + if (! extension_loaded('pgsql')) { + throw new Exception\RuntimeException( + 'The PostgreSQL (pgsql) extension is required for this adapter but the extension is not loaded' + ); + } + } + + /** + * Get connection + * + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Create statement + * + * @param string|null $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } + + if (! $this->connection->isConnected()) { + $this->connection->connect(); + } + + $statement->initialize($this->connection->getResource()); + return $statement; + } + + /** + * Create result + * + * @param resource $resource + * @return Result + */ + public function createResult($resource) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue()); + return $result; + } + + /** + * @return Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * Get prepare Type + * + * @return string + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_POSITIONAL; + } + + /** + * Format parameter name + * + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return '$#'; + } + + /** + * Get last generated value + * + * @param string $name + * @return mixed + */ + public function getLastGeneratedValue($name = null) + { + return $this->connection->getLastGeneratedValue($name); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Result.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Result.php new file mode 100644 index 000000000..24d332e2e --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Result.php @@ -0,0 +1,191 @@ +resource = $resource; + $this->count = pg_num_rows($this->resource); + $this->generatedValue = $generatedValue; + } + + /** + * Current + * + * @return array|bool|mixed + */ + public function current() + { + if ($this->count === 0) { + return false; + } + return pg_fetch_assoc($this->resource, $this->position); + } + + /** + * Next + * + * @return void + */ + public function next() + { + $this->position++; + } + + /** + * Key + * + * @return int|mixed + */ + public function key() + { + return $this->position; + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + return ($this->position < $this->count); + } + + /** + * Rewind + * + * @return void + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Buffer + * + * @return null + */ + public function buffer() + { + return; + } + + /** + * Is buffered + * + * @return false + */ + public function isBuffered() + { + return false; + } + + /** + * Is query result + * + * @return bool + */ + public function isQueryResult() + { + return (pg_num_fields($this->resource) > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + return pg_affected_rows($this->resource); + } + + /** + * Get generated value + * + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } + + /** + * Get resource + */ + public function getResource() + { + // TODO: Implement getResource() method. + } + + /** + * Count + * + * (PHP 5 >= 5.1.0)
+ * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return value is cast to an integer. + */ + public function count() + { + return $this->count; + } + + /** + * Get field count + * + * @return int + */ + public function getFieldCount() + { + return pg_num_fields($this->resource); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Statement.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Statement.php new file mode 100644 index 000000000..d24aefc9f --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Pgsql/Statement.php @@ -0,0 +1,241 @@ +driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Initialize + * + * @param resource $pgsql + * @return void + * @throws Exception\RuntimeException for invalid or missing postgresql connection + */ + public function initialize($pgsql) + { + if (! is_resource($pgsql) || get_resource_type($pgsql) !== 'pgsql link') { + throw new Exception\RuntimeException(sprintf( + '%s: Invalid or missing postgresql connection; received "%s"', + __METHOD__, + get_resource_type($pgsql) + )); + } + $this->pgsql = $pgsql; + } + + /** + * Get resource + * + * @return resource + */ + public function getResource() + { + // TODO: Implement getResource() method. + } + + /** + * Set sql + * + * @param string $sql + * @return self Provides a fluent interface + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * Set parameter container + * + * @param ParameterContainer $parameterContainer + * @return self Provides a fluent interface + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * Get parameter container + * + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * Prepare + * + * @param string $sql + */ + public function prepare($sql = null) + { + $sql = ($sql) ?: $this->sql; + + $pCount = 1; + $sql = preg_replace_callback( + '#\$\##', + function () use (&$pCount) { + return '$' . $pCount++; + }, + $sql + ); + + $this->sql = $sql; + $this->statementName = 'statement' . ++static::$statementIndex; + $this->resource = pg_prepare($this->pgsql, $this->statementName, $sql); + } + + /** + * Is prepared + * + * @return bool + */ + public function isPrepared() + { + return isset($this->resource); + } + + /** + * Execute + * + * @param null|array|ParameterContainer $parameters + * @throws Exception\InvalidQueryException + * @return Result + */ + public function execute($parameters = null) + { + if (! $this->isPrepared()) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (! $this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $parameters = $this->parameterContainer->getPositionalArray(); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + $resultResource = pg_execute($this->pgsql, $this->statementName, (array) $parameters); + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($resultResource === false) { + throw new Exception\InvalidQueryException(pg_last_error()); + } + + $result = $this->driver->createResult($resultResource); + return $result; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/ResultInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/ResultInterface.php new file mode 100644 index 000000000..31240d9a5 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/ResultInterface.php @@ -0,0 +1,66 @@ +setConnectionParameters($connectionInfo); + } elseif (is_resource($connectionInfo)) { + $this->setResource($connectionInfo); + } else { + throw new Exception\InvalidArgumentException('$connection must be an array of parameters or a resource'); + } + } + + /** + * Set driver + * + * @param Sqlsrv $driver + * @return self Provides a fluent interface + */ + public function setDriver(Sqlsrv $driver) + { + $this->driver = $driver; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getCurrentSchema() + { + if (! $this->isConnected()) { + $this->connect(); + } + + $result = sqlsrv_query($this->resource, 'SELECT SCHEMA_NAME()'); + $r = sqlsrv_fetch_array($result); + + return $r[0]; + } + + /** + * Set resource + * + * @param resource $resource + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function setResource($resource) + { + if (get_resource_type($resource) !== 'SQL Server Connection') { + throw new Exception\InvalidArgumentException('Resource provided was not of type SQL Server Connection'); + } + $this->resource = $resource; + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + public function connect() + { + if ($this->resource) { + return $this; + } + + $serverName = '.'; + $params = [ + 'ReturnDatesAsStrings' => true + ]; + foreach ($this->connectionParameters as $key => $value) { + switch (strtolower($key)) { + case 'hostname': + case 'servername': + $serverName = (string) $value; + break; + case 'username': + case 'uid': + $params['UID'] = (string) $value; + break; + case 'password': + case 'pwd': + $params['PWD'] = (string) $value; + break; + case 'database': + case 'dbname': + $params['Database'] = (string) $value; + break; + case 'charset': + $params['CharacterSet'] = (string) $value; + break; + case 'driver_options': + case 'options': + $params = array_merge($params, (array) $value); + break; + } + } + + $this->resource = sqlsrv_connect($serverName, $params); + + if (! $this->resource) { + throw new Exception\RuntimeException( + 'Connect Error', + null, + new ErrorException(sqlsrv_errors()) + ); + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isConnected() + { + return (is_resource($this->resource)); + } + + /** + * {@inheritDoc} + */ + public function disconnect() + { + sqlsrv_close($this->resource); + $this->resource = null; + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + if (! $this->isConnected()) { + $this->connect(); + } + + if (sqlsrv_begin_transaction($this->resource) === false) { + throw new Exception\RuntimeException( + new ErrorException(sqlsrv_errors()) + ); + } + + $this->inTransaction = true; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function commit() + { + // http://msdn.microsoft.com/en-us/library/cc296194.aspx + + if (! $this->isConnected()) { + $this->connect(); + } + + sqlsrv_commit($this->resource); + + $this->inTransaction = false; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function rollback() + { + // http://msdn.microsoft.com/en-us/library/cc296176.aspx + + if (! $this->isConnected()) { + throw new Exception\RuntimeException('Must be connected before you can rollback.'); + } + + sqlsrv_rollback($this->resource); + $this->inTransaction = false; + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + public function execute($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + if (! $this->driver instanceof Sqlsrv) { + throw new Exception\RuntimeException('Connection is missing an instance of Sqlsrv'); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $returnValue = sqlsrv_query($this->resource, $sql); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + // if the returnValue is something other than a Sqlsrv_result, bypass wrapping it + if ($returnValue === false) { + $errors = sqlsrv_errors(); + // ignore general warnings + if ($errors[0]['SQLSTATE'] != '01000') { + throw new Exception\RuntimeException( + 'An exception occurred while trying to execute the provided $sql', + null, + new ErrorException($errors) + ); + } + } + + $result = $this->driver->createResult($returnValue); + + return $result; + } + + /** + * Prepare + * + * @param string $sql + * @return string + */ + public function prepare($sql) + { + if (! $this->isConnected()) { + $this->connect(); + } + + $statement = $this->driver->createStatement($sql); + + return $statement; + } + + /** + * {@inheritDoc} + * + * @return mixed + */ + public function getLastGeneratedValue($name = null) + { + if (! $this->resource) { + $this->connect(); + } + $sql = 'SELECT @@IDENTITY as Current_Identity'; + $result = sqlsrv_query($this->resource, $sql); + $row = sqlsrv_fetch_array($result); + + return $row['Current_Identity']; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ErrorException.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ErrorException.php new file mode 100644 index 000000000..e4db36d2f --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ErrorException.php @@ -0,0 +1,31 @@ +errors = ($errors === false) ? sqlsrv_errors() : $errors; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php new file mode 100644 index 000000000..217fb5905 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php @@ -0,0 +1,15 @@ +resource = $resource; + $this->generatedValue = $generatedValue; + return $this; + } + + /** + * @return null + */ + public function buffer() + { + return; + } + + /** + * @return bool + */ + public function isBuffered() + { + return false; + } + + /** + * Get resource + * + * @return resource + */ + public function getResource() + { + return $this->resource; + } + + /** + * Current + * + * @return mixed + */ + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + $this->load(); + return $this->currentData; + } + + /** + * Next + * + * @return bool + */ + public function next() + { + $this->load(); + return true; + } + + /** + * Load + * + * @param int $row + * @return mixed + */ + protected function load($row = SQLSRV_SCROLL_NEXT) + { + $this->currentData = sqlsrv_fetch_array($this->resource, SQLSRV_FETCH_ASSOC, $row); + $this->currentComplete = true; + $this->position++; + return $this->currentData; + } + + /** + * Key + * + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * Rewind + * + * @return bool + */ + public function rewind() + { + $this->position = 0; + $this->load(SQLSRV_SCROLL_FIRST); + return true; + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + if ($this->currentComplete && $this->currentData) { + return true; + } + + return $this->load(); + } + + /** + * Count + * + * @return int + */ + public function count() + { + return sqlsrv_num_rows($this->resource); + } + + /** + * @return bool|int + */ + public function getFieldCount() + { + return sqlsrv_num_fields($this->resource); + } + + /** + * Is query result + * + * @return bool + */ + public function isQueryResult() + { + if (is_bool($this->resource)) { + return false; + } + return (sqlsrv_num_fields($this->resource) > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + return sqlsrv_rows_affected($this->resource); + } + + /** + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Sqlsrv.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Sqlsrv.php new file mode 100644 index 000000000..9c2860712 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Sqlsrv.php @@ -0,0 +1,222 @@ +registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return self Provides a fluent interface + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statementPrototype + * @return self Provides a fluent interface + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); + return $this; + } + + /** + * Register result prototype + * + * @param Result $resultPrototype + * @return self Provides a fluent interface + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + return $this; + } + + /** + * Get database paltform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + return 'SqlServer'; + } + + return 'SQLServer'; + } + + /** + * Check environment + * + * @throws Exception\RuntimeException + * @return void + */ + public function checkEnvironment() + { + if (! extension_loaded('sqlsrv')) { + throw new Exception\RuntimeException( + 'The Sqlsrv extension is required for this adapter but the extension is not loaded' + ); + } + } + + /** + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * @param string|resource $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + if (is_resource($sqlOrResource)) { + $statement->initialize($sqlOrResource); + } else { + if (! $this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } elseif ($sqlOrResource !== null) { + throw new Exception\InvalidArgumentException( + 'createStatement() only accepts an SQL string or a Sqlsrv resource' + ); + } + } + return $statement; + } + + /** + * @param resource $resource + * @return Result + */ + public function createResult($resource) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue()); + return $result; + } + + /** + * @return Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * @return string + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_POSITIONAL; + } + + /** + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return '?'; + } + + /** + * @return mixed + */ + public function getLastGeneratedValue() + { + return $this->getConnection()->getLastGeneratedValue(); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Statement.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Statement.php new file mode 100644 index 000000000..267e15434 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/Sqlsrv/Statement.php @@ -0,0 +1,316 @@ +driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return self Provides a fluent interface + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * + * One of two resource types will be provided here: + * a) "SQL Server Connection" when a prepared statement needs to still be produced + * b) "SQL Server Statement" when a prepared statement has been already produced + * (there will need to already be a bound param set if it applies to this query) + * + * @param resource $resource + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function initialize($resource) + { + $resourceType = get_resource_type($resource); + + if ($resourceType == 'SQL Server Connection') { + $this->sqlsrv = $resource; + } elseif ($resourceType == 'SQL Server Statement') { + $this->resource = $resource; + $this->isPrepared = true; + } else { + throw new Exception\InvalidArgumentException('Invalid resource provided to ' . __CLASS__); + } + + return $this; + } + + /** + * Set parameter container + * + * @param ParameterContainer $parameterContainer + * @return self Provides a fluent interface + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * @param $resource + * @return self Provides a fluent interface + */ + public function setResource($resource) + { + $this->resource = $resource; + return $this; + } + + /** + * Get resource + * + * @return resource + */ + public function getResource() + { + return $this->resource; + } + + /** + * @param string $sql + * @return self Provides a fluent interface + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @param string $sql + * @param array $options + * @return self Provides a fluent interface + * @throws Exception\RuntimeException + */ + public function prepare($sql = null, array $options = []) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('Already prepared'); + } + $sql = ($sql) ?: $this->sql; + $options = ($options) ?: $this->prepareOptions; + + $pRef = &$this->parameterReferences; + for ($position = 0, $count = substr_count($sql, '?'); $position < $count; $position++) { + if (! isset($this->prepareParams[$position])) { + $pRef[$position] = [&$this->parameterReferenceValues[$position], SQLSRV_PARAM_IN, null, null]; + } else { + $pRef[$position] = &$this->prepareParams[$position]; + } + } + + $this->resource = sqlsrv_prepare($this->sqlsrv, $sql, $pRef, $options); + + $this->isPrepared = true; + + return $this; + } + + /** + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * Execute + * + * @param null|array|ParameterContainer $parameters + * @throws Exception\RuntimeException + * @return Result + */ + public function execute($parameters = null) + { + /** END Standard ParameterContainer Merging Block */ + if (! $this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (! $this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + $resultValue = sqlsrv_execute($this->resource); + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($resultValue === false) { + $errors = sqlsrv_errors(); + // ignore general warnings + if ($errors[0]['SQLSTATE'] != '01000') { + throw new Exception\RuntimeException($errors[0]['message']); + } + } + + $result = $this->driver->createResult($this->resource); + return $result; + } + + /** + * Bind parameters from container + * + */ + protected function bindParametersFromContainer() + { + $values = $this->parameterContainer->getPositionalArray(); + $position = 0; + foreach ($values as $value) { + $this->parameterReferences[$position++][0] = $value; + } + } + + /** + * @param array $prepareParams + */ + public function setPrepareParams(array $prepareParams) + { + $this->prepareParams = $prepareParams; + } + + /** + * @param array $prepareOptions + */ + public function setPrepareOptions(array $prepareOptions) + { + $this->prepareOptions = $prepareOptions; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Driver/StatementInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/StatementInterface.php new file mode 100644 index 000000000..e55763844 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Driver/StatementInterface.php @@ -0,0 +1,44 @@ +parameters = $parameters; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Exception/InvalidQueryException.php b/bundled-libs/laminas/laminas-db/src/Adapter/Exception/InvalidQueryException.php new file mode 100644 index 000000000..ea6603180 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Exception/InvalidQueryException.php @@ -0,0 +1,13 @@ +setFromArray($data); + } + } + + /** + * Offset exists + * + * @param string $name + * @return bool + */ + public function offsetExists($name) + { + return (isset($this->data[$name])); + } + + /** + * Offset get + * + * @param string $name + * @return mixed + */ + public function offsetGet($name) + { + if (isset($this->data[$name])) { + return $this->data[$name]; + } + + $normalizedName = ltrim($name, ':'); + if (isset($this->nameMapping[$normalizedName]) + && isset($this->data[$this->nameMapping[$normalizedName]]) + ) { + return $this->data[$this->nameMapping[$normalizedName]]; + } + + return null; + } + + /** + * @param $name + * @param $from + */ + public function offsetSetReference($name, $from) + { + $this->data[$name] =& $this->data[$from]; + } + + /** + * Offset set + * + * @param string|int $name + * @param mixed $value + * @param mixed $errata + * @param mixed $maxLength + * @throws Exception\InvalidArgumentException + */ + public function offsetSet($name, $value, $errata = null, $maxLength = null) + { + $position = false; + + // if integer, get name for this position + if (is_int($name)) { + if (isset($this->positions[$name])) { + $position = $name; + $name = $this->positions[$name]; + } else { + $name = (string) $name; + } + } elseif (is_string($name)) { + // is a string: + $normalizedName = ltrim($name, ':'); + if (isset($this->nameMapping[$normalizedName])) { + // We have a mapping; get real name from it + $name = $this->nameMapping[$normalizedName]; + } + + $position = array_key_exists($name, $this->data); + + // @todo: this assumes that any data begining with a ":" will be considered a parameter + if (strpos($value, ':') === 0) { + // We have a named parameter; handle name mapping (container creation) + $this->nameMapping[ltrim($value, ':')] = $name; + } + } elseif ($name === null) { + $name = (string) count($this->data); + } else { + throw new Exception\InvalidArgumentException('Keys must be string, integer or null'); + } + + if ($position === false) { + $this->positions[] = $name; + } + + $this->data[$name] = $value; + + if ($errata) { + $this->offsetSetErrata($name, $errata); + } + + if ($maxLength) { + $this->offsetSetMaxLength($name, $maxLength); + } + } + + /** + * Offset unset + * + * @param string $name + * @return self Provides a fluent interface + */ + public function offsetUnset($name) + { + if (is_int($name) && isset($this->positions[$name])) { + $name = $this->positions[$name]; + } + unset($this->data[$name]); + return $this; + } + + /** + * Set from array + * + * @param array $data + * @return self Provides a fluent interface + */ + public function setFromArray(array $data) + { + foreach ($data as $n => $v) { + $this->offsetSet($n, $v); + } + return $this; + } + + /** + * Offset set max length + * + * @param string|int $name + * @param mixed $maxLength + */ + public function offsetSetMaxLength($name, $maxLength) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + $this->maxLength[$name] = $maxLength; + } + + /** + * Offset get max length + * + * @param string|int $name + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function offsetGetMaxLength($name) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + if (! array_key_exists($name, $this->data)) { + throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + } + return $this->maxLength[$name]; + } + + /** + * Offset has max length + * + * @param string|int $name + * @return bool + */ + public function offsetHasMaxLength($name) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + return (isset($this->maxLength[$name])); + } + + /** + * Offset unset max length + * + * @param string|int $name + * @throws Exception\InvalidArgumentException + */ + public function offsetUnsetMaxLength($name) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + if (! array_key_exists($name, $this->maxLength)) { + throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + } + $this->maxLength[$name] = null; + } + + /** + * Get max length iterator + * + * @return \ArrayIterator + */ + public function getMaxLengthIterator() + { + return new \ArrayIterator($this->maxLength); + } + + /** + * Offset set errata + * + * @param string|int $name + * @param mixed $errata + */ + public function offsetSetErrata($name, $errata) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + $this->errata[$name] = $errata; + } + + /** + * Offset get errata + * + * @param string|int $name + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function offsetGetErrata($name) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + if (! array_key_exists($name, $this->data)) { + throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + } + return $this->errata[$name]; + } + + /** + * Offset has errata + * + * @param string|int $name + * @return bool + */ + public function offsetHasErrata($name) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + return (isset($this->errata[$name])); + } + + /** + * Offset unset errata + * + * @param string|int $name + * @throws Exception\InvalidArgumentException + */ + public function offsetUnsetErrata($name) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + if (! array_key_exists($name, $this->errata)) { + throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + } + $this->errata[$name] = null; + } + + /** + * Get errata iterator + * + * @return \ArrayIterator + */ + public function getErrataIterator() + { + return new \ArrayIterator($this->errata); + } + + /** + * getNamedArray + * + * @return array + */ + public function getNamedArray() + { + return $this->data; + } + + /** + * getNamedArray + * + * @return array + */ + public function getPositionalArray() + { + return array_values($this->data); + } + + /** + * count + * + * @return int + */ + public function count() + { + return count($this->data); + } + + /** + * Current + * + * @return mixed + */ + public function current() + { + return current($this->data); + } + + /** + * Next + * + * @return mixed + */ + public function next() + { + return next($this->data); + } + + /** + * Key + * + * @return mixed + */ + public function key() + { + return key($this->data); + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + return (current($this->data) !== false); + } + + /** + * Rewind + */ + public function rewind() + { + reset($this->data); + } + + /** + * @param array|ParameterContainer $parameters + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function merge($parameters) + { + if (! is_array($parameters) && ! $parameters instanceof ParameterContainer) { + throw new Exception\InvalidArgumentException( + '$parameters must be an array or an instance of ParameterContainer' + ); + } + + if (count($parameters) == 0) { + return $this; + } + + if ($parameters instanceof ParameterContainer) { + $parameters = $parameters->getNamedArray(); + } + + foreach ($parameters as $key => $value) { + if (is_int($key)) { + $key = null; + } + $this->offsetSet($key, $value); + } + return $this; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Platform/AbstractPlatform.php b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/AbstractPlatform.php new file mode 100644 index 000000000..465372069 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/AbstractPlatform.php @@ -0,0 +1,141 @@ +quoteIdentifiers) { + return $identifier; + } + + $safeWordsInt = ['*' => true, ' ' => true, '.' => true, 'as' => true]; + + foreach ($safeWords as $sWord) { + $safeWordsInt[strtolower($sWord)] = true; + } + + $parts = preg_split( + $this->quoteIdentifierFragmentPattern, + $identifier, + -1, + PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY + ); + + $identifier = ''; + + foreach ($parts as $part) { + $identifier .= isset($safeWordsInt[strtolower($part)]) + ? $part + : $this->quoteIdentifier[0] + . str_replace($this->quoteIdentifier[0], $this->quoteIdentifierTo, $part) + . $this->quoteIdentifier[1]; + } + + return $identifier; + } + + /** + * {@inheritDoc} + */ + public function quoteIdentifier($identifier) + { + if (! $this->quoteIdentifiers) { + return $identifier; + } + + return $this->quoteIdentifier[0] + . str_replace($this->quoteIdentifier[0], $this->quoteIdentifierTo, $identifier) + . $this->quoteIdentifier[1]; + } + + /** + * {@inheritDoc} + */ + public function quoteIdentifierChain($identifierChain) + { + return '"' . implode('"."', (array) str_replace('"', '\\"', $identifierChain)) . '"'; + } + + /** + * {@inheritDoc} + */ + public function getQuoteIdentifierSymbol() + { + return $this->quoteIdentifier[0]; + } + + /** + * {@inheritDoc} + */ + public function getQuoteValueSymbol() + { + return '\''; + } + + /** + * {@inheritDoc} + */ + public function quoteValue($value) + { + trigger_error( + 'Attempting to quote a value in ' . get_class($this) . + ' without extension/driver support can introduce security vulnerabilities in a production environment' + ); + return '\'' . addcslashes((string) $value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * {@inheritDoc} + */ + public function quoteTrustedValue($value) + { + return '\'' . addcslashes((string) $value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * {@inheritDoc} + */ + public function quoteValueList($valueList) + { + return implode(', ', array_map([$this, 'quoteValue'], (array) $valueList)); + } + + /** + * {@inheritDoc} + */ + public function getIdentifierSeparator() + { + return '.'; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Platform/IbmDb2.php b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/IbmDb2.php new file mode 100644 index 000000000..29734199a --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/IbmDb2.php @@ -0,0 +1,124 @@ +quoteIdentifiers = false; + } + + if (isset($options['identifier_separator'])) { + $this->identifierSeparator = $options['identifier_separator']; + } + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'IBM DB2'; + } + + /** + * {@inheritDoc} + */ + public function quoteIdentifierInFragment($identifier, array $safeWords = []) + { + if (! $this->quoteIdentifiers) { + return $identifier; + } + $safeWordsInt = ['*' => true, ' ' => true, '.' => true, 'as' => true]; + foreach ($safeWords as $sWord) { + $safeWordsInt[strtolower($sWord)] = true; + } + $parts = preg_split( + '/([^0-9,a-z,A-Z$#_:])/i', + $identifier, + -1, + PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY + ); + $identifier = ''; + foreach ($parts as $part) { + $identifier .= isset($safeWordsInt[strtolower($part)]) + ? $part + : $this->quoteIdentifier[0] + . str_replace($this->quoteIdentifier[0], $this->quoteIdentifierTo, $part) + . $this->quoteIdentifier[1]; + } + return $identifier; + } + + /** + * {@inheritDoc} + */ + public function quoteIdentifierChain($identifierChain) + { + if ($this->quoteIdentifiers === false) { + if (is_array($identifierChain)) { + return implode($this->identifierSeparator, $identifierChain); + } else { + return $identifierChain; + } + } + $identifierChain = str_replace('"', '\\"', $identifierChain); + if (is_array($identifierChain)) { + $identifierChain = implode('"' . $this->identifierSeparator . '"', $identifierChain); + } + return '"' . $identifierChain . '"'; + } + + /** + * {@inheritDoc} + */ + public function quoteValue($value) + { + if (function_exists('db2_escape_string')) { + return '\'' . db2_escape_string($value) . '\''; + } + trigger_error( + 'Attempting to quote a value in ' . __CLASS__ . ' without extension/driver support ' + . 'can introduce security vulnerabilities in a production environment.' + ); + return '\'' . str_replace("'", "''", $value) . '\''; + } + + /** + * {@inheritDoc} + */ + public function quoteTrustedValue($value) + { + if (function_exists('db2_escape_string')) { + return '\'' . db2_escape_string($value) . '\''; + } + return '\'' . str_replace("'", "''", $value) . '\''; + } + + /** + * {@inheritDoc} + */ + public function getIdentifierSeparator() + { + return $this->identifierSeparator; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Mysql.php b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Mysql.php new file mode 100644 index 000000000..6f30ddce4 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Mysql.php @@ -0,0 +1,129 @@ +setDriver($driver); + } + } + + /** + * @param \Laminas\Db\Adapter\Driver\Mysqli\Mysqli|\Laminas\Db\Adapter\Driver\Pdo\Pdo|\mysqli|\PDO $driver + * @return self Provides a fluent interface + * @throws \Laminas\Db\Adapter\Exception\InvalidArgumentException + */ + public function setDriver($driver) + { + // handle Laminas\Db drivers + if ($driver instanceof Mysqli\Mysqli + || ($driver instanceof Pdo\Pdo && $driver->getDatabasePlatformName() == 'Mysql') + || ($driver instanceof \mysqli) + || ($driver instanceof \PDO && $driver->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'mysql') + ) { + $this->driver = $driver; + return $this; + } + + throw new Exception\InvalidArgumentException( + '$driver must be a Mysqli or Mysql PDO Laminas\Db\Adapter\Driver, Mysqli instance or MySQL PDO instance' + ); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'MySQL'; + } + + /** + * {@inheritDoc} + */ + public function quoteIdentifierChain($identifierChain) + { + return '`' . implode('`.`', (array) str_replace('`', '``', $identifierChain)) . '`'; + } + + /** + * {@inheritDoc} + */ + public function quoteValue($value) + { + $quotedViaDriverValue = $this->quoteViaDriver($value); + + return $quotedViaDriverValue !== null ? $quotedViaDriverValue : parent::quoteValue($value); + } + + /** + * {@inheritDoc} + */ + public function quoteTrustedValue($value) + { + $quotedViaDriverValue = $this->quoteViaDriver($value); + + return $quotedViaDriverValue !== null ? $quotedViaDriverValue : parent::quoteTrustedValue($value); + } + + /** + * @param string $value + * @return string|null + */ + protected function quoteViaDriver($value) + { + if ($this->driver instanceof DriverInterface) { + $resource = $this->driver->getConnection()->getResource(); + } else { + $resource = $this->driver; + } + + if ($resource instanceof \mysqli) { + return '\'' . $resource->real_escape_string($value) . '\''; + } + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + + return null; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Oracle.php b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Oracle.php new file mode 100644 index 000000000..53d4217c3 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Oracle.php @@ -0,0 +1,129 @@ +quoteIdentifiers = false; + } + + if ($driver) { + $this->setDriver($driver); + } + } + + /** + * @param Pdo|Oci8 $driver + * @return self Provides a fluent interface + * @throws InvalidArgumentException + */ + public function setDriver($driver) + { + if ($driver instanceof Oci8 + || ($driver instanceof Pdo && $driver->getDatabasePlatformName() == 'Oracle') + || ($driver instanceof \oci8) + || ($driver instanceof \PDO && $driver->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'oci') + ) { + $this->resource = $driver; + return $this; + } + + throw new InvalidArgumentException( + '$driver must be a Oci8 or Oracle PDO Laminas\Db\Adapter\Driver, ' + . 'Oci8 instance, or Oci PDO instance' + ); + } + + /** + * @return null|Pdo|Oci8 + */ + public function getDriver() + { + return $this->resource; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'Oracle'; + } + + /** + * {@inheritDoc} + */ + public function quoteIdentifierChain($identifierChain) + { + if ($this->quoteIdentifiers === false) { + return implode('.', (array) $identifierChain); + } + + return '"' . implode('"."', (array) str_replace('"', '\\"', $identifierChain)) . '"'; + } + + /** + * {@inheritDoc} + */ + public function quoteValue($value) + { + if ($this->resource instanceof DriverInterface) { + $resource = $this->resource->getConnection()->getResource(); + } else { + $resource = $this->resource; + } + + if ($resource) { + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + + if (get_resource_type($resource) == 'oci8 connection' + || get_resource_type($resource) == 'oci8 persistent connection' + ) { + return "'" . addcslashes(str_replace("'", "''", $value), "\x00\n\r\"\x1a") . "'"; + } + } + + trigger_error( + 'Attempting to quote a value in ' . __CLASS__ . ' without extension/driver support ' + . 'can introduce security vulnerabilities in a production environment.' + ); + + return "'" . addcslashes(str_replace("'", "''", $value), "\x00\n\r\"\x1a") . "'"; + } + + /** + * {@inheritDoc} + */ + public function quoteTrustedValue($value) + { + return "'" . addcslashes(str_replace('\'', '\'\'', $value), "\x00\n\r\"\x1a") . "'"; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Platform/PlatformInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/PlatformInterface.php new file mode 100644 index 000000000..7b504f435 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/PlatformInterface.php @@ -0,0 +1,93 @@ +setDriver($driver); + } + } + + /** + * @param \Laminas\Db\Adapter\Driver\Pgsql\Pgsql|\Laminas\Db\Adapter\Driver\Pdo\Pdo|resource|\PDO $driver + * @return self Provides a fluent interface + * @throws \Laminas\Db\Adapter\Exception\InvalidArgumentException + */ + public function setDriver($driver) + { + if ($driver instanceof Pgsql\Pgsql + || ($driver instanceof Pdo\Pdo && $driver->getDatabasePlatformName() == 'Postgresql') + || (is_resource($driver) && (in_array(get_resource_type($driver), ['pgsql link', 'pgsql link persistent']))) + || ($driver instanceof \PDO && $driver->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'pgsql') + ) { + $this->driver = $driver; + return $this; + } + + throw new Exception\InvalidArgumentException( + '$driver must be a Pgsql or Postgresql PDO Laminas\Db\Adapter\Driver, pgsql link resource' + . ' or Postgresql PDO instance' + ); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'PostgreSQL'; + } + + /** + * {@inheritDoc} + */ + public function quoteIdentifierChain($identifierChain) + { + return '"' . implode('"."', (array) str_replace('"', '""', $identifierChain)) . '"'; + } + + /** + * {@inheritDoc} + */ + public function quoteValue($value) + { + $quotedViaDriverValue = $this->quoteViaDriver($value); + + return $quotedViaDriverValue !== null ? $quotedViaDriverValue : ('E' . parent::quoteValue($value)); + } + + /** + * {@inheritDoc} + */ + public function quoteTrustedValue($value) + { + $quotedViaDriverValue = $this->quoteViaDriver($value); + + return $quotedViaDriverValue !== null ? $quotedViaDriverValue : ('E' . parent::quoteTrustedValue($value)); + } + + /** + * @param string $value + * @return string|null + */ + protected function quoteViaDriver($value) + { + if ($this->driver instanceof DriverInterface) { + $resource = $this->driver->getConnection()->getResource(); + } else { + $resource = $this->driver; + } + + if (is_resource($resource)) { + return '\'' . pg_escape_string($resource, $value) . '\''; + } + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + + return null; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Sql92.php b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Sql92.php new file mode 100644 index 000000000..0bafe993e --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Sql92.php @@ -0,0 +1,32 @@ +setDriver($driver); + } + } + + /** + * @param \Laminas\Db\Adapter\Driver\Sqlsrv\Sqlsrv|\Laminas\Db\Adapter\Driver\Pdo\Pdo|resource|\PDO $driver + * @return self Provides a fluent interface + * @throws \Laminas\Db\Adapter\Exception\InvalidArgumentException + */ + public function setDriver($driver) + { + // handle Laminas\Db drivers + if (($driver instanceof Pdo\Pdo && in_array($driver->getDatabasePlatformName(), ['SqlServer', 'Dblib'])) + || ($driver instanceof \PDO && in_array($driver->getAttribute(\PDO::ATTR_DRIVER_NAME), ['sqlsrv', 'dblib'])) + ) { + $this->resource = $driver; + return $this; + } + + throw new Exception\InvalidArgumentException( + '$driver must be a Sqlsrv PDO Laminas\Db\Adapter\Driver or Sqlsrv PDO instance' + ); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'SQLServer'; + } + + /** + * {@inheritDoc} + */ + public function getQuoteIdentifierSymbol() + { + return $this->quoteIdentifier; + } + + /** + * {@inheritDoc} + */ + public function quoteIdentifierChain($identifierChain) + { + return '[' . implode('].[', (array) $identifierChain) . ']'; + } + + /** + * {@inheritDoc} + */ + public function quoteValue($value) + { + $resource = $this->resource; + + if ($resource instanceof DriverInterface) { + $resource = $resource->getConnection()->getResource(); + } + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + trigger_error( + 'Attempting to quote a value in ' . __CLASS__ . ' without extension/driver support ' + . 'can introduce security vulnerabilities in a production environment.' + ); + + return '\'' . str_replace('\'', '\'\'', addcslashes($value, "\000\032")) . '\''; + } + + /** + * {@inheritDoc} + */ + public function quoteTrustedValue($value) + { + $resource = $this->resource; + + if ($resource instanceof DriverInterface) { + $resource = $resource->getConnection()->getResource(); + } + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + + return '\'' . str_replace('\'', '\'\'', $value) . '\''; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Sqlite.php b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Sqlite.php new file mode 100644 index 000000000..d079bf048 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Platform/Sqlite.php @@ -0,0 +1,104 @@ +setDriver($driver); + } + } + + /** + * @param \Laminas\Db\Adapter\Driver\Pdo\Pdo|\PDO $driver + * @return self Provides a fluent interface + * @throws \Laminas\Db\Adapter\Exception\InvalidArgumentException + */ + public function setDriver($driver) + { + if (($driver instanceof \PDO && $driver->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'sqlite') + || ($driver instanceof Pdo\Pdo && $driver->getDatabasePlatformName() == 'Sqlite') + ) { + $this->resource = $driver; + return $this; + } + + throw new Exception\InvalidArgumentException( + '$driver must be a Sqlite PDO Laminas\Db\Adapter\Driver, Sqlite PDO instance' + ); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'SQLite'; + } + + /** + * {@inheritDoc} + */ + public function quoteValue($value) + { + $resource = $this->resource; + + if ($resource instanceof DriverInterface) { + $resource = $resource->getConnection()->getResource(); + } + + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + + return parent::quoteValue($value); + } + + /** + * {@inheritDoc} + */ + public function quoteTrustedValue($value) + { + $resource = $this->resource; + + if ($resource instanceof DriverInterface) { + $resource = $resource->getConnection()->getResource(); + } + + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + + return parent::quoteTrustedValue($value); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Profiler/Profiler.php b/bundled-libs/laminas/laminas-db/src/Adapter/Profiler/Profiler.php new file mode 100644 index 000000000..6447be108 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Profiler/Profiler.php @@ -0,0 +1,88 @@ + '', + 'parameters' => null, + 'start' => microtime(true), + 'end' => null, + 'elapse' => null + ]; + if ($target instanceof StatementContainerInterface) { + $profileInformation['sql'] = $target->getSql(); + $profileInformation['parameters'] = clone $target->getParameterContainer(); + } elseif (is_string($target)) { + $profileInformation['sql'] = $target; + } else { + throw new Exception\InvalidArgumentException( + __FUNCTION__ . ' takes either a StatementContainer or a string' + ); + } + + $this->profiles[$this->currentIndex] = $profileInformation; + + return $this; + } + + /** + * @return self Provides a fluent interface + */ + public function profilerFinish() + { + if (! isset($this->profiles[$this->currentIndex])) { + throw new Exception\RuntimeException( + 'A profile must be started before ' . __FUNCTION__ . ' can be called.' + ); + } + $current = &$this->profiles[$this->currentIndex]; + $current['end'] = microtime(true); + $current['elapse'] = $current['end'] - $current['start']; + $this->currentIndex++; + return $this; + } + + /** + * @return array|null + */ + public function getLastProfile() + { + return end($this->profiles); + } + + /** + * @return array + */ + public function getProfiles() + { + return $this->profiles; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/Profiler/ProfilerAwareInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/Profiler/ProfilerAwareInterface.php new file mode 100644 index 000000000..ce202864c --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/Profiler/ProfilerAwareInterface.php @@ -0,0 +1,17 @@ +setSql($sql); + } + $this->parameterContainer = ($parameterContainer) ?: new ParameterContainer; + } + + /** + * @param $sql + * @return self Provides a fluent interface + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @param ParameterContainer $parameterContainer + * @return self Provides a fluent interface + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * @return null|ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Adapter/StatementContainerInterface.php b/bundled-libs/laminas/laminas-db/src/Adapter/StatementContainerInterface.php new file mode 100644 index 000000000..b11c186a7 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Adapter/StatementContainerInterface.php @@ -0,0 +1,42 @@ + $this->getDependencyConfig(), + ]; + } + + /** + * Retrieve laminas-db default dependency configuration. + * + * @return array + */ + public function getDependencyConfig() + { + return [ + 'abstract_factories' => [ + Adapter\AdapterAbstractServiceFactory::class, + ], + 'factories' => [ + Adapter\AdapterInterface::class => Adapter\AdapterServiceFactory::class, + ], + 'aliases' => [ + Adapter\Adapter::class => Adapter\AdapterInterface::class, + + // Legacy Zend Framework aliases + \Zend\Db\Adapter\AdapterInterface::class => Adapter\AdapterInterface::class, + \Zend\Db\Adapter\Adapter::class => Adapter\Adapter::class, + ], + ]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Exception/ErrorException.php b/bundled-libs/laminas/laminas-db/src/Exception/ErrorException.php new file mode 100644 index 000000000..d8daa59d1 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Exception/ErrorException.php @@ -0,0 +1,13 @@ +source = Source\Factory::createSourceFromAdapter($adapter); + } + + /** + * {@inheritdoc} + */ + public function getTables($schema = null, $includeViews = false) + { + return $this->source->getTables($schema, $includeViews); + } + + /** + * {@inheritdoc} + */ + public function getViews($schema = null) + { + return $this->source->getViews($schema); + } + + /** + * {@inheritdoc} + */ + public function getTriggers($schema = null) + { + return $this->source->getTriggers($schema); + } + + /** + * {@inheritdoc} + */ + public function getConstraints($table, $schema = null) + { + return $this->source->getConstraints($table, $schema); + } + + /** + * {@inheritdoc} + */ + public function getColumns($table, $schema = null) + { + return $this->source->getColumns($table, $schema); + } + + /** + * {@inheritdoc} + */ + public function getConstraintKeys($constraint, $table, $schema = null) + { + return $this->source->getConstraintKeys($constraint, $table, $schema); + } + + /** + * {@inheritdoc} + */ + public function getConstraint($constraintName, $table, $schema = null) + { + return $this->source->getConstraint($constraintName, $table, $schema); + } + + /** + * {@inheritdoc} + */ + public function getSchemas() + { + return $this->source->getSchemas(); + } + + /** + * {@inheritdoc} + */ + public function getTableNames($schema = null, $includeViews = false) + { + return $this->source->getTableNames($schema, $includeViews); + } + + /** + * {@inheritdoc} + */ + public function getTable($tableName, $schema = null) + { + return $this->source->getTable($tableName, $schema); + } + + /** + * {@inheritdoc} + */ + public function getViewNames($schema = null) + { + return $this->source->getViewNames($schema); + } + + /** + * {@inheritdoc} + */ + public function getView($viewName, $schema = null) + { + return $this->source->getView($viewName, $schema); + } + + /** + * {@inheritdoc} + */ + public function getTriggerNames($schema = null) + { + return $this->source->getTriggerNames($schema); + } + + /** + * {@inheritdoc} + */ + public function getTrigger($triggerName, $schema = null) + { + return $this->source->getTrigger($triggerName, $schema); + } + + /** + * {@inheritdoc} + */ + public function getColumnNames($table, $schema = null) + { + return $this->source->getColumnNames($table, $schema); + } + + /** + * {@inheritdoc} + */ + public function getColumn($columnName, $table, $schema = null) + { + return $this->source->getColumn($columnName, $table, $schema); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/MetadataInterface.php b/bundled-libs/laminas/laminas-db/src/Metadata/MetadataInterface.php new file mode 100644 index 000000000..f1a493ffb --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/MetadataInterface.php @@ -0,0 +1,153 @@ +setName($name); + } + } + + /** + * Set columns + * + * @param array $columns + */ + public function setColumns(array $columns) + { + $this->columns = $columns; + } + + /** + * Get columns + * + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Set constraints + * + * @param array $constraints + */ + public function setConstraints($constraints) + { + $this->constraints = $constraints; + } + + /** + * Get constraints + * + * @return array + */ + public function getConstraints() + { + return $this->constraints; + } + + /** + * Set name + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Object/ColumnObject.php b/bundled-libs/laminas/laminas-db/src/Metadata/Object/ColumnObject.php new file mode 100644 index 000000000..be630809e --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Object/ColumnObject.php @@ -0,0 +1,387 @@ +setName($name); + $this->setTableName($tableName); + $this->setSchemaName($schemaName); + } + + /** + * Set name + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get table name + * + * @return string + */ + public function getTableName() + { + return $this->tableName; + } + + /** + * Set table name + * + * @param string $tableName + * @return self Provides a fluent interface + */ + public function setTableName($tableName) + { + $this->tableName = $tableName; + return $this; + } + + /** + * Set schema name + * + * @param string $schemaName + */ + public function setSchemaName($schemaName) + { + $this->schemaName = $schemaName; + } + + /** + * Get schema name + * + * @return string + */ + public function getSchemaName() + { + return $this->schemaName; + } + + /** + * @return int $ordinalPosition + */ + public function getOrdinalPosition() + { + return $this->ordinalPosition; + } + + /** + * @param int $ordinalPosition to set + * @return self Provides a fluent interface + */ + public function setOrdinalPosition($ordinalPosition) + { + $this->ordinalPosition = $ordinalPosition; + return $this; + } + + /** + * @return null|string the $columnDefault + */ + public function getColumnDefault() + { + return $this->columnDefault; + } + + /** + * @param mixed $columnDefault to set + * @return self Provides a fluent interface + */ + public function setColumnDefault($columnDefault) + { + $this->columnDefault = $columnDefault; + return $this; + } + + /** + * @return bool $isNullable + */ + public function getIsNullable() + { + return $this->isNullable; + } + + /** + * @param bool $isNullable to set + * @return self Provides a fluent interface + */ + public function setIsNullable($isNullable) + { + $this->isNullable = $isNullable; + return $this; + } + + /** + * @return bool $isNullable + */ + public function isNullable() + { + return $this->isNullable; + } + + /** + * @return null|string the $dataType + */ + public function getDataType() + { + return $this->dataType; + } + + /** + * @param string $dataType the $dataType to set + * @return self Provides a fluent interface + */ + public function setDataType($dataType) + { + $this->dataType = $dataType; + return $this; + } + + /** + * @return int|null the $characterMaximumLength + */ + public function getCharacterMaximumLength() + { + return $this->characterMaximumLength; + } + + /** + * @param int $characterMaximumLength the $characterMaximumLength to set + * @return self Provides a fluent interface + */ + public function setCharacterMaximumLength($characterMaximumLength) + { + $this->characterMaximumLength = $characterMaximumLength; + return $this; + } + + /** + * @return int|null the $characterOctetLength + */ + public function getCharacterOctetLength() + { + return $this->characterOctetLength; + } + + /** + * @param int $characterOctetLength the $characterOctetLength to set + * @return self Provides a fluent interface + */ + public function setCharacterOctetLength($characterOctetLength) + { + $this->characterOctetLength = $characterOctetLength; + return $this; + } + + /** + * @return int the $numericPrecision + */ + public function getNumericPrecision() + { + return $this->numericPrecision; + } + + /** + * @param int $numericPrecision the $numericPrevision to set + * @return self Provides a fluent interface + */ + public function setNumericPrecision($numericPrecision) + { + $this->numericPrecision = $numericPrecision; + return $this; + } + + /** + * @return int the $numericScale + */ + public function getNumericScale() + { + return $this->numericScale; + } + + /** + * @param int $numericScale the $numericScale to set + * @return self Provides a fluent interface + */ + public function setNumericScale($numericScale) + { + $this->numericScale = $numericScale; + return $this; + } + + /** + * @return bool + */ + public function getNumericUnsigned() + { + return $this->numericUnsigned; + } + + /** + * @param bool $numericUnsigned + * @return self Provides a fluent interface + */ + public function setNumericUnsigned($numericUnsigned) + { + $this->numericUnsigned = $numericUnsigned; + return $this; + } + + /** + * @return bool + */ + public function isNumericUnsigned() + { + return $this->numericUnsigned; + } + + /** + * @return array the $errata + */ + public function getErratas() + { + return $this->errata; + } + + /** + * @param array $erratas + * @return self Provides a fluent interface + */ + public function setErratas(array $erratas) + { + foreach ($erratas as $name => $value) { + $this->setErrata($name, $value); + } + return $this; + } + + /** + * @param string $errataName + * @return mixed + */ + public function getErrata($errataName) + { + if (array_key_exists($errataName, $this->errata)) { + return $this->errata[$errataName]; + } + return; + } + + /** + * @param string $errataName + * @param mixed $errataValue + * @return self Provides a fluent interface + */ + public function setErrata($errataName, $errataValue) + { + $this->errata[$errataName] = $errataValue; + return $this; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Object/ConstraintKeyObject.php b/bundled-libs/laminas/laminas-db/src/Metadata/Object/ConstraintKeyObject.php new file mode 100644 index 000000000..f3767be80 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Object/ConstraintKeyObject.php @@ -0,0 +1,248 @@ +setColumnName($column); + } + + /** + * Get column name + * + * @return string + */ + public function getColumnName() + { + return $this->columnName; + } + + /** + * Set column name + * + * @param string $columnName + * @return self Provides a fluent interface + */ + public function setColumnName($columnName) + { + $this->columnName = $columnName; + return $this; + } + + /** + * Get ordinal position + * + * @return int + */ + public function getOrdinalPosition() + { + return $this->ordinalPosition; + } + + /** + * Set ordinal position + * + * @param int $ordinalPosition + * @return self Provides a fluent interface + */ + public function setOrdinalPosition($ordinalPosition) + { + $this->ordinalPosition = $ordinalPosition; + return $this; + } + + /** + * Get position in unique constraint + * + * @return bool + */ + public function getPositionInUniqueConstraint() + { + return $this->positionInUniqueConstraint; + } + + /** + * Set position in unique constraint + * + * @param bool $positionInUniqueConstraint + * @return self Provides a fluent interface + */ + public function setPositionInUniqueConstraint($positionInUniqueConstraint) + { + $this->positionInUniqueConstraint = $positionInUniqueConstraint; + return $this; + } + + /** + * Get referencred table schema + * + * @return string + */ + public function getReferencedTableSchema() + { + return $this->referencedTableSchema; + } + + /** + * Set referenced table schema + * + * @param string $referencedTableSchema + * @return self Provides a fluent interface + */ + public function setReferencedTableSchema($referencedTableSchema) + { + $this->referencedTableSchema = $referencedTableSchema; + return $this; + } + + /** + * Get referenced table name + * + * @return string + */ + public function getReferencedTableName() + { + return $this->referencedTableName; + } + + /** + * Set Referenced table name + * + * @param string $referencedTableName + * @return self Provides a fluent interface + */ + public function setReferencedTableName($referencedTableName) + { + $this->referencedTableName = $referencedTableName; + return $this; + } + + /** + * Get referenced column name + * + * @return string + */ + public function getReferencedColumnName() + { + return $this->referencedColumnName; + } + + /** + * Set referenced column name + * + * @param string $referencedColumnName + * @return self Provides a fluent interface + */ + public function setReferencedColumnName($referencedColumnName) + { + $this->referencedColumnName = $referencedColumnName; + return $this; + } + + /** + * set foreign key update rule + * + * @param string $foreignKeyUpdateRule + */ + public function setForeignKeyUpdateRule($foreignKeyUpdateRule) + { + $this->foreignKeyUpdateRule = $foreignKeyUpdateRule; + } + + /** + * Get foreign key update rule + * + * @return string + */ + public function getForeignKeyUpdateRule() + { + return $this->foreignKeyUpdateRule; + } + + /** + * Set foreign key delete rule + * + * @param string $foreignKeyDeleteRule + */ + public function setForeignKeyDeleteRule($foreignKeyDeleteRule) + { + $this->foreignKeyDeleteRule = $foreignKeyDeleteRule; + } + + /** + * get foreign key delete rule + * + * @return string + */ + public function getForeignKeyDeleteRule() + { + return $this->foreignKeyDeleteRule; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Object/ConstraintObject.php b/bundled-libs/laminas/laminas-db/src/Metadata/Object/ConstraintObject.php new file mode 100644 index 000000000..86a1e31f5 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Object/ConstraintObject.php @@ -0,0 +1,410 @@ +setName($name); + $this->setTableName($tableName); + $this->setSchemaName($schemaName); + } + + /** + * Set name + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set schema name + * + * @param string $schemaName + */ + public function setSchemaName($schemaName) + { + $this->schemaName = $schemaName; + } + + /** + * Get schema name + * + * @return string + */ + public function getSchemaName() + { + return $this->schemaName; + } + + /** + * Get table name + * + * @return string + */ + public function getTableName() + { + return $this->tableName; + } + + /** + * Set table name + * + * @param string $tableName + * @return self Provides a fluent interface + */ + public function setTableName($tableName) + { + $this->tableName = $tableName; + return $this; + } + + /** + * Set type + * + * @param string $type + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * Get type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + public function hasColumns() + { + return (! empty($this->columns)); + } + + /** + * Get Columns. + * + * @return string[] + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Set Columns. + * + * @param string[] $columns + * @return self Provides a fluent interface + */ + public function setColumns(array $columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Get Referenced Table Schema. + * + * @return string + */ + public function getReferencedTableSchema() + { + return $this->referencedTableSchema; + } + + /** + * Set Referenced Table Schema. + * + * @param string $referencedTableSchema + * @return self Provides a fluent interface + */ + public function setReferencedTableSchema($referencedTableSchema) + { + $this->referencedTableSchema = $referencedTableSchema; + return $this; + } + + /** + * Get Referenced Table Name. + * + * @return string + */ + public function getReferencedTableName() + { + return $this->referencedTableName; + } + + /** + * Set Referenced Table Name. + * + * @param string $referencedTableName + * @return self Provides a fluent interface + */ + public function setReferencedTableName($referencedTableName) + { + $this->referencedTableName = $referencedTableName; + return $this; + } + + /** + * Get Referenced Columns. + * + * @return string[] + */ + public function getReferencedColumns() + { + return $this->referencedColumns; + } + + /** + * Set Referenced Columns. + * + * @param string[] $referencedColumns + * @return self Provides a fluent interface + */ + public function setReferencedColumns(array $referencedColumns) + { + $this->referencedColumns = $referencedColumns; + return $this; + } + + /** + * Get Match Option. + * + * @return string + */ + public function getMatchOption() + { + return $this->matchOption; + } + + /** + * Set Match Option. + * + * @param string $matchOption + * @return self Provides a fluent interface + */ + public function setMatchOption($matchOption) + { + $this->matchOption = $matchOption; + return $this; + } + + /** + * Get Update Rule. + * + * @return string + */ + public function getUpdateRule() + { + return $this->updateRule; + } + + /** + * Set Update Rule. + * + * @param string $updateRule + * @return self Provides a fluent interface + */ + public function setUpdateRule($updateRule) + { + $this->updateRule = $updateRule; + return $this; + } + + /** + * Get Delete Rule. + * + * @return string + */ + public function getDeleteRule() + { + return $this->deleteRule; + } + + /** + * Set Delete Rule. + * + * @param string $deleteRule + * @return self Provides a fluent interface + */ + public function setDeleteRule($deleteRule) + { + $this->deleteRule = $deleteRule; + return $this; + } + + /** + * Get Check Clause. + * + * @return string + */ + public function getCheckClause() + { + return $this->checkClause; + } + + /** + * Set Check Clause. + * + * @param string $checkClause + * @return self Provides a fluent interface + */ + public function setCheckClause($checkClause) + { + $this->checkClause = $checkClause; + return $this; + } + + /** + * Is primary key + * + * @return bool + */ + public function isPrimaryKey() + { + return ('PRIMARY KEY' == $this->type); + } + + /** + * Is unique key + * + * @return bool + */ + public function isUnique() + { + return ('UNIQUE' == $this->type); + } + + /** + * Is foreign key + * + * @return bool + */ + public function isForeignKey() + { + return ('FOREIGN KEY' == $this->type); + } + + /** + * Is foreign key + * + * @return bool + */ + public function isCheck() + { + return ('CHECK' == $this->type); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Object/TableObject.php b/bundled-libs/laminas/laminas-db/src/Metadata/Object/TableObject.php new file mode 100644 index 000000000..a4175c6cf --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Object/TableObject.php @@ -0,0 +1,13 @@ +name; + } + + /** + * Set Name. + * + * @param string $name + * @return self Provides a fluent interface + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Get Event Manipulation. + * + * @return string + */ + public function getEventManipulation() + { + return $this->eventManipulation; + } + + /** + * Set Event Manipulation. + * + * @param string $eventManipulation + * @return self Provides a fluent interface + */ + public function setEventManipulation($eventManipulation) + { + $this->eventManipulation = $eventManipulation; + return $this; + } + + /** + * Get Event Object Catalog. + * + * @return string + */ + public function getEventObjectCatalog() + { + return $this->eventObjectCatalog; + } + + /** + * Set Event Object Catalog. + * + * @param string $eventObjectCatalog + * @return self Provides a fluent interface + */ + public function setEventObjectCatalog($eventObjectCatalog) + { + $this->eventObjectCatalog = $eventObjectCatalog; + return $this; + } + + /** + * Get Event Object Schema. + * + * @return string + */ + public function getEventObjectSchema() + { + return $this->eventObjectSchema; + } + + /** + * Set Event Object Schema. + * + * @param string $eventObjectSchema + * @return self Provides a fluent interface + */ + public function setEventObjectSchema($eventObjectSchema) + { + $this->eventObjectSchema = $eventObjectSchema; + return $this; + } + + /** + * Get Event Object Table. + * + * @return string + */ + public function getEventObjectTable() + { + return $this->eventObjectTable; + } + + /** + * Set Event Object Table. + * + * @param string $eventObjectTable + * @return self Provides a fluent interface + */ + public function setEventObjectTable($eventObjectTable) + { + $this->eventObjectTable = $eventObjectTable; + return $this; + } + + /** + * Get Action Order. + * + * @return string + */ + public function getActionOrder() + { + return $this->actionOrder; + } + + /** + * Set Action Order. + * + * @param string $actionOrder + * @return self Provides a fluent interface + */ + public function setActionOrder($actionOrder) + { + $this->actionOrder = $actionOrder; + return $this; + } + + /** + * Get Action Condition. + * + * @return string + */ + public function getActionCondition() + { + return $this->actionCondition; + } + + /** + * Set Action Condition. + * + * @param string $actionCondition + * @return self Provides a fluent interface + */ + public function setActionCondition($actionCondition) + { + $this->actionCondition = $actionCondition; + return $this; + } + + /** + * Get Action Statement. + * + * @return string + */ + public function getActionStatement() + { + return $this->actionStatement; + } + + /** + * Set Action Statement. + * + * @param string $actionStatement + * @return self Provides a fluent interface + */ + public function setActionStatement($actionStatement) + { + $this->actionStatement = $actionStatement; + return $this; + } + + /** + * Get Action Orientation. + * + * @return string + */ + public function getActionOrientation() + { + return $this->actionOrientation; + } + + /** + * Set Action Orientation. + * + * @param string $actionOrientation + * @return self Provides a fluent interface + */ + public function setActionOrientation($actionOrientation) + { + $this->actionOrientation = $actionOrientation; + return $this; + } + + /** + * Get Action Timing. + * + * @return string + */ + public function getActionTiming() + { + return $this->actionTiming; + } + + /** + * Set Action Timing. + * + * @param string $actionTiming + * @return self Provides a fluent interface + */ + public function setActionTiming($actionTiming) + { + $this->actionTiming = $actionTiming; + return $this; + } + + /** + * Get Action Reference Old Table. + * + * @return string + */ + public function getActionReferenceOldTable() + { + return $this->actionReferenceOldTable; + } + + /** + * Set Action Reference Old Table. + * + * @param string $actionReferenceOldTable + * @return self Provides a fluent interface + */ + public function setActionReferenceOldTable($actionReferenceOldTable) + { + $this->actionReferenceOldTable = $actionReferenceOldTable; + return $this; + } + + /** + * Get Action Reference New Table. + * + * @return string + */ + public function getActionReferenceNewTable() + { + return $this->actionReferenceNewTable; + } + + /** + * Set Action Reference New Table. + * + * @param string $actionReferenceNewTable + * @return self Provides a fluent interface + */ + public function setActionReferenceNewTable($actionReferenceNewTable) + { + $this->actionReferenceNewTable = $actionReferenceNewTable; + return $this; + } + + /** + * Get Action Reference Old Row. + * + * @return string + */ + public function getActionReferenceOldRow() + { + return $this->actionReferenceOldRow; + } + + /** + * Set Action Reference Old Row. + * + * @param string $actionReferenceOldRow + * @return self Provides a fluent interface + */ + public function setActionReferenceOldRow($actionReferenceOldRow) + { + $this->actionReferenceOldRow = $actionReferenceOldRow; + return $this; + } + + /** + * Get Action Reference New Row. + * + * @return string + */ + public function getActionReferenceNewRow() + { + return $this->actionReferenceNewRow; + } + + /** + * Set Action Reference New Row. + * + * @param string $actionReferenceNewRow + * @return self Provides a fluent interface + */ + public function setActionReferenceNewRow($actionReferenceNewRow) + { + $this->actionReferenceNewRow = $actionReferenceNewRow; + return $this; + } + + /** + * Get Created. + * + * @return \DateTime + */ + public function getCreated() + { + return $this->created; + } + + /** + * Set Created. + * + * @param \DateTime $created + * @return self Provides a fluent interface + */ + public function setCreated($created) + { + $this->created = $created; + return $this; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Object/ViewObject.php b/bundled-libs/laminas/laminas-db/src/Metadata/Object/ViewObject.php new file mode 100644 index 000000000..de5779cdb --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Object/ViewObject.php @@ -0,0 +1,75 @@ +viewDefinition; + } + + /** + * @param string $viewDefinition to set + * @return self Provides a fluent interface + */ + public function setViewDefinition($viewDefinition) + { + $this->viewDefinition = $viewDefinition; + return $this; + } + + /** + * @return string $checkOption + */ + public function getCheckOption() + { + return $this->checkOption; + } + + /** + * @param string $checkOption to set + * @return self Provides a fluent interface + */ + public function setCheckOption($checkOption) + { + $this->checkOption = $checkOption; + return $this; + } + + /** + * @return bool $isUpdatable + */ + public function getIsUpdatable() + { + return $this->isUpdatable; + } + + /** + * @param bool $isUpdatable to set + * @return self Provides a fluent interface + */ + public function setIsUpdatable($isUpdatable) + { + $this->isUpdatable = $isUpdatable; + return $this; + } + + public function isUpdatable() + { + return $this->isUpdatable; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Source/AbstractSource.php b/bundled-libs/laminas/laminas-db/src/Metadata/Source/AbstractSource.php new file mode 100644 index 000000000..b0a26e495 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Source/AbstractSource.php @@ -0,0 +1,548 @@ +adapter = $adapter; + $this->defaultSchema = ($adapter->getCurrentSchema()) ?: self::DEFAULT_SCHEMA; + } + + /** + * Get schemas + * + */ + public function getSchemas() + { + $this->loadSchemaData(); + + return $this->data['schemas']; + } + + /** + * {@inheritdoc} + */ + public function getTableNames($schema = null, $includeViews = false) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTableNameData($schema); + + if ($includeViews) { + return array_keys($this->data['table_names'][$schema]); + } + + $tableNames = []; + foreach ($this->data['table_names'][$schema] as $tableName => $data) { + if ('BASE TABLE' == $data['table_type']) { + $tableNames[] = $tableName; + } + } + return $tableNames; + } + + /** + * {@inheritdoc} + */ + public function getTables($schema = null, $includeViews = false) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $tables = []; + foreach ($this->getTableNames($schema, $includeViews) as $tableName) { + $tables[] = $this->getTable($tableName, $schema); + } + return $tables; + } + + /** + * {@inheritdoc} + */ + public function getTable($tableName, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTableNameData($schema); + + if (! isset($this->data['table_names'][$schema][$tableName])) { + throw new \Exception('Table "' . $tableName . '" does not exist'); + } + + $data = $this->data['table_names'][$schema][$tableName]; + switch ($data['table_type']) { + case 'BASE TABLE': + $table = new TableObject($tableName); + break; + case 'VIEW': + $table = new ViewObject($tableName); + $table->setViewDefinition($data['view_definition']); + $table->setCheckOption($data['check_option']); + $table->setIsUpdatable($data['is_updatable']); + break; + default: + throw new \Exception( + 'Table "' . $tableName . '" is of an unsupported type "' . $data['table_type'] . '"' + ); + } + $table->setColumns($this->getColumns($tableName, $schema)); + $table->setConstraints($this->getConstraints($tableName, $schema)); + return $table; + } + + /** + * {@inheritdoc} + */ + public function getViewNames($schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTableNameData($schema); + + $viewNames = []; + foreach ($this->data['table_names'][$schema] as $tableName => $data) { + if ('VIEW' == $data['table_type']) { + $viewNames[] = $tableName; + } + } + return $viewNames; + } + + /** + * {@inheritdoc} + */ + public function getViews($schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $views = []; + foreach ($this->getViewNames($schema) as $tableName) { + $views[] = $this->getTable($tableName, $schema); + } + return $views; + } + + /** + * {@inheritdoc} + */ + public function getView($viewName, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTableNameData($schema); + + $tableNames = $this->data['table_names'][$schema]; + if (isset($tableNames[$viewName]) && 'VIEW' == $tableNames[$viewName]['table_type']) { + return $this->getTable($viewName, $schema); + } + throw new \Exception('View "' . $viewName . '" does not exist'); + } + + /** + * {@inheritdoc} + */ + public function getColumnNames($table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadColumnData($table, $schema); + + if (! isset($this->data['columns'][$schema][$table])) { + throw new \Exception('"' . $table . '" does not exist'); + } + + return array_keys($this->data['columns'][$schema][$table]); + } + + /** + * {@inheritdoc} + */ + public function getColumns($table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadColumnData($table, $schema); + + $columns = []; + foreach ($this->getColumnNames($table, $schema) as $columnName) { + $columns[] = $this->getColumn($columnName, $table, $schema); + } + return $columns; + } + + /** + * {@inheritdoc} + */ + public function getColumn($columnName, $table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadColumnData($table, $schema); + + if (! isset($this->data['columns'][$schema][$table][$columnName])) { + throw new \Exception('A column by that name was not found.'); + } + + $info = $this->data['columns'][$schema][$table][$columnName]; + + $column = new ColumnObject($columnName, $table, $schema); + $props = [ + 'ordinal_position', 'column_default', 'is_nullable', + 'data_type', 'character_maximum_length', 'character_octet_length', + 'numeric_precision', 'numeric_scale', 'numeric_unsigned', + 'erratas' + ]; + foreach ($props as $prop) { + if (isset($info[$prop])) { + $column->{'set' . str_replace('_', '', $prop)}($info[$prop]); + } + } + + $column->setOrdinalPosition($info['ordinal_position']); + $column->setColumnDefault($info['column_default']); + $column->setIsNullable($info['is_nullable']); + $column->setDataType($info['data_type']); + $column->setCharacterMaximumLength($info['character_maximum_length']); + $column->setCharacterOctetLength($info['character_octet_length']); + $column->setNumericPrecision($info['numeric_precision']); + $column->setNumericScale($info['numeric_scale']); + $column->setNumericUnsigned($info['numeric_unsigned']); + $column->setErratas($info['erratas']); + + return $column; + } + + /** + * {@inheritdoc} + */ + public function getConstraints($table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadConstraintData($table, $schema); + + $constraints = []; + foreach (array_keys($this->data['constraints'][$schema][$table]) as $constraintName) { + $constraints[] = $this->getConstraint($constraintName, $table, $schema); + } + + return $constraints; + } + + /** + * {@inheritdoc} + */ + public function getConstraint($constraintName, $table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadConstraintData($table, $schema); + + if (! isset($this->data['constraints'][$schema][$table][$constraintName])) { + throw new \Exception('Cannot find a constraint by that name in this table'); + } + + $info = $this->data['constraints'][$schema][$table][$constraintName]; + $constraint = new ConstraintObject($constraintName, $table, $schema); + + foreach ([ + 'constraint_type' => 'setType', + 'match_option' => 'setMatchOption', + 'update_rule' => 'setUpdateRule', + 'delete_rule' => 'setDeleteRule', + 'columns' => 'setColumns', + 'referenced_table_schema' => 'setReferencedTableSchema', + 'referenced_table_name' => 'setReferencedTableName', + 'referenced_columns' => 'setReferencedColumns', + 'check_clause' => 'setCheckClause', + ] as $key => $setMethod) { + if (isset($info[$key])) { + $constraint->{$setMethod}($info[$key]); + } + } + + return $constraint; + } + + /** + * {@inheritdoc} + */ + public function getConstraintKeys($constraint, $table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadConstraintReferences($table, $schema); + + // organize references first + $references = []; + foreach ($this->data['constraint_references'][$schema] as $refKeyInfo) { + if ($refKeyInfo['constraint_name'] == $constraint) { + $references[$refKeyInfo['constraint_name']] = $refKeyInfo; + } + } + + $this->loadConstraintDataKeys($schema); + + $keys = []; + foreach ($this->data['constraint_keys'][$schema] as $constraintKeyInfo) { + if ($constraintKeyInfo['table_name'] == $table && $constraintKeyInfo['constraint_name'] === $constraint) { + $keys[] = $key = new ConstraintKeyObject($constraintKeyInfo['column_name']); + $key->setOrdinalPosition($constraintKeyInfo['ordinal_position']); + if (isset($references[$constraint])) { + //$key->setReferencedTableSchema($constraintKeyInfo['referenced_table_schema']); + $key->setForeignKeyUpdateRule($references[$constraint]['update_rule']); + $key->setForeignKeyDeleteRule($references[$constraint]['delete_rule']); + //$key->setReferencedTableSchema($references[$constraint]['referenced_table_schema']); + $key->setReferencedTableName($references[$constraint]['referenced_table_name']); + $key->setReferencedColumnName($references[$constraint]['referenced_column_name']); + } + } + } + + return $keys; + } + + /** + * {@inheritdoc} + */ + public function getTriggerNames($schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTriggerData($schema); + + return array_keys($this->data['triggers'][$schema]); + } + + /** + * {@inheritdoc} + */ + public function getTriggers($schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $triggers = []; + foreach ($this->getTriggerNames($schema) as $triggerName) { + $triggers[] = $this->getTrigger($triggerName, $schema); + } + return $triggers; + } + + /** + * {@inheritdoc} + */ + public function getTrigger($triggerName, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTriggerData($schema); + + if (! isset($this->data['triggers'][$schema][$triggerName])) { + throw new \Exception('Trigger "' . $triggerName . '" does not exist'); + } + + $info = $this->data['triggers'][$schema][$triggerName]; + + $trigger = new TriggerObject(); + + $trigger->setName($triggerName); + $trigger->setEventManipulation($info['event_manipulation']); + $trigger->setEventObjectCatalog($info['event_object_catalog']); + $trigger->setEventObjectSchema($info['event_object_schema']); + $trigger->setEventObjectTable($info['event_object_table']); + $trigger->setActionOrder($info['action_order']); + $trigger->setActionCondition($info['action_condition']); + $trigger->setActionStatement($info['action_statement']); + $trigger->setActionOrientation($info['action_orientation']); + $trigger->setActionTiming($info['action_timing']); + $trigger->setActionReferenceOldTable($info['action_reference_old_table']); + $trigger->setActionReferenceNewTable($info['action_reference_new_table']); + $trigger->setActionReferenceOldRow($info['action_reference_old_row']); + $trigger->setActionReferenceNewRow($info['action_reference_new_row']); + $trigger->setCreated($info['created']); + + return $trigger; + } + + /** + * Prepare data hierarchy + * + * @param string $type + * @param string $key ... + */ + protected function prepareDataHierarchy($type) + { + $data = &$this->data; + foreach (func_get_args() as $key) { + if (! isset($data[$key])) { + $data[$key] = []; + } + $data = &$data[$key]; + } + } + + /** + * Load schema data + */ + protected function loadSchemaData() + { + } + + /** + * Load table name data + * + * @param string $schema + */ + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + + $this->prepareDataHierarchy('table_names', $schema); + } + + /** + * Load column data + * + * @param string $table + * @param string $schema + */ + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('columns', $schema, $table); + } + + /** + * Load constraint data + * + * @param string $table + * @param string $schema + */ + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema); + } + + /** + * Load constraint data keys + * + * @param string $schema + */ + protected function loadConstraintDataKeys($schema) + { + if (isset($this->data['constraint_keys'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraint_keys', $schema); + } + + /** + * Load constraint references + * + * @param string $table + * @param string $schema + */ + protected function loadConstraintReferences($table, $schema) + { + if (isset($this->data['constraint_references'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraint_references', $schema); + } + + /** + * Load trigger data + * + * @param string $schema + */ + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Source/Factory.php b/bundled-libs/laminas/laminas-db/src/Metadata/Source/Factory.php new file mode 100644 index 000000000..eea545fa8 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Source/Factory.php @@ -0,0 +1,46 @@ +getPlatform()->getName(); + + switch ($platformName) { + case 'MySQL': + return new MysqlMetadata($adapter); + case 'SQLServer': + return new SqlServerMetadata($adapter); + case 'SQLite': + return new SqliteMetadata($adapter); + case 'PostgreSQL': + return new PostgresqlMetadata($adapter); + case 'Oracle': + return new OracleMetadata($adapter); + default: + throw new InvalidArgumentException("Unknown adapter platform '{$platformName}'"); + } + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Source/MysqlMetadata.php b/bundled-libs/laminas/laminas-db/src/Metadata/Source/MysqlMetadata.php new file mode 100644 index 000000000..fc462e82c --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Source/MysqlMetadata.php @@ -0,0 +1,502 @@ +data['schemas'])) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT ' . $p->quoteIdentifier('SCHEMA_NAME') + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'SCHEMATA']) + . ' WHERE ' . $p->quoteIdentifier('SCHEMA_NAME') + . ' != \'INFORMATION_SCHEMA\''; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $schemas = []; + foreach ($results->toArray() as $row) { + $schemas[] = $row['SCHEMA_NAME']; + } + + $this->data['schemas'] = $schemas; + } + + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ + ['T', 'TABLE_NAME'], + ['T', 'TABLE_TYPE'], + ['V', 'VIEW_DEFINITION'], + ['V', 'CHECK_OPTION'], + ['V', 'IS_UPDATABLE'], + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'VIEWS']) . ' V' + . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['V', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['V', 'TABLE_NAME']) + + . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $tables = []; + foreach ($results->toArray() as $row) { + $tables[$row['TABLE_NAME']] = [ + 'table_type' => $row['TABLE_TYPE'], + 'view_definition' => $row['VIEW_DEFINITION'], + 'check_option' => $row['CHECK_OPTION'], + 'is_updatable' => ('YES' == $row['IS_UPDATABLE']), + ]; + } + + $this->data['table_names'][$schema] = $tables; + } + + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + $this->prepareDataHierarchy('columns', $schema, $table); + $p = $this->adapter->getPlatform(); + + $isColumns = [ + ['C', 'ORDINAL_POSITION'], + ['C', 'COLUMN_DEFAULT'], + ['C', 'IS_NULLABLE'], + ['C', 'DATA_TYPE'], + ['C', 'CHARACTER_MAXIMUM_LENGTH'], + ['C', 'CHARACTER_OCTET_LENGTH'], + ['C', 'NUMERIC_PRECISION'], + ['C', 'NUMERIC_SCALE'], + ['C', 'COLUMN_NAME'], + ['C', 'COLUMN_TYPE'], + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' + . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'COLUMNS']) . 'C' + . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['C', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['C', 'TABLE_NAME']) + . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')' + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteTrustedValue($table); + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $columns = []; + foreach ($results->toArray() as $row) { + $erratas = []; + $matches = []; + if (preg_match('/^(?:enum|set)\((.+)\)$/i', $row['COLUMN_TYPE'], $matches)) { + $permittedValues = $matches[1]; + if (preg_match_all( + "/\\s*'((?:[^']++|'')*+)'\\s*(?:,|\$)/", + $permittedValues, + $matches, + PREG_PATTERN_ORDER + ) + ) { + $permittedValues = str_replace("''", "'", $matches[1]); + } else { + $permittedValues = [$permittedValues]; + } + $erratas['permitted_values'] = $permittedValues; + } + $columns[$row['COLUMN_NAME']] = [ + 'ordinal_position' => $row['ORDINAL_POSITION'], + 'column_default' => $row['COLUMN_DEFAULT'], + 'is_nullable' => ('YES' == $row['IS_NULLABLE']), + 'data_type' => $row['DATA_TYPE'], + 'character_maximum_length' => $row['CHARACTER_MAXIMUM_LENGTH'], + 'character_octet_length' => $row['CHARACTER_OCTET_LENGTH'], + 'numeric_precision' => $row['NUMERIC_PRECISION'], + 'numeric_scale' => $row['NUMERIC_SCALE'], + 'numeric_unsigned' => (false !== strpos($row['COLUMN_TYPE'], 'unsigned')), + 'erratas' => $erratas, + ]; + } + + $this->data['columns'][$schema][$table] = $columns; + } + + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + + $isColumns = [ + ['T', 'TABLE_NAME'], + ['TC', 'CONSTRAINT_NAME'], + ['TC', 'CONSTRAINT_TYPE'], + ['KCU', 'COLUMN_NAME'], + ['RC', 'MATCH_OPTION'], + ['RC', 'UPDATE_RULE'], + ['RC', 'DELETE_RULE'], + ['KCU', 'REFERENCED_TABLE_SCHEMA'], + ['KCU', 'REFERENCED_TABLE_NAME'], + ['KCU', 'REFERENCED_COLUMN_NAME'], + ]; + + $p = $this->adapter->getPlatform(); + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . ' T' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS']) . ' TC' + . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) . ' KCU' + . ' ON ' . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) + . ' AND ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'CONSTRAINT_NAME']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS']) . ' RC' + . ' ON ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) + . ' = ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_NAME']) + + . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteTrustedValue($table) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' != \'INFORMATION_SCHEMA\''; + } + + $sql .= ' ORDER BY CASE ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_TYPE']) + . " WHEN 'PRIMARY KEY' THEN 1" + . " WHEN 'UNIQUE' THEN 2" + . " WHEN 'FOREIGN KEY' THEN 3" + . " ELSE 4 END" + + . ', ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) + . ', ' . $p->quoteIdentifierChain(['KCU', 'ORDINAL_POSITION']); + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $realName = null; + $constraints = []; + foreach ($results->toArray() as $row) { + if ($row['CONSTRAINT_NAME'] !== $realName) { + $realName = $row['CONSTRAINT_NAME']; + $isFK = ('FOREIGN KEY' == $row['CONSTRAINT_TYPE']); + if ($isFK) { + $name = $realName; + } else { + $name = '_laminas_' . $row['TABLE_NAME'] . '_' . $realName; + } + $constraints[$name] = [ + 'constraint_name' => $name, + 'constraint_type' => $row['CONSTRAINT_TYPE'], + 'table_name' => $row['TABLE_NAME'], + 'columns' => [], + ]; + if ($isFK) { + $constraints[$name]['referenced_table_schema'] = $row['REFERENCED_TABLE_SCHEMA']; + $constraints[$name]['referenced_table_name'] = $row['REFERENCED_TABLE_NAME']; + $constraints[$name]['referenced_columns'] = []; + $constraints[$name]['match_option'] = $row['MATCH_OPTION']; + $constraints[$name]['update_rule'] = $row['UPDATE_RULE']; + $constraints[$name]['delete_rule'] = $row['DELETE_RULE']; + } + } + $constraints[$name]['columns'][] = $row['COLUMN_NAME']; + if ($isFK) { + $constraints[$name]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME']; + } + } + + $this->data['constraints'][$schema][$table] = $constraints; + } + + protected function loadConstraintDataNames($schema) + { + if (isset($this->data['constraint_names'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraint_names', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ + ['TC', 'TABLE_NAME'], + ['TC', 'CONSTRAINT_NAME'], + ['TC', 'CONSTRAINT_TYPE'], + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' + . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS']) . 'TC' + . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) + . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = []; + foreach ($results->toArray() as $row) { + $data[] = array_change_key_case($row, CASE_LOWER); + } + + $this->data['constraint_names'][$schema] = $data; + } + + protected function loadConstraintDataKeys($schema) + { + if (isset($this->data['constraint_keys'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraint_keys', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ + ['T', 'TABLE_NAME'], + ['KCU', 'CONSTRAINT_NAME'], + ['KCU', 'COLUMN_NAME'], + ['KCU', 'ORDINAL_POSITION'], + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) . 'KCU' + . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) + + . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = []; + foreach ($results->toArray() as $row) { + $data[] = array_change_key_case($row, CASE_LOWER); + } + + $this->data['constraint_keys'][$schema] = $data; + } + + protected function loadConstraintReferences($table, $schema) + { + parent::loadConstraintReferences($table, $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ + ['RC', 'TABLE_NAME'], + ['RC', 'CONSTRAINT_NAME'], + ['RC', 'UPDATE_RULE'], + ['RC', 'DELETE_RULE'], + ['KCU', 'REFERENCED_TABLE_SCHEMA'], + ['KCU', 'REFERENCED_TABLE_NAME'], + ['KCU', 'REFERENCED_COLUMN_NAME'], + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . 'FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS']) . 'RC' + . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['RC', 'TABLE_NAME']) + + . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) . 'KCU' + . ' ON ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['RC', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) + . ' AND ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_NAME']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'CONSTRAINT_NAME']) + + . 'WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = []; + foreach ($results->toArray() as $row) { + $data[] = array_change_key_case($row, CASE_LOWER); + } + + $this->data['constraint_references'][$schema] = $data; + } + + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ +// 'TRIGGER_CATALOG', +// 'TRIGGER_SCHEMA', + 'TRIGGER_NAME', + 'EVENT_MANIPULATION', + 'EVENT_OBJECT_CATALOG', + 'EVENT_OBJECT_SCHEMA', + 'EVENT_OBJECT_TABLE', + 'ACTION_ORDER', + 'ACTION_CONDITION', + 'ACTION_STATEMENT', + 'ACTION_ORIENTATION', + 'ACTION_TIMING', + 'ACTION_REFERENCE_OLD_TABLE', + 'ACTION_REFERENCE_NEW_TABLE', + 'ACTION_REFERENCE_OLD_ROW', + 'ACTION_REFERENCE_NEW_ROW', + 'CREATED', + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifier($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TRIGGERS']) + . ' WHERE '; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = []; + foreach ($results->toArray() as $row) { + $row = array_change_key_case($row, CASE_LOWER); + if (null !== $row['created']) { + $row['created'] = new \DateTime($row['created']); + } + $data[$row['trigger_name']] = $row; + } + + $this->data['triggers'][$schema] = $data; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Source/OracleMetadata.php b/bundled-libs/laminas/laminas-db/src/Metadata/Source/OracleMetadata.php new file mode 100644 index 000000000..b44cfe289 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Source/OracleMetadata.php @@ -0,0 +1,257 @@ + 'CHECK', + 'P' => 'PRIMARY KEY', + 'R' => 'FOREIGN_KEY' + ]; + + /** + * {@inheritdoc} + * @see \Laminas\Db\Metadata\Source\AbstractSource::loadColumnData() + */ + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + + $isColumns = [ + 'COLUMN_ID', + 'COLUMN_NAME', + 'DATA_DEFAULT', + 'NULLABLE', + 'DATA_TYPE', + 'DATA_LENGTH', + 'DATA_PRECISION', + 'DATA_SCALE' + ]; + + $this->prepareDataHierarchy('columns', $schema, $table); + $parameters = [ + ':ownername' => $schema, + ':tablename' => $table + ]; + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM all_tab_columns' + . ' WHERE owner = :ownername AND table_name = :tablename'; + + $result = $this->adapter->query($sql)->execute($parameters); + $columns = []; + + foreach ($result as $row) { + $columns[$row['COLUMN_NAME']] = [ + 'ordinal_position' => $row['COLUMN_ID'], + 'column_default' => $row['DATA_DEFAULT'], + 'is_nullable' => ('Y' == $row['NULLABLE']), + 'data_type' => $row['DATA_TYPE'], + 'character_maximum_length' => $row['DATA_LENGTH'], + 'character_octet_length' => null, + 'numeric_precision' => $row['DATA_PRECISION'], + 'numeric_scale' => $row['DATA_SCALE'], + 'numeric_unsigned' => false, + 'erratas' => [], + ]; + } + + $this->data['columns'][$schema][$table] = $columns; + return $this; + } + + /** + * Constraint type + * + * @param string $type + * @return string + */ + protected function getConstraintType($type) + { + if (isset($this->constraintTypeMap[$type])) { + return $this->constraintTypeMap[$type]; + } + + return $type; + } + + /** + * {@inheritdoc} + * @see \Laminas\Db\Metadata\Source\AbstractSource::loadConstraintData() + */ + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + $sql = ' + SELECT + ac.owner, + ac.constraint_name, + ac.constraint_type, + ac.search_condition check_clause, + ac.table_name, + ac.delete_rule, + cc1.column_name, + cc2.table_name as ref_table, + cc2.column_name as ref_column, + cc2.owner as ref_owner + FROM all_constraints ac + INNER JOIN all_cons_columns cc1 + ON cc1.constraint_name = ac.constraint_name + LEFT JOIN all_cons_columns cc2 + ON cc2.constraint_name = ac.r_constraint_name + AND cc2.position = cc1.position + + WHERE + ac.owner = :ownername AND ac.table_name = :tablename + + ORDER BY ac.constraint_name + '; + + $parameters = [ + ':ownername' => $schema, + ':tablename' => $table + ]; + + $results = $this->adapter->query($sql)->execute($parameters); + $isFK = false; + $name = null; + $constraints = []; + + foreach ($results as $row) { + if ($row['CONSTRAINT_NAME'] !== $name) { + $name = $row['CONSTRAINT_NAME']; + $constraints[$name] = [ + 'constraint_name' => $name, + 'constraint_type' => $this->getConstraintType($row['CONSTRAINT_TYPE']), + 'table_name' => $row['TABLE_NAME'], + ]; + + if ('C' == $row['CONSTRAINT_TYPE']) { + $constraints[$name]['CHECK_CLAUSE'] = $row['CHECK_CLAUSE']; + continue; + } + + $constraints[$name]['columns'] = []; + + $isFK = ('R' == $row['CONSTRAINT_TYPE']); + if ($isFK) { + $constraints[$name]['referenced_table_schema'] = $row['REF_OWNER']; + $constraints[$name]['referenced_table_name'] = $row['REF_TABLE']; + $constraints[$name]['referenced_columns'] = []; + $constraints[$name]['match_option'] = 'NONE'; + $constraints[$name]['update_rule'] = null; + $constraints[$name]['delete_rule'] = $row['DELETE_RULE']; + } + } + + $constraints[$name]['columns'][] = $row['COLUMN_NAME']; + if ($isFK) { + $constraints[$name]['referenced_columns'][] = $row['REF_COLUMN']; + } + } + + $this->data['constraints'][$schema][$table] = $constraints; + + return $this; + } + + /** + * {@inheritdoc} + * @see \Laminas\Db\Metadata\Source\AbstractSource::loadSchemaData() + */ + protected function loadSchemaData() + { + if (isset($this->data['schemas'])) { + return; + } + + $this->prepareDataHierarchy('schemas'); + $sql = 'SELECT USERNAME FROM ALL_USERS'; + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $schemas = []; + foreach ($results->toArray() as $row) { + $schemas[] = $row['USERNAME']; + } + + $this->data['schemas'] = $schemas; + } + + /** + * {@inheritdoc} + * @see \Laminas\Db\Metadata\Source\AbstractSource::loadTableNameData() + */ + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return $this; + } + + $this->prepareDataHierarchy('table_names', $schema); + $tables = []; + + // Tables + $bind = [':OWNER' => strtoupper($schema)]; + $result = $this->adapter->query('SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER=:OWNER')->execute($bind); + + foreach ($result as $row) { + $tables[$row['TABLE_NAME']] = [ + 'table_type' => 'BASE TABLE', + 'view_definition' => null, + 'check_option' => null, + 'is_updatable' => false, + ]; + } + + // Views + $result = $this->adapter->query('SELECT VIEW_NAME, TEXT FROM ALL_VIEWS WHERE OWNER=:OWNER', $bind); + foreach ($result as $row) { + $tables[$row['VIEW_NAME']] = [ + 'table_type' => 'VIEW', + 'view_definition' => null, + 'check_option' => 'NONE', + 'is_updatable' => false, + ]; + } + + $this->data['table_names'][$schema] = $tables; + return $this; + } + + /** + * FIXME: load trigger data + * + * {@inheritdoc} + * + * @see \Laminas\Db\Metadata\Source\AbstractSource::loadTriggerData() + */ + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Source/PostgresqlMetadata.php b/bundled-libs/laminas/laminas-db/src/Metadata/Source/PostgresqlMetadata.php new file mode 100644 index 000000000..b50394d4d --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Source/PostgresqlMetadata.php @@ -0,0 +1,348 @@ +data['schemas'])) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT ' . $p->quoteIdentifier('schema_name') + . ' FROM ' . $p->quoteIdentifierChain(['information_schema', 'schemata']) + . ' WHERE ' . $p->quoteIdentifier('schema_name') + . ' != \'information_schema\'' + . ' AND ' . $p->quoteIdentifier('schema_name') . " NOT LIKE 'pg_%'"; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $schemas = []; + foreach ($results->toArray() as $row) { + $schemas[] = $row['schema_name']; + } + + $this->data['schemas'] = $schemas; + } + + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ + ['t', 'table_name'], + ['t', 'table_type'], + ['v', 'view_definition'], + ['v', 'check_option'], + ['v', 'is_updatable'], + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['information_schema', 'tables']) . ' t' + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'views']) . ' v' + . ' ON ' . $p->quoteIdentifierChain(['t', 'table_schema']) + . ' = ' . $p->quoteIdentifierChain(['v', 'table_schema']) + . ' AND ' . $p->quoteIdentifierChain(['t', 'table_name']) + . ' = ' . $p->quoteIdentifierChain(['v', 'table_name']) + + . ' WHERE ' . $p->quoteIdentifierChain(['t', 'table_type']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['t', 'table_schema']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['t', 'table_schema']) + . ' != \'information_schema\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $tables = []; + foreach ($results->toArray() as $row) { + $tables[$row['table_name']] = [ + 'table_type' => $row['table_type'], + 'view_definition' => $row['view_definition'], + 'check_option' => $row['check_option'], + 'is_updatable' => ('YES' == $row['is_updatable']), + ]; + } + + $this->data['table_names'][$schema] = $tables; + } + + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('columns', $schema, $table); + + $platform = $this->adapter->getPlatform(); + + $isColumns = [ + 'table_name', + 'column_name', + 'ordinal_position', + 'column_default', + 'is_nullable', + 'data_type', + 'character_maximum_length', + 'character_octet_length', + 'numeric_precision', + 'numeric_scale', + ]; + + array_walk($isColumns, function (&$c) use ($platform) { + $c = $platform->quoteIdentifier($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $platform->quoteIdentifier('information_schema') + . $platform->getIdentifierSeparator() . $platform->quoteIdentifier('columns') + . ' WHERE ' . $platform->quoteIdentifier('table_schema') + . ' != \'information\'' + . ' AND ' . $platform->quoteIdentifier('table_name') + . ' = ' . $platform->quoteTrustedValue($table); + + if ($schema != '__DEFAULT_SCHEMA__') { + $sql .= ' AND ' . $platform->quoteIdentifier('table_schema') + . ' = ' . $platform->quoteTrustedValue($schema); + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $columns = []; + foreach ($results->toArray() as $row) { + $columns[$row['column_name']] = [ + 'ordinal_position' => $row['ordinal_position'], + 'column_default' => $row['column_default'], + 'is_nullable' => ('YES' == $row['is_nullable']), + 'data_type' => $row['data_type'], + 'character_maximum_length' => $row['character_maximum_length'], + 'character_octet_length' => $row['character_octet_length'], + 'numeric_precision' => $row['numeric_precision'], + 'numeric_scale' => $row['numeric_scale'], + 'numeric_unsigned' => null, + 'erratas' => [], + ]; + } + + $this->data['columns'][$schema][$table] = $columns; + } + + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + + $isColumns = [ + ['t', 'table_name'], + ['tc', 'constraint_name'], + ['tc', 'constraint_type'], + ['kcu', 'column_name'], + ['cc', 'check_clause'], + ['rc', 'match_option'], + ['rc', 'update_rule'], + ['rc', 'delete_rule'], + ['referenced_table_schema' => 'kcu2', 'table_schema'], + ['referenced_table_name' => 'kcu2', 'table_name'], + ['referenced_column_name' => 'kcu2', 'column_name'], + ]; + + $p = $this->adapter->getPlatform(); + + array_walk($isColumns, function (&$c) use ($p) { + $alias = key($c); + $c = $p->quoteIdentifierChain($c); + if (is_string($alias)) { + $c .= ' ' . $p->quoteIdentifier($alias); + } + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['information_schema', 'tables']) . ' t' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(['information_schema', 'table_constraints']) . ' tc' + . ' ON ' . $p->quoteIdentifierChain(['t', 'table_schema']) + . ' = ' . $p->quoteIdentifierChain(['tc', 'table_schema']) + . ' AND ' . $p->quoteIdentifierChain(['t', 'table_name']) + . ' = ' . $p->quoteIdentifierChain(['tc', 'table_name']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'key_column_usage']) . ' kcu' + . ' ON ' . $p->quoteIdentifierChain(['tc', 'table_schema']) + . ' = ' . $p->quoteIdentifierChain(['kcu', 'table_schema']) + . ' AND ' . $p->quoteIdentifierChain(['tc', 'table_name']) + . ' = ' . $p->quoteIdentifierChain(['kcu', 'table_name']) + . ' AND ' . $p->quoteIdentifierChain(['tc', 'constraint_name']) + . ' = ' . $p->quoteIdentifierChain(['kcu', 'constraint_name']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'check_constraints']) . ' cc' + . ' ON ' . $p->quoteIdentifierChain(['tc', 'constraint_schema']) + . ' = ' . $p->quoteIdentifierChain(['cc', 'constraint_schema']) + . ' AND ' . $p->quoteIdentifierChain(['tc', 'constraint_name']) + . ' = ' . $p->quoteIdentifierChain(['cc', 'constraint_name']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'referential_constraints']) . ' rc' + . ' ON ' . $p->quoteIdentifierChain(['tc', 'constraint_schema']) + . ' = ' . $p->quoteIdentifierChain(['rc', 'constraint_schema']) + . ' AND ' . $p->quoteIdentifierChain(['tc', 'constraint_name']) + . ' = ' . $p->quoteIdentifierChain(['rc', 'constraint_name']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'key_column_usage']) . ' kcu2' + . ' ON ' . $p->quoteIdentifierChain(['rc', 'unique_constraint_schema']) + . ' = ' . $p->quoteIdentifierChain(['kcu2', 'constraint_schema']) + . ' AND ' . $p->quoteIdentifierChain(['rc', 'unique_constraint_name']) + . ' = ' . $p->quoteIdentifierChain(['kcu2', 'constraint_name']) + . ' AND ' . $p->quoteIdentifierChain(['kcu', 'position_in_unique_constraint']) + . ' = ' . $p->quoteIdentifierChain(['kcu2', 'ordinal_position']) + + . ' WHERE ' . $p->quoteIdentifierChain(['t', 'table_name']) + . ' = ' . $p->quoteTrustedValue($table) + . ' AND ' . $p->quoteIdentifierChain(['t', 'table_type']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['t', 'table_schema']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['t', 'table_schema']) + . ' != \'information_schema\''; + } + + $sql .= ' ORDER BY CASE ' . $p->quoteIdentifierChain(['tc', 'constraint_type']) + . " WHEN 'PRIMARY KEY' THEN 1" + . " WHEN 'UNIQUE' THEN 2" + . " WHEN 'FOREIGN KEY' THEN 3" + . " WHEN 'CHECK' THEN 4" + . " ELSE 5 END" + . ', ' . $p->quoteIdentifierChain(['tc', 'constraint_name']) + . ', ' . $p->quoteIdentifierChain(['kcu', 'ordinal_position']); + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $name = null; + $constraints = []; + foreach ($results->toArray() as $row) { + if ($row['constraint_name'] !== $name) { + $name = $row['constraint_name']; + $constraints[$name] = [ + 'constraint_name' => $name, + 'constraint_type' => $row['constraint_type'], + 'table_name' => $row['table_name'], + ]; + if ('CHECK' == $row['constraint_type']) { + $constraints[$name]['check_clause'] = $row['check_clause']; + continue; + } + $constraints[$name]['columns'] = []; + $isFK = ('FOREIGN KEY' == $row['constraint_type']); + if ($isFK) { + $constraints[$name]['referenced_table_schema'] = $row['referenced_table_schema']; + $constraints[$name]['referenced_table_name'] = $row['referenced_table_name']; + $constraints[$name]['referenced_columns'] = []; + $constraints[$name]['match_option'] = $row['match_option']; + $constraints[$name]['update_rule'] = $row['update_rule']; + $constraints[$name]['delete_rule'] = $row['delete_rule']; + } + } + $constraints[$name]['columns'][] = $row['column_name']; + if ($isFK) { + $constraints[$name]['referenced_columns'][] = $row['referenced_column_name']; + } + } + + $this->data['constraints'][$schema][$table] = $constraints; + } + + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ + 'trigger_name', + 'event_manipulation', + 'event_object_catalog', + 'event_object_schema', + 'event_object_table', + 'action_order', + 'action_condition', + 'action_statement', + 'action_orientation', + ['action_timing' => 'condition_timing'], + ['action_reference_old_table' => 'condition_reference_old_table'], + ['action_reference_new_table' => 'condition_reference_new_table'], + 'created', + ]; + + array_walk($isColumns, function (&$c) use ($p) { + if (is_array($c)) { + $alias = key($c); + $c = $p->quoteIdentifierChain($c); + if (is_string($alias)) { + $c .= ' ' . $p->quoteIdentifier($alias); + } + } else { + $c = $p->quoteIdentifier($c); + } + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['information_schema', 'triggers']) + . ' WHERE '; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= $p->quoteIdentifier('trigger_schema') + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= $p->quoteIdentifier('trigger_schema') + . ' != \'information_schema\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = []; + foreach ($results->toArray() as $row) { + $row = array_change_key_case($row, CASE_LOWER); + $row['action_reference_old_row'] = 'OLD'; + $row['action_reference_new_row'] = 'NEW'; + if (null !== $row['created']) { + $row['created'] = new \DateTime($row['created']); + } + $data[$row['trigger_name']] = $row; + } + + $this->data['triggers'][$schema] = $data; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Source/SqlServerMetadata.php b/bundled-libs/laminas/laminas-db/src/Metadata/Source/SqlServerMetadata.php new file mode 100644 index 000000000..f78d3b7ae --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Source/SqlServerMetadata.php @@ -0,0 +1,344 @@ +data['schemas'])) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT ' . $p->quoteIdentifier('SCHEMA_NAME') + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'SCHEMATA']) + . ' WHERE ' . $p->quoteIdentifier('SCHEMA_NAME') + . ' != \'INFORMATION_SCHEMA\''; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $schemas = []; + foreach ($results->toArray() as $row) { + $schemas[] = $row['SCHEMA_NAME']; + } + + $this->data['schemas'] = $schemas; + } + + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ + ['T', 'TABLE_NAME'], + ['T', 'TABLE_TYPE'], + ['V', 'VIEW_DEFINITION'], + ['V', 'CHECK_OPTION'], + ['V', 'IS_UPDATABLE'], + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . ' t' + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'VIEWS']) . ' v' + . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['V', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['V', 'TABLE_NAME']) + + . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $tables = []; + foreach ($results->toArray() as $row) { + $tables[$row['TABLE_NAME']] = [ + 'table_type' => $row['TABLE_TYPE'], + 'view_definition' => $row['VIEW_DEFINITION'], + 'check_option' => $row['CHECK_OPTION'], + 'is_updatable' => ('YES' == $row['IS_UPDATABLE']), + ]; + } + + $this->data['table_names'][$schema] = $tables; + } + + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + $this->prepareDataHierarchy('columns', $schema, $table); + $p = $this->adapter->getPlatform(); + + $isColumns = [ + ['C', 'ORDINAL_POSITION'], + ['C', 'COLUMN_DEFAULT'], + ['C', 'IS_NULLABLE'], + ['C', 'DATA_TYPE'], + ['C', 'CHARACTER_MAXIMUM_LENGTH'], + ['C', 'CHARACTER_OCTET_LENGTH'], + ['C', 'NUMERIC_PRECISION'], + ['C', 'NUMERIC_SCALE'], + ['C', 'COLUMN_NAME'], + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' + . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'COLUMNS']) . 'C' + . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['C', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['C', 'TABLE_NAME']) + . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')' + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteTrustedValue($table); + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $columns = []; + foreach ($results->toArray() as $row) { + $columns[$row['COLUMN_NAME']] = [ + 'ordinal_position' => $row['ORDINAL_POSITION'], + 'column_default' => $row['COLUMN_DEFAULT'], + 'is_nullable' => ('YES' == $row['IS_NULLABLE']), + 'data_type' => $row['DATA_TYPE'], + 'character_maximum_length' => $row['CHARACTER_MAXIMUM_LENGTH'], + 'character_octet_length' => $row['CHARACTER_OCTET_LENGTH'], + 'numeric_precision' => $row['NUMERIC_PRECISION'], + 'numeric_scale' => $row['NUMERIC_SCALE'], + 'numeric_unsigned' => null, + 'erratas' => [], + ]; + } + + $this->data['columns'][$schema][$table] = $columns; + } + + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + + $isColumns = [ + ['T', 'TABLE_NAME'], + ['TC', 'CONSTRAINT_NAME'], + ['TC', 'CONSTRAINT_TYPE'], + ['KCU', 'COLUMN_NAME'], + ['CC', 'CHECK_CLAUSE'], + ['RC', 'MATCH_OPTION'], + ['RC', 'UPDATE_RULE'], + ['RC', 'DELETE_RULE'], + ['REFERENCED_TABLE_SCHEMA' => 'KCU2', 'TABLE_SCHEMA'], + ['REFERENCED_TABLE_NAME' => 'KCU2', 'TABLE_NAME'], + ['REFERENCED_COLUMN_NAME' => 'KCU2', 'COLUMN_NAME'], + ]; + + $p = $this->adapter->getPlatform(); + + array_walk($isColumns, function (&$c) use ($p) { + $alias = key($c); + $c = $p->quoteIdentifierChain($c); + if (is_string($alias)) { + $c .= ' ' . $p->quoteIdentifier($alias); + } + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . ' T' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS']) . ' TC' + . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) . ' KCU' + . ' ON ' . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) + . ' AND ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) + . ' = ' . $p->quoteIdentifierChain(['KCU', 'CONSTRAINT_NAME']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'CHECK_CONSTRAINTS']) . ' CC' + . ' ON ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['CC', 'CONSTRAINT_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) + . ' = ' . $p->quoteIdentifierChain(['CC', 'CONSTRAINT_NAME']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS']) . ' RC' + . ' ON ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) + . ' = ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_NAME']) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) . ' KCU2' + . ' ON ' . $p->quoteIdentifierChain(['RC', 'UNIQUE_CONSTRAINT_SCHEMA']) + . ' = ' . $p->quoteIdentifierChain(['KCU2', 'CONSTRAINT_SCHEMA']) + . ' AND ' . $p->quoteIdentifierChain(['RC', 'UNIQUE_CONSTRAINT_NAME']) + . ' = ' . $p->quoteIdentifierChain(['KCU2', 'CONSTRAINT_NAME']) + . ' AND ' . $p->quoteIdentifierChain(['KCU', 'ORDINAL_POSITION']) + . ' = ' . $p->quoteIdentifierChain(['KCU2', 'ORDINAL_POSITION']) + + . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' . $p->quoteTrustedValue($table) + . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' != \'INFORMATION_SCHEMA\''; + } + + $sql .= ' ORDER BY CASE ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_TYPE']) + . " WHEN 'PRIMARY KEY' THEN 1" + . " WHEN 'UNIQUE' THEN 2" + . " WHEN 'FOREIGN KEY' THEN 3" + . " WHEN 'CHECK' THEN 4" + . " ELSE 5 END" + . ', ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) + . ', ' . $p->quoteIdentifierChain(['KCU', 'ORDINAL_POSITION']); + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $name = null; + $constraints = []; + $isFK = false; + foreach ($results->toArray() as $row) { + if ($row['CONSTRAINT_NAME'] !== $name) { + $name = $row['CONSTRAINT_NAME']; + $constraints[$name] = [ + 'constraint_name' => $name, + 'constraint_type' => $row['CONSTRAINT_TYPE'], + 'table_name' => $row['TABLE_NAME'], + ]; + if ('CHECK' == $row['CONSTRAINT_TYPE']) { + $constraints[$name]['check_clause'] = $row['CHECK_CLAUSE']; + continue; + } + $constraints[$name]['columns'] = []; + $isFK = ('FOREIGN KEY' == $row['CONSTRAINT_TYPE']); + if ($isFK) { + $constraints[$name]['referenced_table_schema'] = $row['REFERENCED_TABLE_SCHEMA']; + $constraints[$name]['referenced_table_name'] = $row['REFERENCED_TABLE_NAME']; + $constraints[$name]['referenced_columns'] = []; + $constraints[$name]['match_option'] = $row['MATCH_OPTION']; + $constraints[$name]['update_rule'] = $row['UPDATE_RULE']; + $constraints[$name]['delete_rule'] = $row['DELETE_RULE']; + } + } + $constraints[$name]['columns'][] = $row['COLUMN_NAME']; + if ($isFK) { + $constraints[$name]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME']; + } + } + + $this->data['constraints'][$schema][$table] = $constraints; + } + + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ + 'TRIGGER_NAME', + 'EVENT_MANIPULATION', + 'EVENT_OBJECT_CATALOG', + 'EVENT_OBJECT_SCHEMA', + 'EVENT_OBJECT_TABLE', + 'ACTION_ORDER', + 'ACTION_CONDITION', + 'ACTION_STATEMENT', + 'ACTION_ORIENTATION', + 'ACTION_TIMING', + 'ACTION_REFERENCE_OLD_TABLE', + 'ACTION_REFERENCE_NEW_TABLE', + 'ACTION_REFERENCE_OLD_ROW', + 'ACTION_REFERENCE_NEW_ROW', + 'CREATED', + ]; + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifier($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TRIGGERS']) + . ' WHERE '; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = []; + foreach ($results->toArray() as $row) { + $row = array_change_key_case($row, CASE_LOWER); + if (null !== $row['created']) { + $row['created'] = new \DateTime($row['created']); + } + $data[$row['trigger_name']] = $row; + } + + $this->data['triggers'][$schema] = $data; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Metadata/Source/SqliteMetadata.php b/bundled-libs/laminas/laminas-db/src/Metadata/Source/SqliteMetadata.php new file mode 100644 index 000000000..f29a2a715 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Metadata/Source/SqliteMetadata.php @@ -0,0 +1,386 @@ +data['schemas'])) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $results = $this->fetchPragma('database_list'); + foreach ($results as $row) { + $schemas[] = $row['name']; + } + $this->data['schemas'] = $schemas; + } + + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + // FEATURE: Filename? + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT "name", "type", "sql" FROM ' . $p->quoteIdentifierChain([$schema, 'sqlite_master']) + . ' WHERE "type" IN (\'table\',\'view\') AND "name" NOT LIKE \'sqlite_%\''; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $tables = []; + foreach ($results->toArray() as $row) { + if ('table' == $row['type']) { + $table = [ + 'table_type' => 'BASE TABLE', + 'view_definition' => null, // VIEW only + 'check_option' => null, // VIEW only + 'is_updatable' => null, // VIEW only + ]; + } else { + $table = [ + 'table_type' => 'VIEW', + 'view_definition' => null, + 'check_option' => 'NONE', + 'is_updatable' => false, + ]; + + // Parse out extra data + if (null !== ($data = $this->parseView($row['sql']))) { + $table = array_merge($table, $data); + } + } + $tables[$row['name']] = $table; + } + $this->data['table_names'][$schema] = $tables; + } + + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + $this->prepareDataHierarchy('columns', $schema, $table); + $this->prepareDataHierarchy('sqlite_columns', $schema, $table); + + $results = $this->fetchPragma('table_info', $table, $schema); + + $columns = []; + + foreach ($results as $row) { + $columns[$row['name']] = [ + // cid appears to be zero-based, ordinal position needs to be one-based + 'ordinal_position' => $row['cid'] + 1, + 'column_default' => $row['dflt_value'], + 'is_nullable' => ! ((bool) $row['notnull']), + 'data_type' => $row['type'], + 'character_maximum_length' => null, + 'character_octet_length' => null, + 'numeric_precision' => null, + 'numeric_scale' => null, + 'numeric_unsigned' => null, + 'erratas' => [], + ]; + // TODO: populate character_ and numeric_values with correct info + } + + $this->data['columns'][$schema][$table] = $columns; + $this->data['sqlite_columns'][$schema][$table] = $results; + } + + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + + $this->loadColumnData($table, $schema); + $primaryKey = []; + + foreach ($this->data['sqlite_columns'][$schema][$table] as $col) { + if ((bool) $col['pk']) { + $primaryKey[] = $col['name']; + } + } + + if (empty($primaryKey)) { + $primaryKey = null; + } + $constraints = []; + $indexes = $this->fetchPragma('index_list', $table, $schema); + foreach ($indexes as $index) { + if (! ((bool) $index['unique'])) { + continue; + } + $constraint = [ + 'constraint_name' => $index['name'], + 'constraint_type' => 'UNIQUE', + 'table_name' => $table, + 'columns' => [], + ]; + + $info = $this->fetchPragma('index_info', $index['name'], $schema); + + foreach ($info as $column) { + $constraint['columns'][] = $column['name']; + } + if ($primaryKey === $constraint['columns']) { + $constraint['constraint_type'] = 'PRIMARY KEY'; + $primaryKey = null; + } + $constraints[$constraint['constraint_name']] = $constraint; + } + + if (null !== $primaryKey) { + $constraintName = '_laminas_' . $table . '_PRIMARY'; + $constraints[$constraintName] = [ + 'constraint_name' => $constraintName, + 'constraint_type' => 'PRIMARY KEY', + 'table_name' => $table, + 'columns' => $primaryKey, + ]; + } + + $foreignKeys = $this->fetchPragma('foreign_key_list', $table, $schema); + + $id = $name = null; + foreach ($foreignKeys as $fk) { + if ($id !== $fk['id']) { + $id = $fk['id']; + $name = '_laminas_' . $table . '_FOREIGN_KEY_' . ($id + 1); + $constraints[$name] = [ + 'constraint_name' => $name, + 'constraint_type' => 'FOREIGN KEY', + 'table_name' => $table, + 'columns' => [], + 'referenced_table_schema' => $schema, + 'referenced_table_name' => $fk['table'], + 'referenced_columns' => [], + // TODO: Verify match, on_update, and on_delete values conform to SQL Standard + 'match_option' => strtoupper($fk['match']), + 'update_rule' => strtoupper($fk['on_update']), + 'delete_rule' => strtoupper($fk['on_delete']), + ]; + } + $constraints[$name]['columns'][] = $fk['from']; + $constraints[$name]['referenced_columns'][] = $fk['to']; + } + + $this->data['constraints'][$schema][$table] = $constraints; + } + + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT "name", "tbl_name", "sql" FROM ' + . $p->quoteIdentifierChain([$schema, 'sqlite_master']) + . ' WHERE "type" = \'trigger\''; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $triggers = []; + foreach ($results->toArray() as $row) { + $trigger = [ + 'trigger_name' => $row['name'], + 'event_manipulation' => null, // in $row['sql'] + 'event_object_catalog' => null, + 'event_object_schema' => $schema, + 'event_object_table' => $row['tbl_name'], + 'action_order' => 0, + 'action_condition' => null, // in $row['sql'] + 'action_statement' => null, // in $row['sql'] + 'action_orientation' => 'ROW', + 'action_timing' => null, // in $row['sql'] + 'action_reference_old_table' => null, + 'action_reference_new_table' => null, + 'action_reference_old_row' => 'OLD', + 'action_reference_new_row' => 'NEW', + 'created' => null, + ]; + + // Parse out extra data + if (null !== ($data = $this->parseTrigger($row['sql']))) { + $trigger = array_merge($trigger, $data); + } + $triggers[$trigger['trigger_name']] = $trigger; + } + + $this->data['triggers'][$schema] = $triggers; + } + + protected function fetchPragma($name, $value = null, $schema = null) + { + $p = $this->adapter->getPlatform(); + + $sql = 'PRAGMA '; + + if (null !== $schema) { + $sql .= $p->quoteIdentifier($schema) . '.'; + } + $sql .= $name; + + if (null !== $value) { + $sql .= '(' . $p->quoteTrustedValue($value) . ')'; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + if ($results instanceof ResultSetInterface) { + return $results->toArray(); + } + return []; + } + + protected function parseView($sql) + { + static $re = null; + if (null === $re) { + $identifierChain = $this->getIdentifierChainRegularExpression(); + $re = $this->buildRegularExpression([ + 'CREATE', + ['TEMP|TEMPORARY'], + 'VIEW', + ['IF', 'NOT', 'EXISTS'], + $identifierChain, + 'AS', + '(?.+)', + [';'], + ]); + } + + if (! preg_match($re, $sql, $matches)) { + return; + } + return [ + 'view_definition' => $matches['view_definition'], + ]; + } + + protected function parseTrigger($sql) + { + static $re = null; + if (null === $re) { + $identifier = $this->getIdentifierRegularExpression(); + $identifierList = $this->getIdentifierListRegularExpression(); + $identifierChain = $this->getIdentifierChainRegularExpression(); + $re = $this->buildRegularExpression([ + 'CREATE', + ['TEMP|TEMPORARY'], + 'TRIGGER', + ['IF', 'NOT', 'EXISTS'], + $identifierChain, + ['(?BEFORE|AFTER|INSTEAD\\s+OF)', ], + '(?DELETE|INSERT|UPDATE)', + ['OF', '(?' . $identifierList . ')'], + 'ON', + '(?' . $identifier . ')', + ['FOR', 'EACH', 'ROW'], + ['WHEN', '(?.+)'], + '(?BEGIN', + '.+', + 'END)', + [';'], + ]); + } + + if (! preg_match($re, $sql, $matches)) { + return; + } + $data = []; + + foreach ($matches as $key => $value) { + if (is_string($key)) { + $data[$key] = $value; + } + } + + // Normalize data and populate defaults, if necessary + + $data['event_manipulation'] = strtoupper($data['event_manipulation']); + if (empty($data['action_condition'])) { + $data['action_condition'] = null; + } + if (! empty($data['action_timing'])) { + $data['action_timing'] = strtoupper($data['action_timing']); + if ('I' == $data['action_timing'][0]) { + // normalize the white-space between the two words + $data['action_timing'] = 'INSTEAD OF'; + } + } else { + $data['action_timing'] = 'AFTER'; + } + unset($data['column_usage']); + + return $data; + } + + protected function buildRegularExpression(array $re) + { + foreach ($re as &$value) { + if (is_array($value)) { + $value = '(?:' . implode('\\s*+', $value) . '\\s*+)?'; + } else { + $value .= '\\s*+'; + } + } + unset($value); + $re = '/^' . implode('\\s*+', $re) . '$/'; + return $re; + } + + protected function getIdentifierRegularExpression() + { + static $re = null; + if (null === $re) { + $re = '(?:' . implode('|', [ + '"(?:[^"\\\\]++|\\\\.)*+"', + '`(?:[^`]++|``)*+`', + '\\[[^\\]]+\\]', + '[^\\s\\.]+', + ]) . ')'; + } + + return $re; + } + + protected function getIdentifierChainRegularExpression() + { + static $re = null; + if (null === $re) { + $identifier = $this->getIdentifierRegularExpression(); + $re = $identifier . '(?:\\s*\\.\\s*' . $identifier . ')*+'; + } + return $re; + } + + protected function getIdentifierListRegularExpression() + { + static $re = null; + if (null === $re) { + $identifier = $this->getIdentifierRegularExpression(); + $re = $identifier . '(?:\\s*,\\s*' . $identifier . ')*+'; + } + return $re; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Module.php b/bundled-libs/laminas/laminas-db/src/Module.php new file mode 100644 index 000000000..1bd7b84ce --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Module.php @@ -0,0 +1,25 @@ + $provider->getDependencyConfig(), + ]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/ResultSet/AbstractResultSet.php b/bundled-libs/laminas/laminas-db/src/ResultSet/AbstractResultSet.php new file mode 100644 index 000000000..359080ba3 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/ResultSet/AbstractResultSet.php @@ -0,0 +1,296 @@ +buffer)) { + $this->buffer = []; + } + + if ($dataSource instanceof ResultInterface) { + $this->fieldCount = $dataSource->getFieldCount(); + $this->dataSource = $dataSource; + if ($dataSource->isBuffered()) { + $this->buffer = -1; + } + if (is_array($this->buffer)) { + $this->dataSource->rewind(); + } + return $this; + } + + if (is_array($dataSource)) { + // its safe to get numbers from an array + $first = current($dataSource); + reset($dataSource); + $this->fieldCount = $first === false ? 0 : count($first); + $this->dataSource = new ArrayIterator($dataSource); + $this->buffer = -1; // array's are a natural buffer + } elseif ($dataSource instanceof IteratorAggregate) { + $this->dataSource = $dataSource->getIterator(); + } elseif ($dataSource instanceof Iterator) { + $this->dataSource = $dataSource; + } else { + throw new Exception\InvalidArgumentException( + 'DataSource provided is not an array, nor does it implement Iterator or IteratorAggregate' + ); + } + + return $this; + } + + /** + * @return self Provides a fluent interface + * @throws Exception\RuntimeException + */ + public function buffer() + { + if ($this->buffer === -2) { + throw new Exception\RuntimeException('Buffering must be enabled before iteration is started'); + } elseif ($this->buffer === null) { + $this->buffer = []; + if ($this->dataSource instanceof ResultInterface) { + $this->dataSource->rewind(); + } + } + return $this; + } + + public function isBuffered() + { + if ($this->buffer === -1 || is_array($this->buffer)) { + return true; + } + return false; + } + + /** + * Get the data source used to create the result set + * + * @return null|Iterator + */ + public function getDataSource() + { + return $this->dataSource; + } + + /** + * Retrieve count of fields in individual rows of the result set + * + * @return int + */ + public function getFieldCount() + { + if (null !== $this->fieldCount) { + return $this->fieldCount; + } + + $dataSource = $this->getDataSource(); + if (null === $dataSource) { + return 0; + } + + $dataSource->rewind(); + if (! $dataSource->valid()) { + $this->fieldCount = 0; + return 0; + } + + $row = $dataSource->current(); + if (is_object($row) && $row instanceof Countable) { + $this->fieldCount = $row->count(); + return $this->fieldCount; + } + + $row = (array) $row; + $this->fieldCount = count($row); + return $this->fieldCount; + } + + /** + * Iterator: move pointer to next item + * + * @return void + */ + public function next() + { + if ($this->buffer === null) { + $this->buffer = -2; // implicitly disable buffering from here on + } + if (! is_array($this->buffer) || $this->position == $this->dataSource->key()) { + $this->dataSource->next(); + } + $this->position++; + } + + /** + * Iterator: retrieve current key + * + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * Iterator: get current item + * + * @return array|null + */ + public function current() + { + if (-1 === $this->buffer) { + // datasource was an array when the resultset was initialized + return $this->dataSource->current(); + } + + if ($this->buffer === null) { + $this->buffer = -2; // implicitly disable buffering from here on + } elseif (is_array($this->buffer) && isset($this->buffer[$this->position])) { + return $this->buffer[$this->position]; + } + $data = $this->dataSource->current(); + if (is_array($this->buffer)) { + $this->buffer[$this->position] = $data; + } + return is_array($data) ? $data : null; + } + + /** + * Iterator: is pointer valid? + * + * @return bool + */ + public function valid() + { + if (is_array($this->buffer) && isset($this->buffer[$this->position])) { + return true; + } + if ($this->dataSource instanceof Iterator) { + return $this->dataSource->valid(); + } else { + $key = key($this->dataSource); + return ($key !== null); + } + } + + /** + * Iterator: rewind + * + * @return void + */ + public function rewind() + { + if (! is_array($this->buffer)) { + if ($this->dataSource instanceof Iterator) { + $this->dataSource->rewind(); + } else { + reset($this->dataSource); + } + } + $this->position = 0; + } + + /** + * Countable: return count of rows + * + * @return int + */ + public function count() + { + if ($this->count !== null) { + return $this->count; + } + + if ($this->dataSource instanceof Countable) { + $this->count = count($this->dataSource); + } + + return $this->count; + } + + /** + * Cast result set to array of arrays + * + * @return array + * @throws Exception\RuntimeException if any row is not castable to an array + */ + public function toArray() + { + $return = []; + foreach ($this as $row) { + if (is_array($row)) { + $return[] = $row; + continue; + } + + if (! is_object($row) + || ( + ! method_exists($row, 'toArray') + && ! method_exists($row, 'getArrayCopy') + ) + ) { + throw new Exception\RuntimeException( + 'Rows as part of this DataSource, with type ' . gettype($row) . ' cannot be cast to an array' + ); + } + + $return[] = method_exists($row, 'toArray') ? $row->toArray() : $row->getArrayCopy(); + } + return $return; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/ResultSet/Exception/ExceptionInterface.php b/bundled-libs/laminas/laminas-db/src/ResultSet/Exception/ExceptionInterface.php new file mode 100644 index 000000000..1fab20614 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/ResultSet/Exception/ExceptionInterface.php @@ -0,0 +1,15 @@ +setHydrator($hydrator ?: new $defaultHydratorClass()); + $this->setObjectPrototype(($objectPrototype) ?: new ArrayObject); + } + + /** + * Set the row object prototype + * + * @param object $objectPrototype + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function setObjectPrototype($objectPrototype) + { + if (! is_object($objectPrototype)) { + throw new Exception\InvalidArgumentException( + 'An object must be set as the object prototype, a ' . gettype($objectPrototype) . ' was provided.' + ); + } + $this->objectPrototype = $objectPrototype; + return $this; + } + + /** + * Get the row object prototype + * + * @return object + */ + public function getObjectPrototype() + { + return $this->objectPrototype; + } + + /** + * Set the hydrator to use for each row object + * + * @param HydratorInterface $hydrator + * @return self Provides a fluent interface + */ + public function setHydrator(HydratorInterface $hydrator) + { + $this->hydrator = $hydrator; + return $this; + } + + /** + * Get the hydrator to use for each row object + * + * @return HydratorInterface + */ + public function getHydrator() + { + return $this->hydrator; + } + + /** + * Iterator: get current item + * + * @return object|null + */ + public function current() + { + if ($this->buffer === null) { + $this->buffer = -2; // implicitly disable buffering from here on + } elseif (is_array($this->buffer) && isset($this->buffer[$this->position])) { + return $this->buffer[$this->position]; + } + $data = $this->dataSource->current(); + $current = is_array($data) ? $this->hydrator->hydrate($data, clone $this->objectPrototype) : null; + + if (is_array($this->buffer)) { + $this->buffer[$this->position] = $current; + } + + return $current; + } + + /** + * Cast result set to array of arrays + * + * @return array + * @throws Exception\RuntimeException if any row is not castable to an array + */ + public function toArray() + { + $return = []; + foreach ($this as $row) { + $return[] = $this->hydrator->extract($row); + } + return $return; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/ResultSet/ResultSet.php b/bundled-libs/laminas/laminas-db/src/ResultSet/ResultSet.php new file mode 100644 index 000000000..b55a94921 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/ResultSet/ResultSet.php @@ -0,0 +1,119 @@ +allowedReturnTypes, true)) { + $this->returnType = $returnType; + } else { + $this->returnType = self::TYPE_ARRAYOBJECT; + } + if ($this->returnType === self::TYPE_ARRAYOBJECT) { + $this->setArrayObjectPrototype(($arrayObjectPrototype) ?: new ArrayObject([], ArrayObject::ARRAY_AS_PROPS)); + } + } + + /** + * Set the row object prototype + * + * @param ArrayObject $arrayObjectPrototype + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function setArrayObjectPrototype($arrayObjectPrototype) + { + if (! is_object($arrayObjectPrototype) + || ( + ! $arrayObjectPrototype instanceof ArrayObject + && ! method_exists($arrayObjectPrototype, 'exchangeArray') + ) + ) { + throw new Exception\InvalidArgumentException( + 'Object must be of type ArrayObject, or at least implement exchangeArray' + ); + } + $this->arrayObjectPrototype = $arrayObjectPrototype; + return $this; + } + + /** + * Get the row object prototype + * + * @return ArrayObject + */ + public function getArrayObjectPrototype() + { + return $this->arrayObjectPrototype; + } + + /** + * Get the return type to use when returning objects from the set + * + * @return string + */ + public function getReturnType() + { + return $this->returnType; + } + + /** + * @return array|\ArrayObject|null + */ + public function current() + { + $data = parent::current(); + + if ($this->returnType === self::TYPE_ARRAYOBJECT && is_array($data)) { + /** @var $ao ArrayObject */ + $ao = clone $this->arrayObjectPrototype; + if ($ao instanceof ArrayObject || method_exists($ao, 'exchangeArray')) { + $ao->exchangeArray($data); + } + return $ao; + } + + return $data; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/ResultSet/ResultSetInterface.php b/bundled-libs/laminas/laminas-db/src/ResultSet/ResultSetInterface.php new file mode 100644 index 000000000..bf19808bb --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/ResultSet/ResultSetInterface.php @@ -0,0 +1,32 @@ +isInitialized) { + return; + } + + if (! $this->featureSet instanceof Feature\FeatureSet) { + $this->featureSet = new Feature\FeatureSet; + } + + $this->featureSet->setRowGateway($this); + $this->featureSet->apply('preInitialize', []); + + if (! is_string($this->table) && ! $this->table instanceof TableIdentifier) { + throw new Exception\RuntimeException('This row object does not have a valid table set.'); + } + + if ($this->primaryKeyColumn === null) { + throw new Exception\RuntimeException('This row object does not have a primary key column set.'); + } elseif (is_string($this->primaryKeyColumn)) { + $this->primaryKeyColumn = (array) $this->primaryKeyColumn; + } + + if (! $this->sql instanceof Sql) { + throw new Exception\RuntimeException('This row object does not have a Sql object set.'); + } + + $this->featureSet->apply('postInitialize', []); + + $this->isInitialized = true; + } + + /** + * Populate Data + * + * @param array $rowData + * @param bool $rowExistsInDatabase + * @return self Provides a fluent interface + */ + public function populate(array $rowData, $rowExistsInDatabase = false) + { + $this->initialize(); + + $this->data = $rowData; + if ($rowExistsInDatabase == true) { + $this->processPrimaryKeyData(); + } else { + $this->primaryKeyData = null; + } + + return $this; + } + + /** + * @param mixed $array + * @return AbstractRowGateway + */ + public function exchangeArray($array) + { + return $this->populate($array, true); + } + + /** + * Save + * + * @return int + */ + public function save() + { + $this->initialize(); + + if ($this->rowExistsInDatabase()) { + // UPDATE + + $data = $this->data; + $where = []; + $isPkModified = false; + + // primary key is always an array even if its a single column + foreach ($this->primaryKeyColumn as $pkColumn) { + $where[$pkColumn] = $this->primaryKeyData[$pkColumn]; + if ($data[$pkColumn] == $this->primaryKeyData[$pkColumn]) { + unset($data[$pkColumn]); + } else { + $isPkModified = true; + } + } + + $statement = $this->sql->prepareStatementForSqlObject($this->sql->update()->set($data)->where($where)); + $result = $statement->execute(); + $rowsAffected = $result->getAffectedRows(); + unset($statement, $result); // cleanup + + // If one or more primary keys are modified, we update the where clause + if ($isPkModified) { + foreach ($this->primaryKeyColumn as $pkColumn) { + if ($data[$pkColumn] != $this->primaryKeyData[$pkColumn]) { + $where[$pkColumn] = $data[$pkColumn]; + } + } + } + } else { + // INSERT + $insert = $this->sql->insert(); + $insert->values($this->data); + + $statement = $this->sql->prepareStatementForSqlObject($insert); + + $result = $statement->execute(); + if (($primaryKeyValue = $result->getGeneratedValue()) && count($this->primaryKeyColumn) == 1) { + $this->primaryKeyData = [$this->primaryKeyColumn[0] => $primaryKeyValue]; + } else { + // make primary key data available so that $where can be complete + $this->processPrimaryKeyData(); + } + $rowsAffected = $result->getAffectedRows(); + unset($statement, $result); // cleanup + + $where = []; + // primary key is always an array even if its a single column + foreach ($this->primaryKeyColumn as $pkColumn) { + $where[$pkColumn] = $this->primaryKeyData[$pkColumn]; + } + } + + // refresh data + $statement = $this->sql->prepareStatementForSqlObject($this->sql->select()->where($where)); + $result = $statement->execute(); + $rowData = $result->current(); + unset($statement, $result); // cleanup + + // make sure data and original data are in sync after save + $this->populate($rowData, true); + + // return rows affected + return $rowsAffected; + } + + /** + * Delete + * + * @return int + */ + public function delete() + { + $this->initialize(); + + $where = []; + // primary key is always an array even if its a single column + foreach ($this->primaryKeyColumn as $pkColumn) { + $where[$pkColumn] = isset($this->primaryKeyData[$pkColumn]) + ? $this->primaryKeyData[$pkColumn] + : null; + } + + // @todo determine if we need to do a select to ensure 1 row will be affected + + $statement = $this->sql->prepareStatementForSqlObject($this->sql->delete()->where($where)); + $result = $statement->execute(); + + $affectedRows = $result->getAffectedRows(); + if ($affectedRows == 1) { + // detach from database + $this->primaryKeyData = null; + } + + return $affectedRows; + } + + /** + * Offset Exists + * + * @param string $offset + * @return bool + */ + public function offsetExists($offset) + { + return array_key_exists($offset, $this->data); + } + + /** + * Offset get + * + * @param string $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->data[$offset]; + } + + /** + * Offset set + * + * @param string $offset + * @param mixed $value + * @return self Provides a fluent interface + */ + public function offsetSet($offset, $value) + { + $this->data[$offset] = $value; + return $this; + } + + /** + * Offset unset + * + * @param string $offset + * @return self Provides a fluent interface + */ + public function offsetUnset($offset) + { + $this->data[$offset] = null; + return $this; + } + + /** + * @return int + */ + public function count() + { + return count($this->data); + } + + /** + * To array + * + * @return array + */ + public function toArray() + { + return $this->data; + } + + /** + * __get + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function __get($name) + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } else { + throw new Exception\InvalidArgumentException('Not a valid column in this row: ' . $name); + } + } + + /** + * __set + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $this->offsetSet($name, $value); + } + + /** + * __isset + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + return $this->offsetExists($name); + } + + /** + * __unset + * + * @param string $name + * @return void + */ + public function __unset($name) + { + $this->offsetUnset($name); + } + + /** + * @return bool + */ + public function rowExistsInDatabase() + { + return ($this->primaryKeyData !== null); + } + + /** + * @throws Exception\RuntimeException + */ + protected function processPrimaryKeyData() + { + $this->primaryKeyData = []; + foreach ($this->primaryKeyColumn as $column) { + if (! isset($this->data[$column])) { + throw new Exception\RuntimeException( + 'While processing primary key data, a known key ' . $column . ' was not found in the data array' + ); + } + $this->primaryKeyData[$column] = $this->data[$column]; + } + } +} diff --git a/bundled-libs/laminas/laminas-db/src/RowGateway/Exception/ExceptionInterface.php b/bundled-libs/laminas/laminas-db/src/RowGateway/Exception/ExceptionInterface.php new file mode 100644 index 000000000..a3b2a7d93 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/RowGateway/Exception/ExceptionInterface.php @@ -0,0 +1,15 @@ +rowGateway = $rowGateway; + } + + /** + * @throws \Laminas\Db\RowGateway\Exception\RuntimeException + */ + public function initialize() + { + throw new Exception\RuntimeException('This method is not intended to be called on this object.'); + } + + /** + * @return array + */ + public function getMagicMethodSpecifications() + { + return []; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/RowGateway/Feature/FeatureSet.php b/bundled-libs/laminas/laminas-db/src/RowGateway/Feature/FeatureSet.php new file mode 100644 index 000000000..cd180895e --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/RowGateway/Feature/FeatureSet.php @@ -0,0 +1,160 @@ +addFeatures($features); + } + } + + /** + * @param AbstractRowGateway $rowGateway + * @return self Provides a fluent interface + */ + public function setRowGateway(AbstractRowGateway $rowGateway) + { + $this->rowGateway = $rowGateway; + foreach ($this->features as $feature) { + $feature->setRowGateway($this->rowGateway); + } + return $this; + } + + public function getFeatureByClassName($featureClassName) + { + $feature = false; + foreach ($this->features as $potentialFeature) { + if ($potentialFeature instanceof $featureClassName) { + $feature = $potentialFeature; + break; + } + } + return $feature; + } + + /** + * @param array $features + * @return self Provides a fluent interface + */ + public function addFeatures(array $features) + { + foreach ($features as $feature) { + $this->addFeature($feature); + } + return $this; + } + + /** + * @param AbstractFeature $feature + * @return self Provides a fluent interface + */ + public function addFeature(AbstractFeature $feature) + { + $this->features[] = $feature; + $feature->setRowGateway($feature); + return $this; + } + + public function apply($method, $args) + { + foreach ($this->features as $feature) { + if (method_exists($feature, $method)) { + $return = call_user_func_array([$feature, $method], $args); + if ($return === self::APPLY_HALT) { + break; + } + } + } + } + + /** + * @param string $property + * @return bool + */ + public function canCallMagicGet($property) + { + return false; + } + + /** + * @param string $property + * @return mixed + */ + public function callMagicGet($property) + { + $return = null; + return $return; + } + + /** + * @param string $property + * @return bool + */ + public function canCallMagicSet($property) + { + return false; + } + + /** + * @param $property + * @param $value + * @return mixed + */ + public function callMagicSet($property, $value) + { + $return = null; + return $return; + } + + /** + * @param string $method + * @return bool + */ + public function canCallMagicCall($method) + { + return false; + } + + /** + * @param string $method + * @param array $arguments + * @return mixed + */ + public function callMagicCall($method, $arguments) + { + $return = null; + return $return; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/RowGateway/RowGateway.php b/bundled-libs/laminas/laminas-db/src/RowGateway/RowGateway.php new file mode 100644 index 000000000..5eddef75e --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/RowGateway/RowGateway.php @@ -0,0 +1,49 @@ +primaryKeyColumn = empty($primaryKeyColumn) ? null : (array) $primaryKeyColumn; + + // set table + $this->table = $table; + + // set Sql object + if ($adapterOrSql instanceof Sql) { + $this->sql = $adapterOrSql; + } elseif ($adapterOrSql instanceof AdapterInterface) { + $this->sql = new Sql($adapterOrSql, $this->table); + } else { + throw new Exception\InvalidArgumentException('A valid Sql object was not provided.'); + } + + if ($this->sql->getTable() !== $this->table) { + throw new Exception\InvalidArgumentException( + 'The Sql object provided does not have a table that matches this row object' + ); + } + + $this->initialize(); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/RowGateway/RowGatewayInterface.php b/bundled-libs/laminas/laminas-db/src/RowGateway/RowGatewayInterface.php new file mode 100644 index 000000000..8c5f6768f --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/RowGateway/RowGatewayInterface.php @@ -0,0 +1,15 @@ +buildNormalizedArgument($argument, self::TYPE_VALUE); + } + + if (is_scalar($argument) || $argument === null) { + return $this->buildNormalizedArgument($argument, $defaultType); + } + + if (is_array($argument)) { + $value = current($argument); + + if ($value instanceof ExpressionInterface || $value instanceof SqlInterface) { + return $this->buildNormalizedArgument($value, self::TYPE_VALUE); + } + + $key = key($argument); + + if (is_integer($key) && ! in_array($value, $this->allowedTypes)) { + return $this->buildNormalizedArgument($value, $defaultType); + } + + return $this->buildNormalizedArgument($key, $value); + } + + throw new Exception\InvalidArgumentException(sprintf( + '$argument should be %s or %s or %s or %s or %s, "%s" given', + 'null', + 'scalar', + 'array', + 'Laminas\Db\Sql\ExpressionInterface', + 'Laminas\Db\Sql\SqlInterface', + is_object($argument) ? get_class($argument) : gettype($argument) + )); + } + + /** + * @param mixed $argument + * @param string $argumentType + * + * @return array + * + * @throws Exception\InvalidArgumentException + */ + private function buildNormalizedArgument($argument, $argumentType) + { + if (! in_array($argumentType, $this->allowedTypes)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Argument type should be in array(%s)', + implode(',', $this->allowedTypes) + )); + } + + return [ + $argument, + $argumentType, + ]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/AbstractPreparableSql.php b/bundled-libs/laminas/laminas-db/src/Sql/AbstractPreparableSql.php new file mode 100644 index 000000000..46700cb0f --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/AbstractPreparableSql.php @@ -0,0 +1,38 @@ +getParameterContainer(); + + if (! $parameterContainer instanceof ParameterContainer) { + $parameterContainer = new ParameterContainer(); + + $statementContainer->setParameterContainer($parameterContainer); + } + + $statementContainer->setSql( + $this->buildSqlString($adapter->getPlatform(), $adapter->getDriver(), $parameterContainer) + ); + + return $statementContainer; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/AbstractSql.php b/bundled-libs/laminas/laminas-db/src/Sql/AbstractSql.php new file mode 100644 index 000000000..50dbed161 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/AbstractSql.php @@ -0,0 +1,479 @@ + '', 'subselectCount' => 0]; + + /** + * @var array + */ + protected $instanceParameterIndex = []; + + /** + * {@inheritDoc} + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + $adapterPlatform = ($adapterPlatform) ?: new DefaultAdapterPlatform; + return $this->buildSqlString($adapterPlatform); + } + + /** + * @param PlatformInterface $platform + * @param null|DriverInterface $driver + * @param null|ParameterContainer $parameterContainer + * @return string + */ + protected function buildSqlString( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + $this->localizeVariables(); + + $sqls = []; + $parameters = []; + + foreach ($this->specifications as $name => $specification) { + $parameters[$name] = $this->{'process' . $name}( + $platform, + $driver, + $parameterContainer, + $sqls, + $parameters + ); + + if ($specification && is_array($parameters[$name])) { + $sqls[$name] = $this->createSqlFromSpecificationAndParameters($specification, $parameters[$name]); + + continue; + } + + if (is_string($parameters[$name])) { + $sqls[$name] = $parameters[$name]; + } + } + + return rtrim(implode(' ', $sqls), "\n ,"); + } + + /** + * Render table with alias in from/join parts + * + * @todo move TableIdentifier concatenation here + * @param string $table + * @param string $alias + * @return string + */ + protected function renderTable($table, $alias = null) + { + return $table . ($alias ? ' AS ' . $alias : ''); + } + + /** + * @staticvar int $runtimeExpressionPrefix + * @param ExpressionInterface $expression + * @param PlatformInterface $platform + * @param null|DriverInterface $driver + * @param null|ParameterContainer $parameterContainer + * @param null|string $namedParameterPrefix + * @return string + * @throws Exception\RuntimeException + */ + protected function processExpression( + ExpressionInterface $expression, + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null, + $namedParameterPrefix = null + ) { + $namedParameterPrefix = ! $namedParameterPrefix + ? $namedParameterPrefix + : $this->processInfo['paramPrefix'] . $namedParameterPrefix; + // static counter for the number of times this method was invoked across the PHP runtime + static $runtimeExpressionPrefix = 0; + + if ($parameterContainer && ((! is_string($namedParameterPrefix) || $namedParameterPrefix == ''))) { + $namedParameterPrefix = sprintf('expr%04dParam', ++$runtimeExpressionPrefix); + } else { + $namedParameterPrefix = preg_replace('/\s/', '__', $namedParameterPrefix); + } + + $sql = ''; + + // initialize variables + $parts = $expression->getExpressionData(); + + if (! isset($this->instanceParameterIndex[$namedParameterPrefix])) { + $this->instanceParameterIndex[$namedParameterPrefix] = 1; + } + + $expressionParamIndex = &$this->instanceParameterIndex[$namedParameterPrefix]; + + foreach ($parts as $part) { + // #7407: use $expression->getExpression() to get the unescaped + // version of the expression + if (is_string($part) && $expression instanceof Expression) { + $sql .= $expression->getExpression(); + continue; + } + + // If it is a string, simply tack it onto the return sql + // "specification" string + if (is_string($part)) { + $sql .= $part; + continue; + } + + if (! is_array($part)) { + throw new Exception\RuntimeException( + 'Elements returned from getExpressionData() array must be a string or array.' + ); + } + + // Process values and types (the middle and last position of the + // expression data) + $values = $part[1]; + $types = isset($part[2]) ? $part[2] : []; + foreach ($values as $vIndex => $value) { + if (! isset($types[$vIndex])) { + continue; + } + $type = $types[$vIndex]; + if ($value instanceof Select) { + // process sub-select + $values[$vIndex] = '(' + . $this->processSubSelect($value, $platform, $driver, $parameterContainer) + . ')'; + } elseif ($value instanceof ExpressionInterface) { + // recursive call to satisfy nested expressions + $values[$vIndex] = $this->processExpression( + $value, + $platform, + $driver, + $parameterContainer, + $namedParameterPrefix . $vIndex . 'subpart' + ); + } elseif ($type == ExpressionInterface::TYPE_IDENTIFIER) { + $values[$vIndex] = $platform->quoteIdentifierInFragment($value); + } elseif ($type == ExpressionInterface::TYPE_VALUE) { + // if prepareType is set, it means that this particular value must be + // passed back to the statement in a way it can be used as a placeholder value + if ($parameterContainer) { + $name = $namedParameterPrefix . $expressionParamIndex++; + $parameterContainer->offsetSet($name, $value); + $values[$vIndex] = $driver->formatParameterName($name); + continue; + } + + // if not a preparable statement, simply quote the value and move on + $values[$vIndex] = $platform->quoteValue($value); + } elseif ($type == ExpressionInterface::TYPE_LITERAL) { + $values[$vIndex] = $value; + } + } + + // After looping the values, interpolate them into the sql string + // (they might be placeholder names, or values) + $sql .= vsprintf($part[0], $values); + } + + return $sql; + } + + /** + * @param string|array $specifications + * @param array $parameters + * + * @return string + * + * @throws Exception\RuntimeException + */ + protected function createSqlFromSpecificationAndParameters($specifications, $parameters) + { + if (is_string($specifications)) { + return vsprintf($specifications, $parameters); + } + + $parametersCount = count($parameters); + + foreach ($specifications as $specificationString => $paramSpecs) { + if ($parametersCount == count($paramSpecs)) { + break; + } + + unset($specificationString, $paramSpecs); + } + + if (! isset($specificationString)) { + throw new Exception\RuntimeException( + 'A number of parameters was found that is not supported by this specification' + ); + } + + $topParameters = []; + foreach ($parameters as $position => $paramsForPosition) { + if (isset($paramSpecs[$position]['combinedby'])) { + $multiParamValues = []; + foreach ($paramsForPosition as $multiParamsForPosition) { + if (is_array($multiParamsForPosition)) { + $ppCount = count($multiParamsForPosition); + } else { + $ppCount = 1; + $multiParamsForPosition = [$multiParamsForPosition]; + } + + if (! isset($paramSpecs[$position][$ppCount])) { + throw new Exception\RuntimeException(sprintf( + 'A number of parameters (%d) was found that is not supported by this specification', + $ppCount + )); + } + $multiParamValues[] = vsprintf($paramSpecs[$position][$ppCount], $multiParamsForPosition); + } + $topParameters[] = implode($paramSpecs[$position]['combinedby'], $multiParamValues); + } elseif ($paramSpecs[$position] !== null) { + $ppCount = count($paramsForPosition); + if (! isset($paramSpecs[$position][$ppCount])) { + throw new Exception\RuntimeException(sprintf( + 'A number of parameters (%d) was found that is not supported by this specification', + $ppCount + )); + } + $topParameters[] = vsprintf($paramSpecs[$position][$ppCount], $paramsForPosition); + } else { + $topParameters[] = $paramsForPosition; + } + } + return vsprintf($specificationString, $topParameters); + } + + /** + * @param Select $subselect + * @param PlatformInterface $platform + * @param null|DriverInterface $driver + * @param null|ParameterContainer $parameterContainer + * @return string + */ + protected function processSubSelect( + Select $subselect, + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this instanceof PlatformDecoratorInterface) { + $decorator = clone $this; + $decorator->setSubject($subselect); + } else { + $decorator = $subselect; + } + + if ($parameterContainer) { + // Track subselect prefix and count for parameters + $processInfoContext = ($decorator instanceof PlatformDecoratorInterface) ? $subselect : $decorator; + $this->processInfo['subselectCount']++; + $processInfoContext->processInfo['subselectCount'] = $this->processInfo['subselectCount']; + $processInfoContext->processInfo['paramPrefix'] = 'subselect' + . $processInfoContext->processInfo['subselectCount']; + + $sql = $decorator->buildSqlString($platform, $driver, $parameterContainer); + + // copy count + $this->processInfo['subselectCount'] = $decorator->processInfo['subselectCount']; + return $sql; + } + + return $decorator->buildSqlString($platform, $driver, $parameterContainer); + } + + /** + * @param Join[] $joins + * @param PlatformInterface $platform + * @param null|DriverInterface $driver + * @param null|ParameterContainer $parameterContainer + * @return null|string[] Null if no joins present, array of JOIN statements + * otherwise + * @throws Exception\InvalidArgumentException for invalid JOIN table names. + */ + protected function processJoin( + Join $joins, + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if (! $joins->count()) { + return; + } + + // process joins + $joinSpecArgArray = []; + foreach ($joins->getJoins() as $j => $join) { + $joinName = null; + $joinAs = null; + + // table name + if (is_array($join['name'])) { + $joinName = current($join['name']); + $joinAs = $platform->quoteIdentifier(key($join['name'])); + } else { + $joinName = $join['name']; + } + + if ($joinName instanceof Expression) { + $joinName = $joinName->getExpression(); + } elseif ($joinName instanceof TableIdentifier) { + $joinName = $joinName->getTableAndSchema(); + $joinName = ($joinName[1] + ? $platform->quoteIdentifier($joinName[1]) . $platform->getIdentifierSeparator() + : '') . $platform->quoteIdentifier($joinName[0]); + } elseif ($joinName instanceof Select) { + $joinName = '(' . $this->processSubSelect($joinName, $platform, $driver, $parameterContainer) . ')'; + } elseif (is_string($joinName) || (is_object($joinName) && is_callable([$joinName, '__toString']))) { + $joinName = $platform->quoteIdentifier($joinName); + } else { + throw new Exception\InvalidArgumentException(sprintf( + 'Join name expected to be Expression|TableIdentifier|Select|string, "%s" given', + gettype($joinName) + )); + } + + $joinSpecArgArray[$j] = [ + strtoupper($join['type']), + $this->renderTable($joinName, $joinAs), + ]; + + // on expression + // note: for Expression objects, pass them to processExpression with a prefix specific to each join + // (used for named parameters) + if (($join['on'] instanceof ExpressionInterface)) { + $joinSpecArgArray[$j][] = $this->processExpression( + $join['on'], + $platform, + $driver, + $parameterContainer, + 'join' . ($j + 1) . 'part' + ); + } else { + // on + $joinSpecArgArray[$j][] = $platform->quoteIdentifierInFragment( + $join['on'], + ['=', 'AND', 'OR', '(', ')', 'BETWEEN', '<', '>'] + ); + } + } + + return [$joinSpecArgArray]; + } + + /** + * @param null|array|ExpressionInterface|Select $column + * @param PlatformInterface $platform + * @param null|DriverInterface $driver + * @param null|string $namedParameterPrefix + * @param null|ParameterContainer $parameterContainer + * @return string + */ + protected function resolveColumnValue( + $column, + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null, + $namedParameterPrefix = null + ) { + $namedParameterPrefix = ! $namedParameterPrefix + ? $namedParameterPrefix + : $this->processInfo['paramPrefix'] . $namedParameterPrefix; + $isIdentifier = false; + $fromTable = ''; + if (is_array($column)) { + if (isset($column['isIdentifier'])) { + $isIdentifier = (bool) $column['isIdentifier']; + } + if (isset($column['fromTable']) && $column['fromTable'] !== null) { + $fromTable = $column['fromTable']; + } + $column = $column['column']; + } + + if ($column instanceof ExpressionInterface) { + return $this->processExpression($column, $platform, $driver, $parameterContainer, $namedParameterPrefix); + } + if ($column instanceof Select) { + return '(' . $this->processSubSelect($column, $platform, $driver, $parameterContainer) . ')'; + } + if ($column === null) { + return 'NULL'; + } + return $isIdentifier + ? $fromTable . $platform->quoteIdentifierInFragment($column) + : $platform->quoteValue($column); + } + + /** + * @param string|TableIdentifier|Select $table + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @return string + */ + protected function resolveTable( + $table, + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + $schema = null; + if ($table instanceof TableIdentifier) { + list($table, $schema) = $table->getTableAndSchema(); + } + + if ($table instanceof Select) { + $table = '(' . $this->processSubselect($table, $platform, $driver, $parameterContainer) . ')'; + } elseif ($table) { + $table = $platform->quoteIdentifier($table); + } + + if ($schema && $table) { + $table = $platform->quoteIdentifier($schema) . $platform->getIdentifierSeparator() . $table; + } + return $table; + } + + /** + * Copy variables from the subject into the local properties + */ + protected function localizeVariables() + { + if (! $this instanceof PlatformDecoratorInterface) { + return; + } + + foreach (get_object_vars($this->subject) as $name => $value) { + $this->{$name} = $value; + } + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Combine.php b/bundled-libs/laminas/laminas-db/src/Sql/Combine.php new file mode 100644 index 000000000..39f060405 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Combine.php @@ -0,0 +1,211 @@ + '%1$s (%2$s) ', + ]; + + /** + * @var Select[][] + */ + private $combine = []; + + /** + * @param Select|array|null $select + * @param string $type + * @param string $modifier + */ + public function __construct($select = null, $type = self::COMBINE_UNION, $modifier = '') + { + if ($select) { + $this->combine($select, $type, $modifier); + } + } + + /** + * Create combine clause + * + * @param Select|array $select + * @param string $type + * @param string $modifier + * + * @return self Provides a fluent interface + * + * @throws Exception\InvalidArgumentException + */ + public function combine($select, $type = self::COMBINE_UNION, $modifier = '') + { + if (is_array($select)) { + foreach ($select as $combine) { + if ($combine instanceof Select) { + $combine = [$combine]; + } + + $this->combine( + $combine[0], + isset($combine[1]) ? $combine[1] : $type, + isset($combine[2]) ? $combine[2] : $modifier + ); + } + return $this; + } + + if (! $select instanceof Select) { + throw new Exception\InvalidArgumentException(sprintf( + '$select must be a array or instance of Select, "%s" given', + is_object($select) ? get_class($select) : gettype($select) + )); + } + + $this->combine[] = [ + 'select' => $select, + 'type' => $type, + 'modifier' => $modifier + ]; + return $this; + } + + /** + * Create union clause + * + * @param Select|array $select + * @param string $modifier + * + * @return self + */ + public function union($select, $modifier = '') + { + return $this->combine($select, self::COMBINE_UNION, $modifier); + } + + /** + * Create except clause + * + * @param Select|array $select + * @param string $modifier + * + * @return self + */ + public function except($select, $modifier = '') + { + return $this->combine($select, self::COMBINE_EXCEPT, $modifier); + } + + /** + * Create intersect clause + * + * @param Select|array $select + * @param string $modifier + * @return self + */ + public function intersect($select, $modifier = '') + { + return $this->combine($select, self::COMBINE_INTERSECT, $modifier); + } + + /** + * Build sql string + * + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * + * @return string + */ + protected function buildSqlString( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if (! $this->combine) { + return; + } + + $sql = ''; + foreach ($this->combine as $i => $combine) { + $type = $i == 0 + ? '' + : strtoupper($combine['type'] . ($combine['modifier'] ? ' ' . $combine['modifier'] : '')); + $select = $this->processSubSelect($combine['select'], $platform, $driver, $parameterContainer); + $sql .= sprintf( + $this->specifications[self::COMBINE], + $type, + $select + ); + } + return trim($sql, ' '); + } + + /** + * @return self Provides a fluent interface + */ + public function alignColumns() + { + if (! $this->combine) { + return $this; + } + + $allColumns = []; + foreach ($this->combine as $combine) { + $allColumns = array_merge( + $allColumns, + $combine['select']->getRawState(self::COLUMNS) + ); + } + + foreach ($this->combine as $combine) { + $combineColumns = $combine['select']->getRawState(self::COLUMNS); + $aligned = []; + foreach ($allColumns as $alias => $column) { + $aligned[$alias] = isset($combineColumns[$alias]) + ? $combineColumns[$alias] + : new Predicate\Expression('NULL'); + } + $combine['select']->columns($aligned, false); + } + return $this; + } + + /** + * Get raw state + * + * @param string $key + * + * @return array + */ + public function getRawState($key = null) + { + $rawState = [ + self::COMBINE => $this->combine, + self::COLUMNS => $this->combine + ? $this->combine[0]['select']->getRawState(self::COLUMNS) + : [], + ]; + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/AlterTable.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/AlterTable.php new file mode 100644 index 000000000..c66f3354c --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/AlterTable.php @@ -0,0 +1,237 @@ + "ALTER TABLE %1\$s\n", + self::ADD_COLUMNS => [ + "%1\$s" => [ + [1 => "ADD COLUMN %1\$s,\n", 'combinedby' => ""] + ] + ], + self::CHANGE_COLUMNS => [ + "%1\$s" => [ + [2 => "CHANGE COLUMN %1\$s %2\$s,\n", 'combinedby' => ""], + ] + ], + self::DROP_COLUMNS => [ + "%1\$s" => [ + [1 => "DROP COLUMN %1\$s,\n", 'combinedby' => ""], + ] + ], + self::ADD_CONSTRAINTS => [ + "%1\$s" => [ + [1 => "ADD %1\$s,\n", 'combinedby' => ""], + ] + ], + self::DROP_CONSTRAINTS => [ + "%1\$s" => [ + [1 => "DROP CONSTRAINT %1\$s,\n", 'combinedby' => ""], + ] + ] + ]; + + /** + * @var string + */ + protected $table = ''; + + /** + * @param string|TableIdentifier $table + */ + public function __construct($table = '') + { + ($table) ? $this->setTable($table) : null; + } + + /** + * @param string $name + * @return self Provides a fluent interface + */ + public function setTable($name) + { + $this->table = $name; + + return $this; + } + + /** + * @param Column\ColumnInterface $column + * @return self Provides a fluent interface + */ + public function addColumn(Column\ColumnInterface $column) + { + $this->addColumns[] = $column; + + return $this; + } + + /** + * @param string $name + * @param Column\ColumnInterface $column + * @return self Provides a fluent interface + */ + public function changeColumn($name, Column\ColumnInterface $column) + { + $this->changeColumns[$name] = $column; + + return $this; + } + + /** + * @param string $name + * @return self Provides a fluent interface + */ + public function dropColumn($name) + { + $this->dropColumns[] = $name; + + return $this; + } + + /** + * @param string $name + * @return self Provides a fluent interface + */ + public function dropConstraint($name) + { + $this->dropConstraints[] = $name; + + return $this; + } + + /** + * @param Constraint\ConstraintInterface $constraint + * @return self Provides a fluent interface + */ + public function addConstraint(Constraint\ConstraintInterface $constraint) + { + $this->addConstraints[] = $constraint; + + return $this; + } + + /** + * @param string|null $key + * @return array + */ + public function getRawState($key = null) + { + $rawState = [ + self::TABLE => $this->table, + self::ADD_COLUMNS => $this->addColumns, + self::DROP_COLUMNS => $this->dropColumns, + self::CHANGE_COLUMNS => $this->changeColumns, + self::ADD_CONSTRAINTS => $this->addConstraints, + self::DROP_CONSTRAINTS => $this->dropConstraints, + ]; + + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + protected function processTable(PlatformInterface $adapterPlatform = null) + { + return [$this->resolveTable($this->table, $adapterPlatform)]; + } + + protected function processAddColumns(PlatformInterface $adapterPlatform = null) + { + $sqls = []; + foreach ($this->addColumns as $column) { + $sqls[] = $this->processExpression($column, $adapterPlatform); + } + + return [$sqls]; + } + + protected function processChangeColumns(PlatformInterface $adapterPlatform = null) + { + $sqls = []; + foreach ($this->changeColumns as $name => $column) { + $sqls[] = [ + $adapterPlatform->quoteIdentifier($name), + $this->processExpression($column, $adapterPlatform) + ]; + } + + return [$sqls]; + } + + protected function processDropColumns(PlatformInterface $adapterPlatform = null) + { + $sqls = []; + foreach ($this->dropColumns as $column) { + $sqls[] = $adapterPlatform->quoteIdentifier($column); + } + + return [$sqls]; + } + + protected function processAddConstraints(PlatformInterface $adapterPlatform = null) + { + $sqls = []; + foreach ($this->addConstraints as $constraint) { + $sqls[] = $this->processExpression($constraint, $adapterPlatform); + } + + return [$sqls]; + } + + protected function processDropConstraints(PlatformInterface $adapterPlatform = null) + { + $sqls = []; + foreach ($this->dropConstraints as $constraint) { + $sqls[] = $adapterPlatform->quoteIdentifier($constraint); + } + + return [$sqls]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/AbstractLengthColumn.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/AbstractLengthColumn.php new file mode 100644 index 000000000..89e0f3297 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/AbstractLengthColumn.php @@ -0,0 +1,70 @@ +setLength($length); + + parent::__construct($name, $nullable, $default, $options); + } + + /** + * @param int $length + * @return self Provides a fluent interface + */ + public function setLength($length) + { + $this->length = (int) $length; + + return $this; + } + + /** + * @return int + */ + public function getLength() + { + return $this->length; + } + + /** + * @return string + */ + protected function getLengthExpression() + { + return (string) $this->length; + } + + /** + * @return array + */ + public function getExpressionData() + { + $data = parent::getExpressionData(); + + if ($this->getLengthExpression()) { + $data[0][1][1] .= '(' . $this->getLengthExpression() . ')'; + } + + return $data; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/AbstractPrecisionColumn.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/AbstractPrecisionColumn.php new file mode 100644 index 000000000..7d2156689 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/AbstractPrecisionColumn.php @@ -0,0 +1,85 @@ +setDecimal($decimal); + + parent::__construct($name, $digits, $nullable, $default, $options); + } + + /** + * @param int $digits + * + * @return self + */ + public function setDigits($digits) + { + return $this->setLength($digits); + } + + /** + * @return int + */ + public function getDigits() + { + return $this->getLength(); + } + + /** + * @param int|null $decimal + * @return self Provides a fluent interface + */ + public function setDecimal($decimal) + { + $this->decimal = null === $decimal ? null : (int) $decimal; + + return $this; + } + + /** + * @return int|null + */ + public function getDecimal() + { + return $this->decimal; + } + + /** + * {@inheritDoc} + */ + protected function getLengthExpression() + { + if ($this->decimal !== null) { + return $this->length . ',' . $this->decimal; + } + + return $this->length; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/AbstractTimestampColumn.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/AbstractTimestampColumn.php new file mode 100644 index 000000000..51341970d --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/AbstractTimestampColumn.php @@ -0,0 +1,62 @@ +specification; + + $params = []; + $params[] = $this->name; + $params[] = $this->type; + + $types = [self::TYPE_IDENTIFIER, self::TYPE_LITERAL]; + + if (! $this->isNullable) { + $spec .= ' NOT NULL'; + } + + if ($this->default !== null) { + $spec .= ' DEFAULT %s'; + $params[] = $this->default; + $types[] = self::TYPE_VALUE; + } + + $options = $this->getOptions(); + + if (isset($options['on_update'])) { + $spec .= ' %s'; + $params[] = 'ON UPDATE CURRENT_TIMESTAMP'; + $types[] = self::TYPE_LITERAL; + } + + $data = [[ + $spec, + $params, + $types, + ]]; + + foreach ($this->constraints as $constraint) { + $data[] = ' '; + $data = array_merge($data, $constraint->getExpressionData()); + } + + return $data; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/BigInteger.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/BigInteger.php new file mode 100644 index 000000000..849e96b0c --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/BigInteger.php @@ -0,0 +1,17 @@ +setName($name); + $this->setNullable($nullable); + $this->setDefault($default); + $this->setOptions($options); + } + + /** + * @param string $name + * @return self Provides a fluent interface + */ + public function setName($name) + { + $this->name = (string) $name; + return $this; + } + + /** + * @return null|string + */ + public function getName() + { + return $this->name; + } + + /** + * @param bool $nullable + * @return self Provides a fluent interface + */ + public function setNullable($nullable) + { + $this->isNullable = (bool) $nullable; + return $this; + } + + /** + * @return bool + */ + public function isNullable() + { + return $this->isNullable; + } + + /** + * @param null|string|int $default + * @return self Provides a fluent interface + */ + public function setDefault($default) + { + $this->default = $default; + return $this; + } + + /** + * @return null|string|int + */ + public function getDefault() + { + return $this->default; + } + + /** + * @param array $options + * @return self Provides a fluent interface + */ + public function setOptions(array $options) + { + $this->options = $options; + return $this; + } + + /** + * @param string $name + * @param string $value + * @return self Provides a fluent interface + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + return $this; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @param ConstraintInterface $constraint + * + * @return self Provides a fluent interface + */ + public function addConstraint(ConstraintInterface $constraint) + { + $this->constraints[] = $constraint; + + return $this; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + + $params = []; + $params[] = $this->name; + $params[] = $this->type; + + $types = [self::TYPE_IDENTIFIER, self::TYPE_LITERAL]; + + if (! $this->isNullable) { + $spec .= ' NOT NULL'; + } + + if ($this->default !== null) { + $spec .= ' DEFAULT %s'; + $params[] = $this->default; + $types[] = self::TYPE_VALUE; + } + + $data = [[ + $spec, + $params, + $types, + ]]; + + foreach ($this->constraints as $constraint) { + $data[] = ' '; + $data = array_merge($data, $constraint->getExpressionData()); + } + + return $data; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/ColumnInterface.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/ColumnInterface.php new file mode 100644 index 000000000..b7f6d525c --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/ColumnInterface.php @@ -0,0 +1,39 @@ +getOptions(); + + if (isset($options['length'])) { + $data[0][1][1] .= '(' . $options['length'] . ')'; + } + + return $data; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/Text.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/Text.php new file mode 100644 index 000000000..4d2057120 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Column/Text.php @@ -0,0 +1,17 @@ +setColumns($columns); + } + + $this->setName($name); + } + + /** + * @param string $name + * @return self Provides a fluent interface + */ + public function setName($name) + { + $this->name = (string) $name; + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param null|string|array $columns + * @return self Provides a fluent interface + */ + public function setColumns($columns) + { + $this->columns = (array) $columns; + + return $this; + } + + /** + * @param string $column + * @return self Provides a fluent interface + */ + public function addColumn($column) + { + $this->columns[] = $column; + return $this; + } + + /** + * {@inheritDoc} + */ + public function getColumns() + { + return $this->columns; + } + + /** + * {@inheritDoc} + */ + public function getExpressionData() + { + $colCount = count($this->columns); + $newSpecTypes = []; + $values = []; + $newSpec = ''; + + if ($this->name) { + $newSpec .= $this->namedSpecification; + $values[] = $this->name; + $newSpecTypes[] = self::TYPE_IDENTIFIER; + } + + $newSpec .= $this->specification; + + if ($colCount) { + $values = array_merge($values, $this->columns); + $newSpecParts = array_fill(0, $colCount, '%s'); + $newSpecTypes = array_merge($newSpecTypes, array_fill(0, $colCount, self::TYPE_IDENTIFIER)); + $newSpec .= sprintf($this->columnSpecification, implode(', ', $newSpecParts)); + } + + return [[ + $newSpec, + $values, + $newSpecTypes, + ]]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Constraint/Check.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Constraint/Check.php new file mode 100644 index 000000000..68e234ede --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Constraint/Check.php @@ -0,0 +1,55 @@ +expression = $expression; + $this->name = $name; + } + + /** + * {@inheritDoc} + */ + public function getExpressionData() + { + $newSpecTypes = [self::TYPE_LITERAL]; + $values = [$this->expression]; + $newSpec = ''; + + if ($this->name) { + $newSpec .= $this->namedSpecification; + + array_unshift($values, $this->name); + array_unshift($newSpecTypes, self::TYPE_IDENTIFIER); + } + + return [[ + $newSpec . $this->specification, + $values, + $newSpecTypes, + ]]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Constraint/ConstraintInterface.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Constraint/ConstraintInterface.php new file mode 100644 index 000000000..8be1e3c01 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Constraint/ConstraintInterface.php @@ -0,0 +1,16 @@ +setName($name); + $this->setColumns($columns); + $this->setReferenceTable($referenceTable); + $this->setReferenceColumn($referenceColumn); + + if ($onDeleteRule) { + $this->setOnDeleteRule($onDeleteRule); + } + + if ($onUpdateRule) { + $this->setOnUpdateRule($onUpdateRule); + } + } + + /** + * @param string $referenceTable + * @return self Provides a fluent interface + */ + public function setReferenceTable($referenceTable) + { + $this->referenceTable = (string) $referenceTable; + return $this; + } + + /** + * @return string + */ + public function getReferenceTable() + { + return $this->referenceTable; + } + + /** + * @param null|string|array $referenceColumn + * @return self Provides a fluent interface + */ + public function setReferenceColumn($referenceColumn) + { + $this->referenceColumn = (array) $referenceColumn; + + return $this; + } + + /** + * @return array + */ + public function getReferenceColumn() + { + return $this->referenceColumn; + } + + /** + * @param string $onDeleteRule + * @return self Provides a fluent interface + */ + public function setOnDeleteRule($onDeleteRule) + { + $this->onDeleteRule = (string) $onDeleteRule; + + return $this; + } + + /** + * @return string + */ + public function getOnDeleteRule() + { + return $this->onDeleteRule; + } + + /** + * @param string $onUpdateRule + * @return self Provides a fluent interface + */ + public function setOnUpdateRule($onUpdateRule) + { + $this->onUpdateRule = (string) $onUpdateRule; + + return $this; + } + + /** + * @return string + */ + public function getOnUpdateRule() + { + return $this->onUpdateRule; + } + + /** + * @return array + */ + public function getExpressionData() + { + $data = parent::getExpressionData(); + $colCount = count($this->referenceColumn); + $newSpecTypes = [self::TYPE_IDENTIFIER]; + $values = [$this->referenceTable]; + + $data[0][0] .= $this->referenceSpecification[0]; + + if ($colCount) { + $values = array_merge($values, $this->referenceColumn); + $newSpecParts = array_fill(0, $colCount, '%s'); + $newSpecTypes = array_merge($newSpecTypes, array_fill(0, $colCount, self::TYPE_IDENTIFIER)); + + $data[0][0] .= sprintf('(%s) ', implode(', ', $newSpecParts)); + } + + $data[0][0] .= $this->referenceSpecification[1]; + + $values[] = $this->onDeleteRule; + $values[] = $this->onUpdateRule; + $newSpecTypes[] = self::TYPE_LITERAL; + $newSpecTypes[] = self::TYPE_LITERAL; + + $data[0][1] = array_merge($data[0][1], $values); + $data[0][2] = array_merge($data[0][2], $newSpecTypes); + + return $data; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Constraint/PrimaryKey.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Constraint/PrimaryKey.php new file mode 100644 index 000000000..791c2f649 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Constraint/PrimaryKey.php @@ -0,0 +1,17 @@ + 'CREATE %1$sTABLE %2$s (', + self::COLUMNS => [ + "\n %1\$s" => [ + [1 => '%1$s', 'combinedby' => ",\n "] + ] + ], + 'combinedBy' => ",", + self::CONSTRAINTS => [ + "\n %1\$s" => [ + [1 => '%1$s', 'combinedby' => ",\n "] + ] + ], + 'statementEnd' => '%1$s', + ]; + + /** + * @var string + */ + protected $table = ''; + + /** + * @param string|TableIdentifier $table + * @param bool $isTemporary + */ + public function __construct($table = '', $isTemporary = false) + { + $this->table = $table; + $this->setTemporary($isTemporary); + } + + /** + * @param bool $temporary + * @return self Provides a fluent interface + */ + public function setTemporary($temporary) + { + $this->isTemporary = (bool) $temporary; + return $this; + } + + /** + * @return bool + */ + public function isTemporary() + { + return $this->isTemporary; + } + + /** + * @param string $name + * @return self Provides a fluent interface + */ + public function setTable($name) + { + $this->table = $name; + return $this; + } + + /** + * @param Column\ColumnInterface $column + * @return self Provides a fluent interface + */ + public function addColumn(Column\ColumnInterface $column) + { + $this->columns[] = $column; + return $this; + } + + /** + * @param Constraint\ConstraintInterface $constraint + * @return self Provides a fluent interface + */ + public function addConstraint(Constraint\ConstraintInterface $constraint) + { + $this->constraints[] = $constraint; + return $this; + } + + /** + * @param string|null $key + * @return array + */ + public function getRawState($key = null) + { + $rawState = [ + self::COLUMNS => $this->columns, + self::CONSTRAINTS => $this->constraints, + self::TABLE => $this->table, + ]; + + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + /** + * @param PlatformInterface $adapterPlatform + * + * @return string[] + */ + protected function processTable(PlatformInterface $adapterPlatform = null) + { + return [ + $this->isTemporary ? 'TEMPORARY ' : '', + $this->resolveTable($this->table, $adapterPlatform), + ]; + } + + /** + * @param PlatformInterface $adapterPlatform + * + * @return string[][]|null + */ + protected function processColumns(PlatformInterface $adapterPlatform = null) + { + if (! $this->columns) { + return; + } + + $sqls = []; + + foreach ($this->columns as $column) { + $sqls[] = $this->processExpression($column, $adapterPlatform); + } + + return [$sqls]; + } + + /** + * @param PlatformInterface $adapterPlatform + * + * @return array|string + */ + protected function processCombinedby(PlatformInterface $adapterPlatform = null) + { + if ($this->constraints && $this->columns) { + return $this->specifications['combinedBy']; + } + } + + /** + * @param PlatformInterface $adapterPlatform + * + * @return string[][]|null + */ + protected function processConstraints(PlatformInterface $adapterPlatform = null) + { + if (! $this->constraints) { + return; + } + + $sqls = []; + + foreach ($this->constraints as $constraint) { + $sqls[] = $this->processExpression($constraint, $adapterPlatform); + } + + return [$sqls]; + } + + /** + * @param PlatformInterface $adapterPlatform + * + * @return string[] + */ + protected function processStatementEnd(PlatformInterface $adapterPlatform = null) + { + return ["\n)"]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/DropTable.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/DropTable.php new file mode 100644 index 000000000..cff15aa68 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/DropTable.php @@ -0,0 +1,43 @@ + 'DROP TABLE %1$s' + ]; + + /** + * @var string + */ + protected $table = ''; + + /** + * @param string|TableIdentifier $table + */ + public function __construct($table = '') + { + $this->table = $table; + } + + protected function processTable(PlatformInterface $adapterPlatform = null) + { + return [$this->resolveTable($this->table, $adapterPlatform)]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Index/AbstractIndex.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Index/AbstractIndex.php new file mode 100644 index 000000000..22e08aedb --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/Index/AbstractIndex.php @@ -0,0 +1,15 @@ +setColumns($columns); + + $this->name = null === $name ? null : (string) $name; + $this->lengths = $lengths; + } + + /** + * + * @return array of array|string should return an array in the format: + * + * array ( + * // a sprintf formatted string + * string $specification, + * + * // the values for the above sprintf formatted string + * array $values, + * + * // an array of equal length of the $values array, with either TYPE_IDENTIFIER or TYPE_VALUE for each value + * array $types, + * ) + * + */ + public function getExpressionData() + { + $colCount = count($this->columns); + $values = []; + $values[] = $this->name ?: ''; + $newSpecTypes = [self::TYPE_IDENTIFIER]; + $newSpecParts = []; + + for ($i = 0; $i < $colCount; $i++) { + $specPart = '%s'; + + if (isset($this->lengths[$i])) { + $specPart .= "({$this->lengths[$i]})"; + } + + $newSpecParts[] = $specPart; + $newSpecTypes[] = self::TYPE_IDENTIFIER; + } + + $newSpec = str_replace('...', implode(', ', $newSpecParts), $this->specification); + + return [[ + $newSpec, + array_merge($values, $this->columns), + $newSpecTypes, + ]]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Ddl/SqlInterface.php b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/SqlInterface.php new file mode 100644 index 000000000..2f24be5aa --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Ddl/SqlInterface.php @@ -0,0 +1,15 @@ + 'DELETE FROM %1$s', + self::SPECIFICATION_WHERE => 'WHERE %1$s' + ]; + + /** + * @var string|TableIdentifier + */ + protected $table = ''; + + /** + * @var bool + */ + protected $emptyWhereProtection = true; + + /** + * @var array + */ + protected $set = []; + + /** + * @var null|string|Where + */ + protected $where = null; + + /** + * Constructor + * + * @param null|string|TableIdentifier $table + */ + public function __construct($table = null) + { + if ($table) { + $this->from($table); + } + $this->where = new Where(); + } + + /** + * Create from statement + * + * @param string|TableIdentifier $table + * @return self Provides a fluent interface + */ + public function from($table) + { + $this->table = $table; + return $this; + } + + /** + * @param null $key + * + * @return mixed + */ + public function getRawState($key = null) + { + $rawState = [ + 'emptyWhereProtection' => $this->emptyWhereProtection, + 'table' => $this->table, + 'set' => $this->set, + 'where' => $this->where + ]; + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + /** + * Create where clause + * + * @param Where|\Closure|string|array $predicate + * @param string $combination One of the OP_* constants from Predicate\PredicateSet + * + * @return self Provides a fluent interface + */ + public function where($predicate, $combination = Predicate\PredicateSet::OP_AND) + { + if ($predicate instanceof Where) { + $this->where = $predicate; + } else { + $this->where->addPredicates($predicate, $combination); + } + return $this; + } + + /** + * @param PlatformInterface $platform + * @param DriverInterface|null $driver + * @param ParameterContainer|null $parameterContainer + * + * @return string + */ + protected function processDelete( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + return sprintf( + $this->specifications[static::SPECIFICATION_DELETE], + $this->resolveTable($this->table, $platform, $driver, $parameterContainer) + ); + } + + /** + * @param PlatformInterface $platform + * @param DriverInterface|null $driver + * @param ParameterContainer|null $parameterContainer + * + * @return null|string + */ + protected function processWhere( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->where->count() == 0) { + return; + } + + return sprintf( + $this->specifications[static::SPECIFICATION_WHERE], + $this->processExpression($this->where, $platform, $driver, $parameterContainer, 'where') + ); + } + + /** + * Property overloading + * + * Overloads "where" only. + * + * @param string $name + * + * @return Where|null + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'where': + return $this->where; + } + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Exception/ExceptionInterface.php b/bundled-libs/laminas/laminas-db/src/Sql/Exception/ExceptionInterface.php new file mode 100644 index 000000000..ac4d462ae --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Exception/ExceptionInterface.php @@ -0,0 +1,15 @@ +setExpression($expression); + } + + if ($types) { // should be deprecated and removed version 3.0.0 + if (is_array($parameters)) { + foreach ($parameters as $i => $parameter) { + $parameters[$i] = [ + $parameter => isset($types[$i]) ? $types[$i] : self::TYPE_VALUE, + ]; + } + } elseif (is_scalar($parameters)) { + $parameters = [ + $parameters => $types[0], + ]; + } + } + + if ($parameters !== null) { + $this->setParameters($parameters); + } + } + + /** + * @param $expression + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function setExpression($expression) + { + if (! is_string($expression) || $expression == '') { + throw new Exception\InvalidArgumentException('Supplied expression must be a string.'); + } + $this->expression = $expression; + return $this; + } + + /** + * @return string + */ + public function getExpression() + { + return $this->expression; + } + + /** + * @param $parameters + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function setParameters($parameters) + { + if (! is_scalar($parameters) && ! is_array($parameters)) { + throw new Exception\InvalidArgumentException('Expression parameters must be a scalar or array.'); + } + $this->parameters = $parameters; + return $this; + } + + /** + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * @deprecated + * @param array $types + * @return self Provides a fluent interface + */ + public function setTypes(array $types) + { + $this->types = $types; + return $this; + } + + /** + * @deprecated + * @return array + */ + public function getTypes() + { + return $this->types; + } + + /** + * @return array + * @throws Exception\RuntimeException + */ + public function getExpressionData() + { + $parameters = (is_scalar($this->parameters)) ? [$this->parameters] : $this->parameters; + $parametersCount = count($parameters); + $expression = str_replace('%', '%%', $this->expression); + + if ($parametersCount === 0) { + return [ + str_ireplace(self::PLACEHOLDER, '', $expression) + ]; + } + + // assign locally, escaping % signs + $expression = str_replace(self::PLACEHOLDER, '%s', $expression, $count); + + // test number of replacements without considering same variable begin used many times first, which is + // faster, if the test fails then resort to regex which are slow and used rarely + if ($count !== $parametersCount) { + preg_match_all('/\:\w*/', $expression, $matches); + if ($parametersCount !== count(array_unique($matches[0]))) { + throw new Exception\RuntimeException( + 'The number of replacements in the expression does not match the number of parameters' + ); + } + } + + foreach ($parameters as $parameter) { + list($values[], $types[]) = $this->normalizeArgument($parameter, self::TYPE_VALUE); + } + return [[ + $expression, + $values, + $types + ]]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/ExpressionInterface.php b/bundled-libs/laminas/laminas-db/src/Sql/ExpressionInterface.php new file mode 100644 index 000000000..c2120e0c8 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/ExpressionInterface.php @@ -0,0 +1,36 @@ + 'INSERT INTO %1$s (%2$s) VALUES (%3$s)', + self::SPECIFICATION_SELECT => 'INSERT INTO %1$s %2$s %3$s', + ]; + + /** + * @var string|TableIdentifier + */ + protected $table = null; + protected $columns = []; + + /** + * @var array|Select + */ + protected $select = null; + + /** + * Constructor + * + * @param null|string|TableIdentifier $table + */ + public function __construct($table = null) + { + if ($table) { + $this->into($table); + } + } + + /** + * Create INTO clause + * + * @param string|TableIdentifier $table + * @return self Provides a fluent interface + */ + public function into($table) + { + $this->table = $table; + return $this; + } + + /** + * Specify columns + * + * @param array $columns + * @return self Provides a fluent interface + */ + public function columns(array $columns) + { + $this->columns = array_flip($columns); + return $this; + } + + /** + * Specify values to insert + * + * @param array|Select $values + * @param string $flag one of VALUES_MERGE or VALUES_SET; defaults to VALUES_SET + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function values($values, $flag = self::VALUES_SET) + { + if ($values instanceof Select) { + if ($flag == self::VALUES_MERGE) { + throw new Exception\InvalidArgumentException( + 'A Laminas\Db\Sql\Select instance cannot be provided with the merge flag' + ); + } + $this->select = $values; + return $this; + } + + if (! is_array($values)) { + throw new Exception\InvalidArgumentException( + 'values() expects an array of values or Laminas\Db\Sql\Select instance' + ); + } + + if ($this->select && $flag == self::VALUES_MERGE) { + throw new Exception\InvalidArgumentException( + 'An array of values cannot be provided with the merge flag when a Laminas\Db\Sql\Select' + . ' instance already exists as the value source' + ); + } + + if ($flag == self::VALUES_SET) { + $this->columns = $this->isAssocativeArray($values) + ? $values + : array_combine(array_keys($this->columns), array_values($values)); + } else { + foreach ($values as $column => $value) { + $this->columns[$column] = $value; + } + } + return $this; + } + + + /** + * Simple test for an associative array + * + * @link http://stackoverflow.com/questions/173400/how-to-check-if-php-array-is-associative-or-sequential + * @param array $array + * @return bool + */ + private function isAssocativeArray(array $array) + { + return array_keys($array) !== range(0, count($array) - 1); + } + + /** + * Create INTO SELECT clause + * + * @param Select $select + * @return self + */ + public function select(Select $select) + { + return $this->values($select); + } + + /** + * Get raw state + * + * @param string $key + * @return mixed + */ + public function getRawState($key = null) + { + $rawState = [ + 'table' => $this->table, + 'columns' => array_keys($this->columns), + 'values' => array_values($this->columns) + ]; + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + protected function processInsert( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->select) { + return; + } + if (! $this->columns) { + throw new Exception\InvalidArgumentException('values or select should be present'); + } + + $columns = []; + $values = []; + $i = 0; + + foreach ($this->columns as $column => $value) { + $columns[] = $platform->quoteIdentifier($column); + if (is_scalar($value) && $parameterContainer) { + // use incremental value instead of column name for PDO + // @see https://github.com/zendframework/zend-db/issues/35 + if ($driver instanceof Pdo) { + $column = 'c_' . $i++; + } + $values[] = $driver->formatParameterName($column); + $parameterContainer->offsetSet($column, $value); + } else { + $values[] = $this->resolveColumnValue( + $value, + $platform, + $driver, + $parameterContainer + ); + } + } + return sprintf( + $this->specifications[static::SPECIFICATION_INSERT], + $this->resolveTable($this->table, $platform, $driver, $parameterContainer), + implode(', ', $columns), + implode(', ', $values) + ); + } + + protected function processSelect( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if (! $this->select) { + return; + } + $selectSql = $this->processSubSelect($this->select, $platform, $driver, $parameterContainer); + + $columns = array_map([$platform, 'quoteIdentifier'], array_keys($this->columns)); + $columns = implode(', ', $columns); + + return sprintf( + $this->specifications[static::SPECIFICATION_SELECT], + $this->resolveTable($this->table, $platform, $driver, $parameterContainer), + $columns ? "($columns)" : "", + $selectSql + ); + } + + /** + * Overloading: variable setting + * + * Proxies to values, using VALUES_MERGE strategy + * + * @param string $name + * @param mixed $value + * @return self Provides a fluent interface + */ + public function __set($name, $value) + { + $this->columns[$name] = $value; + return $this; + } + + /** + * Overloading: variable unset + * + * Proxies to values and columns + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @return void + */ + public function __unset($name) + { + if (! array_key_exists($name, $this->columns)) { + throw new Exception\InvalidArgumentException( + 'The key ' . $name . ' was not found in this objects column list' + ); + } + + unset($this->columns[$name]); + } + + /** + * Overloading: variable isset + * + * Proxies to columns; does a column of that name exist? + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + return array_key_exists($name, $this->columns); + } + + /** + * Overloading: variable retrieval + * + * Retrieves value by column name + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function __get($name) + { + if (! array_key_exists($name, $this->columns)) { + throw new Exception\InvalidArgumentException( + 'The key ' . $name . ' was not found in this objects column list' + ); + } + return $this->columns[$name]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/InsertIgnore.php b/bundled-libs/laminas/laminas-db/src/Sql/InsertIgnore.php new file mode 100644 index 000000000..50f1cc02e --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/InsertIgnore.php @@ -0,0 +1,20 @@ + 'INSERT IGNORE INTO %1$s (%2$s) VALUES (%3$s)', + self::SPECIFICATION_SELECT => 'INSERT IGNORE INTO %1$s %2$s %3$s', + ]; +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Join.php b/bundled-libs/laminas/laminas-db/src/Sql/Join.php new file mode 100644 index 000000000..71e3c4d99 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Join.php @@ -0,0 +1,165 @@ +position = 0; + } + + /** + * Rewind iterator. + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Return current join specification. + * + * @return array + */ + public function current() + { + return $this->joins[$this->position]; + } + + /** + * Return the current iterator index. + * + * @return int + */ + public function key() + { + return $this->position; + } + + /** + * Advance to the next JOIN specification. + */ + public function next() + { + ++$this->position; + } + + /** + * Is the iterator at a valid position? + * + * @return bool + */ + public function valid() + { + return isset($this->joins[$this->position]); + } + + /** + * @return array + */ + public function getJoins() + { + return $this->joins; + } + + /** + * @param string|array|TableIdentifier $name A table name on which to join, or a single + * element associative array, of the form alias => table, or TableIdentifier instance + * @param string|Predicate\Expression $on A specification describing the fields to join on. + * @param string|string[]|int|int[] $columns A single column name, an array + * of column names, or (a) specification(s) such as SQL_STAR representing + * the columns to join. + * @param string $type The JOIN type to use; see the JOIN_* constants. + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException for invalid $name values. + */ + public function join($name, $on, $columns = [Select::SQL_STAR], $type = Join::JOIN_INNER) + { + if (is_array($name) && (! is_string(key($name)) || count($name) !== 1)) { + throw new Exception\InvalidArgumentException( + sprintf("join() expects '%s' as a single element associative array", array_shift($name)) + ); + } + + if (! is_array($columns)) { + $columns = [$columns]; + } + + $this->joins[] = [ + 'name' => $name, + 'on' => $on, + 'columns' => $columns, + 'type' => $type ? $type : Join::JOIN_INNER + ]; + + return $this; + } + + /** + * Reset to an empty list of JOIN specifications. + * + * @return self Provides a fluent interface + */ + public function reset() + { + $this->joins = []; + return $this; + } + + /** + * Get count of attached predicates + * + * @return int + */ + public function count() + { + return count($this->joins); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Literal.php b/bundled-libs/laminas/laminas-db/src/Sql/Literal.php new file mode 100644 index 000000000..aec00dc78 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Literal.php @@ -0,0 +1,55 @@ +literal = $literal; + } + + /** + * @param string $literal + * @return self Provides a fluent interface + */ + public function setLiteral($literal) + { + $this->literal = $literal; + return $this; + } + + /** + * @return string + */ + public function getLiteral() + { + return $this->literal; + } + + /** + * @return array + */ + public function getExpressionData() + { + return [[ + str_replace('%', '%%', $this->literal), + [], + [] + ]]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/AbstractPlatform.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/AbstractPlatform.php new file mode 100644 index 000000000..98fb4c5e5 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/AbstractPlatform.php @@ -0,0 +1,111 @@ +subject = $subject; + + return $this; + } + + /** + * @param string $type + * @param PlatformDecoratorInterface $decorator + * + * @return void + */ + public function setTypeDecorator($type, PlatformDecoratorInterface $decorator) + { + $this->decorators[$type] = $decorator; + } + + /** + * @param PreparableSqlInterface|SqlInterface $subject + * @return PlatformDecoratorInterface|PreparableSqlInterface|SqlInterface + */ + public function getTypeDecorator($subject) + { + foreach ($this->decorators as $type => $decorator) { + if ($subject instanceof $type) { + $decorator->setSubject($subject); + + return $decorator; + } + } + + return $subject; + } + + /** + * @return array|PlatformDecoratorInterface[] + */ + public function getDecorators() + { + return $this->decorators; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + if (! $this->subject instanceof PreparableSqlInterface) { + throw new Exception\RuntimeException( + 'The subject does not appear to implement Laminas\Db\Sql\PreparableSqlInterface, thus calling ' + . 'prepareStatement() has no effect' + ); + } + + $this->getTypeDecorator($this->subject)->prepareStatement($adapter, $statementContainer); + + return $statementContainer; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + if (! $this->subject instanceof SqlInterface) { + throw new Exception\RuntimeException( + 'The subject does not appear to implement Laminas\Db\Sql\SqlInterface, thus calling ' + . 'prepareStatement() has no effect' + ); + } + + return $this->getTypeDecorator($this->subject)->getSqlString($adapterPlatform); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/IbmDb2/IbmDb2.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/IbmDb2/IbmDb2.php new file mode 100644 index 000000000..188b69bb4 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/IbmDb2/IbmDb2.php @@ -0,0 +1,22 @@ +setTypeDecorator('Laminas\Db\Sql\Select', ($selectDecorator) ?: new SelectDecorator()); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/IbmDb2/SelectDecorator.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/IbmDb2/SelectDecorator.php new file mode 100644 index 000000000..7c21f3bcf --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/IbmDb2/SelectDecorator.php @@ -0,0 +1,211 @@ +isSelectContainDistinct; + } + + /** + * @param boolean $isSelectContainDistinct + */ + public function setIsSelectContainDistinct($isSelectContainDistinct) + { + $this->isSelectContainDistinct = $isSelectContainDistinct; + } + + /** + * @param Select $select + */ + public function setSubject($select) + { + $this->subject = $select; + } + + /** + * @return bool + */ + public function getSupportsLimitOffset() + { + return $this->supportsLimitOffset; + } + + /** + * @param bool $supportsLimitOffset + */ + public function setSupportsLimitOffset($supportsLimitOffset) + { + $this->supportsLimitOffset = $supportsLimitOffset; + } + + /** + * @see Select::renderTable + */ + protected function renderTable($table, $alias = null) + { + return $table . ' ' . $alias; + } + + protected function localizeVariables() + { + parent::localizeVariables(); + // set specifications + unset($this->specifications[self::LIMIT]); + unset($this->specifications[self::OFFSET]); + + $this->specifications['LIMITOFFSET'] = null; + } + + /** + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @param array $sqls + * @param array $parameters + */ + protected function processLimitOffset( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null, + &$sqls, + &$parameters + ) { + if ($this->limit === null && $this->offset === null) { + return; + } + + if ($this->supportsLimitOffset) { + // Note: db2_prepare/db2_execute fails with positional parameters, for LIMIT & OFFSET + $limit = (int) $this->limit; + if (! $limit) { + return; + } + + $offset = (int) $this->offset; + if ($offset) { + array_push($sqls, sprintf("LIMIT %s OFFSET %s", $limit, $offset)); + return; + } + + array_push($sqls, sprintf("LIMIT %s", $limit)); + return; + } + + $selectParameters = $parameters[self::SELECT]; + + $starSuffix = $platform->getIdentifierSeparator() . self::SQL_STAR; + foreach ($selectParameters[0] as $i => $columnParameters) { + if ($columnParameters[0] == self::SQL_STAR + || (isset($columnParameters[1]) && $columnParameters[1] == self::SQL_STAR) + || strpos($columnParameters[0], $starSuffix) + ) { + $selectParameters[0] = [[self::SQL_STAR]]; + break; + } + + if (isset($columnParameters[1])) { + array_shift($columnParameters); + $selectParameters[0][$i] = $columnParameters; + } + } + + // first, produce column list without compound names (using the AS portion only) + array_unshift($sqls, $this->createSqlFromSpecificationAndParameters( + ['SELECT %1$s FROM (' => current($this->specifications[self::SELECT])], + $selectParameters + )); + + if (preg_match('/DISTINCT/i', $sqls[0])) { + $this->setIsSelectContainDistinct(true); + } + + if ($parameterContainer) { + // create bottom part of query, with offset and limit using row_number + $limitParamName = $driver->formatParameterName('limit'); + $offsetParamName = $driver->formatParameterName('offset'); + + array_push($sqls, sprintf( + // @codingStandardsIgnoreStart + ") AS LAMINAS_IBMDB2_SERVER_LIMIT_OFFSET_EMULATION WHERE LAMINAS_IBMDB2_SERVER_LIMIT_OFFSET_EMULATION.LAMINAS_DB_ROWNUM BETWEEN %s AND %s", + // @codingStandardsIgnoreEnd + $offsetParamName, + $limitParamName + )); + + if ((int) $this->offset > 0) { + $parameterContainer->offsetSet('offset', (int) $this->offset + 1); + } else { + $parameterContainer->offsetSet('offset', (int) $this->offset); + } + + $parameterContainer->offsetSet('limit', (int) $this->limit + (int) $this->offset); + } else { + if ((int) $this->offset > 0) { + $offset = (int) $this->offset + 1; + } else { + $offset = (int) $this->offset; + } + + array_push($sqls, sprintf( + // @codingStandardsIgnoreStart + ") AS LAMINAS_IBMDB2_SERVER_LIMIT_OFFSET_EMULATION WHERE LAMINAS_IBMDB2_SERVER_LIMIT_OFFSET_EMULATION.LAMINAS_DB_ROWNUM BETWEEN %d AND %d", + // @codingStandardsIgnoreEnd + $offset, + (int) $this->limit + (int) $this->offset + )); + } + + if (isset($sqls[self::ORDER])) { + $orderBy = $sqls[self::ORDER]; + unset($sqls[self::ORDER]); + } else { + $orderBy = ''; + } + + // add a column for row_number() using the order specification //dense_rank() + if ($this->getIsSelectContainDistinct()) { + $parameters[self::SELECT][0][] = ['DENSE_RANK() OVER (' . $orderBy . ')', 'LAMINAS_DB_ROWNUM']; + } else { + $parameters[self::SELECT][0][] = ['ROW_NUMBER() OVER (' . $orderBy . ')', 'LAMINAS_DB_ROWNUM']; + } + + $sqls[self::SELECT] = $this->createSqlFromSpecificationAndParameters( + $this->specifications[self::SELECT], + $parameters[self::SELECT] + ); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php new file mode 100644 index 000000000..ff9ed1ce2 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php @@ -0,0 +1,250 @@ + 0, + 'zerofill' => 1, + 'identity' => 2, + 'serial' => 2, + 'autoincrement' => 2, + 'comment' => 3, + 'columnformat' => 4, + 'format' => 4, + 'storage' => 5, + 'after' => 6 + ]; + + /** + * @param AlterTable $subject + * @return self Provides a fluent interface + */ + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * @param string $sql + * @return array + */ + protected function getSqlInsertOffsets($sql) + { + $sqlLength = strlen($sql); + $insertStart = []; + + foreach (['NOT NULL', 'NULL', 'DEFAULT', 'UNIQUE', 'PRIMARY', 'REFERENCES'] as $needle) { + $insertPos = strpos($sql, ' ' . $needle); + + if ($insertPos !== false) { + switch ($needle) { + case 'REFERENCES': + $insertStart[2] = ! isset($insertStart[2]) ? $insertPos : $insertStart[2]; + // no break + case 'PRIMARY': + case 'UNIQUE': + $insertStart[1] = ! isset($insertStart[1]) ? $insertPos : $insertStart[1]; + // no break + default: + $insertStart[0] = ! isset($insertStart[0]) ? $insertPos : $insertStart[0]; + } + } + } + + foreach (range(0, 3) as $i) { + $insertStart[$i] = isset($insertStart[$i]) ? $insertStart[$i] : $sqlLength; + } + + return $insertStart; + } + + /** + * @param PlatformInterface $adapterPlatform + * @return array + */ + protected function processAddColumns(PlatformInterface $adapterPlatform = null) + { + $sqls = []; + + foreach ($this->addColumns as $i => $column) { + $sql = $this->processExpression($column, $adapterPlatform); + $insertStart = $this->getSqlInsertOffsets($sql); + $columnOptions = $column->getOptions(); + + uksort($columnOptions, [$this, 'compareColumnOptions']); + + foreach ($columnOptions as $coName => $coValue) { + $insert = ''; + + if (! $coValue) { + continue; + } + + switch ($this->normalizeColumnOption($coName)) { + case 'unsigned': + $insert = ' UNSIGNED'; + $j = 0; + break; + case 'zerofill': + $insert = ' ZEROFILL'; + $j = 0; + break; + case 'identity': + case 'serial': + case 'autoincrement': + $insert = ' AUTO_INCREMENT'; + $j = 1; + break; + case 'comment': + $insert = ' COMMENT ' . $adapterPlatform->quoteValue($coValue); + $j = 2; + break; + case 'columnformat': + case 'format': + $insert = ' COLUMN_FORMAT ' . strtoupper($coValue); + $j = 2; + break; + case 'storage': + $insert = ' STORAGE ' . strtoupper($coValue); + $j = 2; + break; + case 'after': + $insert = ' AFTER ' . $adapterPlatform->quoteIdentifier($coValue); + $j = 2; + } + + if ($insert) { + $j = isset($j) ? $j : 0; + $sql = substr_replace($sql, $insert, $insertStart[$j], 0); + $insertStartCount = count($insertStart); + for (; $j < $insertStartCount; ++$j) { + $insertStart[$j] += strlen($insert); + } + } + } + $sqls[$i] = $sql; + } + return [$sqls]; + } + + /** + * @param PlatformInterface $adapterPlatform + * @return array + */ + protected function processChangeColumns(PlatformInterface $adapterPlatform = null) + { + $sqls = []; + foreach ($this->changeColumns as $name => $column) { + $sql = $this->processExpression($column, $adapterPlatform); + $insertStart = $this->getSqlInsertOffsets($sql); + $columnOptions = $column->getOptions(); + + uksort($columnOptions, [$this, 'compareColumnOptions']); + + foreach ($columnOptions as $coName => $coValue) { + $insert = ''; + + if (! $coValue) { + continue; + } + + switch ($this->normalizeColumnOption($coName)) { + case 'unsigned': + $insert = ' UNSIGNED'; + $j = 0; + break; + case 'zerofill': + $insert = ' ZEROFILL'; + $j = 0; + break; + case 'identity': + case 'serial': + case 'autoincrement': + $insert = ' AUTO_INCREMENT'; + $j = 1; + break; + case 'comment': + $insert = ' COMMENT ' . $adapterPlatform->quoteValue($coValue); + $j = 2; + break; + case 'columnformat': + case 'format': + $insert = ' COLUMN_FORMAT ' . strtoupper($coValue); + $j = 2; + break; + case 'storage': + $insert = ' STORAGE ' . strtoupper($coValue); + $j = 2; + break; + } + + if ($insert) { + $j = isset($j) ? $j : 0; + $sql = substr_replace($sql, $insert, $insertStart[$j], 0); + $insertStartCount = count($insertStart); + for (; $j < $insertStartCount; ++$j) { + $insertStart[$j] += strlen($insert); + } + } + } + $sqls[] = [ + $adapterPlatform->quoteIdentifier($name), + $sql + ]; + } + + return [$sqls]; + } + + /** + * @param string $name + * + * @return string + */ + private function normalizeColumnOption($name) + { + return strtolower(str_replace(['-', '_', ' '], '', $name)); + } + + /** + * + * @param string $columnA + * @param string $columnB + * + * @return int + */ + private function compareColumnOptions($columnA, $columnB) + { + $columnA = $this->normalizeColumnOption($columnA); + $columnA = isset($this->columnOptionSortOrder[$columnA]) + ? $this->columnOptionSortOrder[$columnA] : count($this->columnOptionSortOrder); + + $columnB = $this->normalizeColumnOption($columnB); + $columnB = isset($this->columnOptionSortOrder[$columnB]) + ? $this->columnOptionSortOrder[$columnB] : count($this->columnOptionSortOrder); + + return $columnA - $columnB; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php new file mode 100644 index 000000000..d33e8c6ea --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php @@ -0,0 +1,183 @@ + 0, + 'zerofill' => 1, + 'identity' => 2, + 'serial' => 2, + 'autoincrement' => 2, + 'comment' => 3, + 'columnformat' => 4, + 'format' => 4, + 'storage' => 5, + ]; + + /** + * @param CreateTable $subject + * + * @return self Provides a fluent interface + */ + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * @param string $sql + * @return array + */ + protected function getSqlInsertOffsets($sql) + { + $sqlLength = strlen($sql); + $insertStart = []; + + foreach (['NOT NULL', 'NULL', 'DEFAULT', 'UNIQUE', 'PRIMARY', 'REFERENCES'] as $needle) { + $insertPos = strpos($sql, ' ' . $needle); + + if ($insertPos !== false) { + switch ($needle) { + case 'REFERENCES': + $insertStart[2] = ! isset($insertStart[2]) ? $insertPos : $insertStart[2]; + // no break + case 'PRIMARY': + case 'UNIQUE': + $insertStart[1] = ! isset($insertStart[1]) ? $insertPos : $insertStart[1]; + // no break + default: + $insertStart[0] = ! isset($insertStart[0]) ? $insertPos : $insertStart[0]; + } + } + } + + foreach (range(0, 3) as $i) { + $insertStart[$i] = isset($insertStart[$i]) ? $insertStart[$i] : $sqlLength; + } + + return $insertStart; + } + + /** + * {@inheritDoc} + */ + protected function processColumns(PlatformInterface $platform = null) + { + if (! $this->columns) { + return; + } + + $sqls = []; + + foreach ($this->columns as $i => $column) { + $sql = $this->processExpression($column, $platform); + $insertStart = $this->getSqlInsertOffsets($sql); + $columnOptions = $column->getOptions(); + + uksort($columnOptions, [$this, 'compareColumnOptions']); + + foreach ($columnOptions as $coName => $coValue) { + $insert = ''; + + if (! $coValue) { + continue; + } + + switch ($this->normalizeColumnOption($coName)) { + case 'unsigned': + $insert = ' UNSIGNED'; + $j = 0; + break; + case 'zerofill': + $insert = ' ZEROFILL'; + $j = 0; + break; + case 'identity': + case 'serial': + case 'autoincrement': + $insert = ' AUTO_INCREMENT'; + $j = 1; + break; + case 'comment': + $insert = ' COMMENT ' . $platform->quoteValue($coValue); + $j = 2; + break; + case 'columnformat': + case 'format': + $insert = ' COLUMN_FORMAT ' . strtoupper($coValue); + $j = 2; + break; + case 'storage': + $insert = ' STORAGE ' . strtoupper($coValue); + $j = 2; + break; + } + + if ($insert) { + $j = isset($j) ? $j : 0; + $sql = substr_replace($sql, $insert, $insertStart[$j], 0); + $insertStartCount = count($insertStart); + for (; $j < $insertStartCount; ++$j) { + $insertStart[$j] += strlen($insert); + } + } + } + + $sqls[$i] = $sql; + } + + return [$sqls]; + } + + /** + * @param string $name + * + * @return string + */ + private function normalizeColumnOption($name) + { + return strtolower(str_replace(['-', '_', ' '], '', $name)); + } + + /** + * + * @param string $columnA + * @param string $columnB + * + * @return int + */ + private function compareColumnOptions($columnA, $columnB) + { + $columnA = $this->normalizeColumnOption($columnA); + $columnA = isset($this->columnOptionSortOrder[$columnA]) + ? $this->columnOptionSortOrder[$columnA] : count($this->columnOptionSortOrder); + + $columnB = $this->normalizeColumnOption($columnB); + $columnB = isset($this->columnOptionSortOrder[$columnB]) + ? $this->columnOptionSortOrder[$columnB] : count($this->columnOptionSortOrder); + + return $columnA - $columnB; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/Mysql.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/Mysql.php new file mode 100644 index 000000000..e24f0daf3 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/Mysql.php @@ -0,0 +1,21 @@ +setTypeDecorator('Laminas\Db\Sql\Select', new SelectDecorator()); + $this->setTypeDecorator('Laminas\Db\Sql\Ddl\CreateTable', new Ddl\CreateTableDecorator()); + $this->setTypeDecorator('Laminas\Db\Sql\Ddl\AlterTable', new Ddl\AlterTableDecorator()); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/SelectDecorator.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/SelectDecorator.php new file mode 100644 index 000000000..ab1cb8760 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Mysql/SelectDecorator.php @@ -0,0 +1,76 @@ +subject = $select; + } + + protected function localizeVariables() + { + parent::localizeVariables(); + if ($this->limit === null && $this->offset !== null) { + $this->specifications[self::LIMIT] = 'LIMIT 18446744073709551615'; + } + } + + protected function processLimit( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->limit === null && $this->offset !== null) { + return ['']; + } + if ($this->limit === null) { + return; + } + if ($parameterContainer) { + $paramPrefix = $this->processInfo['paramPrefix']; + $parameterContainer->offsetSet($paramPrefix . 'limit', $this->limit, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName($paramPrefix . 'limit')]; + } + + return [$this->limit]; + } + + protected function processOffset( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->offset === null) { + return; + } + if ($parameterContainer) { + $paramPrefix = $this->processInfo['paramPrefix']; + $parameterContainer->offsetSet($paramPrefix . 'offset', $this->offset, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName($paramPrefix . 'offset')]; + } + + return [$this->offset]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/Oracle/Oracle.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Oracle/Oracle.php new file mode 100644 index 000000000..11e8f5ac7 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Oracle/Oracle.php @@ -0,0 +1,19 @@ +setTypeDecorator('Laminas\Db\Sql\Select', ($selectDecorator) ?: new SelectDecorator()); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/Oracle/SelectDecorator.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Oracle/SelectDecorator.php new file mode 100644 index 000000000..0731c43bb --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Oracle/SelectDecorator.php @@ -0,0 +1,154 @@ +subject = $select; + } + + /** + * @see \Laminas\Db\Sql\Select::renderTable + */ + protected function renderTable($table, $alias = null) + { + return $table . ($alias ? ' ' . $alias : ''); + } + + protected function localizeVariables() + { + parent::localizeVariables(); + unset($this->specifications[self::LIMIT]); + unset($this->specifications[self::OFFSET]); + + $this->specifications['LIMITOFFSET'] = null; + } + + /** + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @param array $sqls + * @param array $parameters + * @return null + */ + protected function processLimitOffset( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null, + &$sqls = [], + &$parameters = [] + ) { + if ($this->limit === null && $this->offset === null) { + return; + } + + $selectParameters = $parameters[self::SELECT]; + $starSuffix = $platform->getIdentifierSeparator() . self::SQL_STAR; + + foreach ($selectParameters[0] as $i => $columnParameters) { + if ($columnParameters[0] == self::SQL_STAR + || (isset($columnParameters[1]) && $columnParameters[1] == self::SQL_STAR) + || strpos($columnParameters[0], $starSuffix) + ) { + $selectParameters[0] = [[self::SQL_STAR]]; + break; + } + + if (isset($columnParameters[1])) { + array_shift($columnParameters); + $selectParameters[0][$i] = $columnParameters; + } + } + + if ($this->offset === null) { + $this->offset = 0; + } + + // first, produce column list without compound names (using the AS portion only) + array_unshift($sqls, $this->createSqlFromSpecificationAndParameters([ + 'SELECT %1$s FROM (SELECT b.%1$s, rownum b_rownum FROM (' => current($this->specifications[self::SELECT]), + ], $selectParameters)); + + if ($parameterContainer) { + $number = $this->processInfo['subselectCount'] ? $this->processInfo['subselectCount'] : ''; + + if ($this->limit === null) { + array_push( + $sqls, + ') b ) WHERE b_rownum > (:offset' . $number . ')' + ); + $parameterContainer->offsetSet( + 'offset' . $number, + $this->offset, + $parameterContainer::TYPE_INTEGER + ); + } else { + // create bottom part of query, with offset and limit using row_number + array_push( + $sqls, + ') b WHERE rownum <= (:offset' + . $number + . '+:limit' + . $number + . ')) WHERE b_rownum >= (:offset' + . $number + . ' + 1)' + ); + $parameterContainer->offsetSet( + 'offset' . $number, + $this->offset, + $parameterContainer::TYPE_INTEGER + ); + $parameterContainer->offsetSet( + 'limit' . $number, + $this->limit, + $parameterContainer::TYPE_INTEGER + ); + } + $this->processInfo['subselectCount']++; + } else { + if ($this->limit === null) { + array_push($sqls, ') b ) WHERE b_rownum > (' . (int) $this->offset . ')'); + } else { + array_push( + $sqls, + ') b WHERE rownum <= (' + . (int) $this->offset + . '+' + . (int) $this->limit + . ')) WHERE b_rownum >= (' + . (int) $this->offset + . ' + 1)' + ); + } + } + + $sqls[self::SELECT] = $this->createSqlFromSpecificationAndParameters( + $this->specifications[self::SELECT], + $parameters[self::SELECT] + ); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/Platform.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Platform.php new file mode 100644 index 000000000..9d1cb781b --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Platform.php @@ -0,0 +1,172 @@ +defaultPlatform = $adapter->getPlatform(); + + $mySqlPlatform = new Mysql\Mysql(); + $sqlServerPlatform = new SqlServer\SqlServer(); + $oraclePlatform = new Oracle\Oracle(); + $ibmDb2Platform = new IbmDb2\IbmDb2(); + $sqlitePlatform = new Sqlite\Sqlite(); + + $this->decorators['mysql'] = $mySqlPlatform->getDecorators(); + $this->decorators['sqlserver'] = $sqlServerPlatform->getDecorators(); + $this->decorators['oracle'] = $oraclePlatform->getDecorators(); + $this->decorators['ibmdb2'] = $ibmDb2Platform->getDecorators(); + $this->decorators['sqlite'] = $sqlitePlatform->getDecorators(); + } + + /** + * @param string $type + * @param PlatformDecoratorInterface $decorator + * @param AdapterInterface|PlatformInterface $adapterOrPlatform + */ + public function setTypeDecorator($type, PlatformDecoratorInterface $decorator, $adapterOrPlatform = null) + { + $platformName = $this->resolvePlatformName($adapterOrPlatform); + $this->decorators[$platformName][$type] = $decorator; + } + + /** + * @param PreparableSqlInterface|SqlInterface $subject + * @param AdapterInterface|PlatformInterface|null $adapterOrPlatform + * @return PlatformDecoratorInterface|PreparableSqlInterface|SqlInterface + */ + public function getTypeDecorator($subject, $adapterOrPlatform = null) + { + $platformName = $this->resolvePlatformName($adapterOrPlatform); + + if (isset($this->decorators[$platformName])) { + foreach ($this->decorators[$platformName] as $type => $decorator) { + if ($subject instanceof $type && is_a($decorator, $type, true)) { + $decorator->setSubject($subject); + return $decorator; + } + } + } + + return $subject; + } + + /** + * @return array|PlatformDecoratorInterface[] + */ + public function getDecorators() + { + $platformName = $this->resolvePlatformName($this->getDefaultPlatform()); + return $this->decorators[$platformName]; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + if (! $this->subject instanceof PreparableSqlInterface) { + throw new Exception\RuntimeException( + 'The subject does not appear to implement Laminas\Db\Sql\PreparableSqlInterface, thus calling ' + . 'prepareStatement() has no effect' + ); + } + + $this->getTypeDecorator($this->subject, $adapter)->prepareStatement($adapter, $statementContainer); + + return $statementContainer; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + if (! $this->subject instanceof SqlInterface) { + throw new Exception\RuntimeException( + 'The subject does not appear to implement Laminas\Db\Sql\SqlInterface, thus calling ' + . 'prepareStatement() has no effect' + ); + } + + $adapterPlatform = $this->resolvePlatform($adapterPlatform); + + return $this->getTypeDecorator($this->subject, $adapterPlatform)->getSqlString($adapterPlatform); + } + + protected function resolvePlatformName($adapterOrPlatform) + { + $platformName = $this->resolvePlatform($adapterOrPlatform)->getName(); + return str_replace([' ', '_'], '', strtolower($platformName)); + } + /** + * @param null|PlatformInterface|AdapterInterface $adapterOrPlatform + * + * @return PlatformInterface + * + * @throws Exception\InvalidArgumentException + */ + protected function resolvePlatform($adapterOrPlatform) + { + if (! $adapterOrPlatform) { + return $this->getDefaultPlatform(); + } + + if ($adapterOrPlatform instanceof AdapterInterface) { + return $adapterOrPlatform->getPlatform(); + } + + if ($adapterOrPlatform instanceof PlatformInterface) { + return $adapterOrPlatform; + } + + throw new Exception\InvalidArgumentException(sprintf( + '$adapterOrPlatform should be null, %s, or %s', + 'Laminas\Db\Adapter\AdapterInterface', + 'Laminas\Db\Adapter\Platform\PlatformInterface' + )); + } + + /** + * @return PlatformInterface + * + * @throws Exception\RuntimeException + */ + protected function getDefaultPlatform() + { + if (! $this->defaultPlatform) { + throw new Exception\RuntimeException('$this->defaultPlatform was not set'); + } + + return $this->defaultPlatform; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/PlatformDecoratorInterface.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/PlatformDecoratorInterface.php new file mode 100644 index 000000000..cdb4fc39d --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/PlatformDecoratorInterface.php @@ -0,0 +1,19 @@ +subject = $subject; + return $this; + } + + /** + * @param PlatformInterface $adapterPlatform + * @return array + */ + protected function processTable(PlatformInterface $adapterPlatform = null) + { + $table = ($this->isTemporary ? '#' : '') . ltrim($this->table, '#'); + return [ + '', + $adapterPlatform->quoteIdentifier($table), + ]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/SqlServer/SelectDecorator.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/SqlServer/SelectDecorator.php new file mode 100644 index 000000000..f27c39986 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/SqlServer/SelectDecorator.php @@ -0,0 +1,130 @@ +subject = $select; + } + + protected function localizeVariables() + { + parent::localizeVariables(); + // set specifications + unset($this->specifications[self::LIMIT]); + unset($this->specifications[self::OFFSET]); + + $this->specifications['LIMITOFFSET'] = null; + } + + /** + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @param $sqls + * @param $parameters + * @return null + */ + protected function processLimitOffset( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null, + &$sqls, + &$parameters + ) { + if ($this->limit === null && $this->offset === null) { + return; + } + + $selectParameters = $parameters[self::SELECT]; + + /** if this is a DISTINCT query then real SELECT part goes to second element in array **/ + $parameterIndex = 0; + if ($selectParameters[0] === 'DISTINCT') { + unset($selectParameters[0]); + $selectParameters = array_values($selectParameters); + $parameterIndex = 1; + } + + $starSuffix = $platform->getIdentifierSeparator() . self::SQL_STAR; + foreach ($selectParameters[0] as $i => $columnParameters) { + if ($columnParameters[0] == self::SQL_STAR + || (isset($columnParameters[1]) && $columnParameters[1] == self::SQL_STAR) + || strpos($columnParameters[0], $starSuffix) + ) { + $selectParameters[0] = [[self::SQL_STAR]]; + break; + } + if (isset($columnParameters[1])) { + array_shift($columnParameters); + $selectParameters[0][$i] = $columnParameters; + } + } + + // first, produce column list without compound names (using the AS portion only) + array_unshift($sqls, $this->createSqlFromSpecificationAndParameters( + ['SELECT %1$s FROM (' => current($this->specifications[self::SELECT])], + $selectParameters + )); + + if ($parameterContainer) { + // create bottom part of query, with offset and limit using row_number + $limitParamName = $driver->formatParameterName('limit'); + $offsetParamName = $driver->formatParameterName('offset'); + $offsetForSumParamName = $driver->formatParameterName('offsetForSum'); + // @codingStandardsIgnoreStart + array_push($sqls, ') AS [LAMINAS_SQL_SERVER_LIMIT_OFFSET_EMULATION] WHERE [LAMINAS_SQL_SERVER_LIMIT_OFFSET_EMULATION].[__LAMINAS_ROW_NUMBER] BETWEEN ' + . $offsetParamName . '+1 AND ' . $limitParamName . '+' . $offsetForSumParamName); + // @codingStandardsIgnoreEnd + $parameterContainer->offsetSet('offset', $this->offset); + $parameterContainer->offsetSet('limit', $this->limit); + $parameterContainer->offsetSetReference('offsetForSum', 'offset'); + } else { + // @codingStandardsIgnoreStart + array_push($sqls, ') AS [LAMINAS_SQL_SERVER_LIMIT_OFFSET_EMULATION] WHERE [LAMINAS_SQL_SERVER_LIMIT_OFFSET_EMULATION].[__LAMINAS_ROW_NUMBER] BETWEEN ' + . (int) $this->offset . '+1 AND ' + . (int) $this->limit . '+' . (int) $this->offset); + // @codingStandardsIgnoreEnd + } + + if (isset($sqls[self::ORDER])) { + $orderBy = $sqls[self::ORDER]; + unset($sqls[self::ORDER]); + } else { + $orderBy = 'ORDER BY (SELECT 1)'; + } + + // add a column for row_number() using the order specification + $parameters[self::SELECT][$parameterIndex][] = [ + 'ROW_NUMBER() OVER (' . $orderBy . ')', + '[__LAMINAS_ROW_NUMBER]', + ]; + + $sqls[self::SELECT] = $this->createSqlFromSpecificationAndParameters( + $this->specifications[self::SELECT], + $parameters[self::SELECT] + ); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/SqlServer/SqlServer.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/SqlServer/SqlServer.php new file mode 100644 index 000000000..52bd20ec0 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/SqlServer/SqlServer.php @@ -0,0 +1,20 @@ +setTypeDecorator('Laminas\Db\Sql\Select', ($selectDecorator) ?: new SelectDecorator()); + $this->setTypeDecorator('Laminas\Db\Sql\Ddl\CreateTable', new Ddl\CreateTableDecorator()); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/Sqlite/SelectDecorator.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Sqlite/SelectDecorator.php new file mode 100644 index 000000000..ab6ad49c5 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Sqlite/SelectDecorator.php @@ -0,0 +1,104 @@ +subject = $select; + + return $this; + } + + /** + * {@inheritDoc} + */ + protected function localizeVariables() + { + parent::localizeVariables(); + $this->specifications[self::COMBINE] = '%1$s %2$s'; + } + + /** + * {@inheritDoc} + */ + protected function processStatementStart( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + return ''; + } + + protected function processLimit( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->limit === null && $this->offset !== null) { + return ['']; + } + if ($this->limit === null) { + return; + } + if ($parameterContainer) { + $paramPrefix = $this->processInfo['paramPrefix']; + $parameterContainer->offsetSet($paramPrefix . 'limit', $this->limit, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName('limit')]; + } + + return [$this->limit]; + } + + protected function processOffset( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->offset === null) { + return; + } + if ($parameterContainer) { + $paramPrefix = $this->processInfo['paramPrefix']; + $parameterContainer->offsetSet($paramPrefix . 'offset', $this->offset, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName('offset')]; + } + + return [$this->offset]; + } + + /** + * {@inheritDoc} + */ + protected function processStatementEnd( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + return ''; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Platform/Sqlite/Sqlite.php b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Sqlite/Sqlite.php new file mode 100644 index 000000000..b4741b97f --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Platform/Sqlite/Sqlite.php @@ -0,0 +1,24 @@ +setTypeDecorator('Laminas\Db\Sql\Select', new SelectDecorator()); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Between.php b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Between.php new file mode 100644 index 000000000..64a8138c7 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Between.php @@ -0,0 +1,146 @@ +setIdentifier($identifier); + } + if ($minValue !== null) { + $this->setMinValue($minValue); + } + if ($maxValue !== null) { + $this->setMaxValue($maxValue); + } + } + + /** + * Set identifier for comparison + * + * @param string $identifier + * @return self Provides a fluent interface + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + return $this; + } + + /** + * Get identifier of comparison + * + * @return null|string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Set minimum boundary for comparison + * + * @param int|float|string $minValue + * @return self Provides a fluent interface + */ + public function setMinValue($minValue) + { + $this->minValue = $minValue; + return $this; + } + + /** + * Get minimum boundary for comparison + * + * @return null|int|float|string + */ + public function getMinValue() + { + return $this->minValue; + } + + /** + * Set maximum boundary for comparison + * + * @param int|float|string $maxValue + * @return self Provides a fluent interface + */ + public function setMaxValue($maxValue) + { + $this->maxValue = $maxValue; + return $this; + } + + /** + * Get maximum boundary for comparison + * + * @return null|int|float|string + */ + public function getMaxValue() + { + return $this->maxValue; + } + + /** + * Set specification string to use in forming SQL predicate + * + * @param string $specification + * @return self Provides a fluent interface + */ + public function setSpecification($specification) + { + $this->specification = $specification; + return $this; + } + + /** + * Get specification string to use in forming SQL predicate + * + * @return string + */ + public function getSpecification() + { + return $this->specification; + } + + /** + * Return "where" parts + * + * @return array + */ + public function getExpressionData() + { + list($values[], $types[]) = $this->normalizeArgument($this->identifier, self::TYPE_IDENTIFIER); + list($values[], $types[]) = $this->normalizeArgument($this->minValue, self::TYPE_VALUE); + list($values[], $types[]) = $this->normalizeArgument($this->maxValue, self::TYPE_VALUE); + return [ + [ + $this->getSpecification(), + $values, + $types, + ], + ]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Expression.php b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Expression.php new file mode 100644 index 000000000..a29925e03 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Expression.php @@ -0,0 +1,29 @@ +setExpression($expression); + } + + $this->setParameters(is_array($valueParameter) ? $valueParameter : array_slice(func_get_args(), 1)); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Predicate/In.php b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/In.php new file mode 100644 index 000000000..006f87df2 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/In.php @@ -0,0 +1,143 @@ +setIdentifier($identifier); + } + if ($valueSet !== null) { + $this->setValueSet($valueSet); + } + } + + /** + * Set identifier for comparison + * + * @param string|array $identifier + * @return self Provides a fluent interface + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + + return $this; + } + + /** + * Get identifier of comparison + * + * @return null|string|array + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Set set of values for IN comparison + * + * @param array|Select $valueSet + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function setValueSet($valueSet) + { + if (! is_array($valueSet) && ! $valueSet instanceof Select) { + throw new Exception\InvalidArgumentException( + '$valueSet must be either an array or a Laminas\Db\Sql\Select object, ' . gettype($valueSet) . ' given' + ); + } + $this->valueSet = $valueSet; + + return $this; + } + + /** + * Gets set of values in IN comparison + * + * @return array|Select + */ + public function getValueSet() + { + return $this->valueSet; + } + + /** + * Return array of parts for where statement + * + * @return array + */ + public function getExpressionData() + { + $identifier = $this->getIdentifier(); + $values = $this->getValueSet(); + $replacements = []; + + if (is_array($identifier)) { + $countIdentifier = count($identifier); + $identifierSpecFragment = '(' . implode(', ', array_fill(0, $countIdentifier, '%s')) . ')'; + $types = array_fill(0, $countIdentifier, self::TYPE_IDENTIFIER); + $replacements = $identifier; + } else { + $identifierSpecFragment = '%s'; + $replacements[] = $identifier; + $types = [self::TYPE_IDENTIFIER]; + } + + if ($values instanceof Select) { + $specification = vsprintf( + $this->specification, + [$identifierSpecFragment, '%s'] + ); + $replacements[] = $values; + $types[] = self::TYPE_VALUE; + } else { + foreach ($values as $argument) { + list($replacements[], $types[]) = $this->normalizeArgument($argument, self::TYPE_VALUE); + } + $countValues = count($values); + $valuePlaceholders = $countValues > 0 ? array_fill(0, $countValues, '%s') : []; + $inValueList = implode(', ', $valuePlaceholders); + if ('' === $inValueList) { + $inValueList = 'NULL'; + } + $specification = vsprintf( + $this->specification, + [$identifierSpecFragment, '(' . $inValueList . ')'] + ); + } + + return [[ + $specification, + $replacements, + $types, + ]]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Predicate/IsNotNull.php b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/IsNotNull.php new file mode 100644 index 000000000..3004a8192 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/IsNotNull.php @@ -0,0 +1,14 @@ +setIdentifier($identifier); + } + } + + /** + * Set identifier for comparison + * + * @param string $identifier + * @return self Provides a fluent interface + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + return $this; + } + + /** + * Get identifier of comparison + * + * @return null|string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Set specification string to use in forming SQL predicate + * + * @param string $specification + * @return self Provides a fluent interface + */ + public function setSpecification($specification) + { + $this->specification = $specification; + return $this; + } + + /** + * Get specification string to use in forming SQL predicate + * + * @return string + */ + public function getSpecification() + { + return $this->specification; + } + + /** + * Get parts for where statement + * + * @return array + */ + public function getExpressionData() + { + $identifier = $this->normalizeArgument($this->identifier, self::TYPE_IDENTIFIER); + return [[ + $this->getSpecification(), + [$identifier[0]], + [$identifier[1]], + ]]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Like.php b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Like.php new file mode 100644 index 000000000..91604f789 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Like.php @@ -0,0 +1,113 @@ +setIdentifier($identifier); + } + if ($like) { + $this->setLike($like); + } + } + + /** + * @param string $identifier + * @return self Provides a fluent interface + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + return $this; + } + + /** + * @return string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * @param string $like + * @return self Provides a fluent interface + */ + public function setLike($like) + { + $this->like = $like; + return $this; + } + + /** + * @return string + */ + public function getLike() + { + return $this->like; + } + + /** + * @param string $specification + * @return self Provides a fluent interface + */ + public function setSpecification($specification) + { + $this->specification = $specification; + return $this; + } + + /** + * @return string + */ + public function getSpecification() + { + return $this->specification; + } + + /** + * @return array + */ + public function getExpressionData() + { + list($values[], $types[]) = $this->normalizeArgument($this->identifier, self::TYPE_IDENTIFIER); + list($values[], $types[]) = $this->normalizeArgument($this->like, self::TYPE_VALUE); + return [ + [ + $this->specification, + $values, + $types, + ] + ]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Literal.php b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Literal.php new file mode 100644 index 000000000..d40ed8b34 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Literal.php @@ -0,0 +1,15 @@ +'; + const OP_GT = '>'; + + const OPERATOR_GREATER_THAN_OR_EQUAL_TO = '>='; + const OP_GTE = '>='; + + /** + * {@inheritDoc} + */ + protected $allowedTypes = [ + self::TYPE_IDENTIFIER, + self::TYPE_VALUE, + ]; + + /** + * @var int|float|bool|string + */ + protected $left; + + /** + * @var int|float|bool|string + */ + protected $right; + + /** + * @var string + */ + protected $leftType = self::TYPE_IDENTIFIER; + + /** + * @var string + */ + protected $rightType = self::TYPE_VALUE; + + /** + * @var string + */ + protected $operator = self::OPERATOR_EQUAL_TO; + + /** + * Constructor + * + * @param int|float|bool|string $left + * @param string $operator + * @param int|float|bool|string $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + */ + public function __construct( + $left = null, + $operator = self::OPERATOR_EQUAL_TO, + $right = null, + $leftType = self::TYPE_IDENTIFIER, + $rightType = self::TYPE_VALUE + ) { + if ($left !== null) { + $this->setLeft($left); + } + + if ($operator !== self::OPERATOR_EQUAL_TO) { + $this->setOperator($operator); + } + + if ($right !== null) { + $this->setRight($right); + } + + if ($leftType !== self::TYPE_IDENTIFIER) { + $this->setLeftType($leftType); + } + + if ($rightType !== self::TYPE_VALUE) { + $this->setRightType($rightType); + } + } + + /** + * Set left side of operator + * + * @param int|float|bool|string $left + * + * @return self Provides a fluent interface + */ + public function setLeft($left) + { + $this->left = $left; + + if (is_array($left)) { + $left = $this->normalizeArgument($left, $this->leftType); + $this->leftType = $left[1]; + } + + return $this; + } + + /** + * Get left side of operator + * + * @return int|float|bool|string + */ + public function getLeft() + { + return $this->left; + } + + /** + * Set parameter type for left side of operator + * + * @param string $type TYPE_IDENTIFIER or TYPE_VALUE {@see allowedTypes} + * + * @return self Provides a fluent interface + * + * @throws Exception\InvalidArgumentException + */ + public function setLeftType($type) + { + if (! in_array($type, $this->allowedTypes)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid type "%s" provided; must be of type "%s" or "%s"', + $type, + __CLASS__ . '::TYPE_IDENTIFIER', + __CLASS__ . '::TYPE_VALUE' + )); + } + + $this->leftType = $type; + + return $this; + } + + /** + * Get parameter type on left side of operator + * + * @return string + */ + public function getLeftType() + { + return $this->leftType; + } + + /** + * Set operator string + * + * @param string $operator + * @return self Provides a fluent interface + */ + public function setOperator($operator) + { + $this->operator = $operator; + + return $this; + } + + /** + * Get operator string + * + * @return string + */ + public function getOperator() + { + return $this->operator; + } + + /** + * Set right side of operator + * + * @param int|float|bool|string $right + * + * @return self Provides a fluent interface + */ + public function setRight($right) + { + $this->right = $right; + + if (is_array($right)) { + $right = $this->normalizeArgument($right, $this->rightType); + $this->rightType = $right[1]; + } + + return $this; + } + + /** + * Get right side of operator + * + * @return int|float|bool|string + */ + public function getRight() + { + return $this->right; + } + + /** + * Set parameter type for right side of operator + * + * @param string $type TYPE_IDENTIFIER or TYPE_VALUE {@see allowedTypes} + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function setRightType($type) + { + if (! in_array($type, $this->allowedTypes)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid type "%s" provided; must be of type "%s" or "%s"', + $type, + __CLASS__ . '::TYPE_IDENTIFIER', + __CLASS__ . '::TYPE_VALUE' + )); + } + + $this->rightType = $type; + + return $this; + } + + /** + * Get parameter type on right side of operator + * + * @return string + */ + public function getRightType() + { + return $this->rightType; + } + + /** + * Get predicate parts for where statement + * + * @return array + */ + public function getExpressionData() + { + list($values[], $types[]) = $this->normalizeArgument($this->left, $this->leftType); + list($values[], $types[]) = $this->normalizeArgument($this->right, $this->rightType); + + return [[ + '%s ' . $this->operator . ' %s', + $values, + $types + ]]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Predicate.php b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Predicate.php new file mode 100644 index 000000000..f266a661e --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/Predicate.php @@ -0,0 +1,454 @@ +setUnnest($this); + $this->addPredicate($predicateSet, ($this->nextPredicateCombineOperator) ?: $this->defaultCombination); + $this->nextPredicateCombineOperator = null; + return $predicateSet; + } + + /** + * Indicate what predicate will be unnested + * + * @param Predicate $predicate + * @return void + */ + public function setUnnest(Predicate $predicate) + { + $this->unnest = $predicate; + } + + /** + * Indicate end of nested predicate + * + * @return Predicate + * @throws RuntimeException + */ + public function unnest() + { + if ($this->unnest === null) { + throw new RuntimeException('Not nested'); + } + $unnest = $this->unnest; + $this->unnest = null; + return $unnest; + } + + /** + * Create "Equal To" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string|Expression $left + * @param int|float|bool|string|Expression $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return self Provides a fluent interface + */ + public function equalTo($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_EQUAL_TO, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Not Equal To" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string|Expression $left + * @param int|float|bool|string|Expression $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return self Provides a fluent interface + */ + public function notEqualTo($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_NOT_EQUAL_TO, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Less Than" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string|Expression $left + * @param int|float|bool|string|Expression $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return self Provides a fluent interface + */ + public function lessThan($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_LESS_THAN, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Greater Than" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string|Expression $left + * @param int|float|bool|string|Expression $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return self Provides a fluent interface + */ + public function greaterThan($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_GREATER_THAN, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Less Than Or Equal To" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string|Expression $left + * @param int|float|bool|string|Expression $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return self Provides a fluent interface + */ + public function lessThanOrEqualTo($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_LESS_THAN_OR_EQUAL_TO, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Greater Than Or Equal To" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string|Expression $left + * @param int|float|bool|string|Expression $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return self Provides a fluent interface + */ + public function greaterThanOrEqualTo( + $left, + $right, + $leftType = self::TYPE_IDENTIFIER, + $rightType = self::TYPE_VALUE + ) { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_GREATER_THAN_OR_EQUAL_TO, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Like" predicate + * + * Utilizes Like predicate + * + * @param string|Expression $identifier + * @param string $like + * @return self Provides a fluent interface + */ + public function like($identifier, $like) + { + $this->addPredicate( + new Like($identifier, $like), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + /** + * Create "notLike" predicate + * + * Utilizes In predicate + * + * @param string|Expression $identifier + * @param string $notLike + * @return self Provides a fluent interface + */ + public function notLike($identifier, $notLike) + { + $this->addPredicate( + new NotLike($identifier, $notLike), + ($this->nextPredicateCombineOperator) ? : $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + return $this; + } + + /** + * Create an expression, with parameter placeholders + * + * @param $expression + * @param $parameters + * @return self Provides a fluent interface + */ + public function expression($expression, $parameters = null) + { + $this->addPredicate( + new Expression($expression, $parameters), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Literal" predicate + * + * Literal predicate, for parameters, use expression() + * + * @param string $literal + * @return self Provides a fluent interface + */ + public function literal($literal) + { + // process deprecated parameters from previous literal($literal, $parameters = null) signature + if (func_num_args() >= 2) { + $parameters = func_get_arg(1); + $predicate = new Expression($literal, $parameters); + } + + // normal workflow for "Literals" here + if (! isset($predicate)) { + $predicate = new Literal($literal); + } + + $this->addPredicate( + $predicate, + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "IS NULL" predicate + * + * Utilizes IsNull predicate + * + * @param string|Expression $identifier + * @return self Provides a fluent interface + */ + public function isNull($identifier) + { + $this->addPredicate( + new IsNull($identifier), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "IS NOT NULL" predicate + * + * Utilizes IsNotNull predicate + * + * @param string|Expression $identifier + * @return self Provides a fluent interface + */ + public function isNotNull($identifier) + { + $this->addPredicate( + new IsNotNull($identifier), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "IN" predicate + * + * Utilizes In predicate + * + * @param string|Expression $identifier + * @param array|\Laminas\Db\Sql\Select $valueSet + * @return self Provides a fluent interface + */ + public function in($identifier, $valueSet = null) + { + $this->addPredicate( + new In($identifier, $valueSet), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "NOT IN" predicate + * + * Utilizes NotIn predicate + * + * @param string|Expression $identifier + * @param array|\Laminas\Db\Sql\Select $valueSet + * @return self Provides a fluent interface + */ + public function notIn($identifier, $valueSet = null) + { + $this->addPredicate( + new NotIn($identifier, $valueSet), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "between" predicate + * + * Utilizes Between predicate + * + * @param string|Expression $identifier + * @param int|float|string $minValue + * @param int|float|string $maxValue + * @return self Provides a fluent interface + */ + public function between($identifier, $minValue, $maxValue) + { + $this->addPredicate( + new Between($identifier, $minValue, $maxValue), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "NOT BETWEEN" predicate + * + * Utilizes NotBetween predicate + * + * @param string|Expression $identifier + * @param int|float|string $minValue + * @param int|float|string $maxValue + * @return self Provides a fluent interface + */ + public function notBetween($identifier, $minValue, $maxValue) + { + $this->addPredicate( + new NotBetween($identifier, $minValue, $maxValue), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Use given predicate directly + * + * Contrary to {@link addPredicate()} this method respects formerly set + * AND / OR combination operator, thus allowing generic predicates to be + * used fluently within where chains as any other concrete predicate. + * + * @param PredicateInterface $predicate + * @return self Provides a fluent interface + */ + public function predicate(PredicateInterface $predicate) + { + $this->addPredicate( + $predicate, + $this->nextPredicateCombineOperator ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Overloading + * + * Overloads "or", "and", "nest", and "unnest" + * + * @param string $name + * @return self Provides a fluent interface + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'or': + $this->nextPredicateCombineOperator = self::OP_OR; + break; + case 'and': + $this->nextPredicateCombineOperator = self::OP_AND; + break; + case 'nest': + return $this->nest(); + case 'unnest': + return $this->unnest(); + } + return $this; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Predicate/PredicateInterface.php b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/PredicateInterface.php new file mode 100644 index 000000000..5777aec9d --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Predicate/PredicateInterface.php @@ -0,0 +1,15 @@ +defaultCombination = $defaultCombination; + if ($predicates) { + foreach ($predicates as $predicate) { + $this->addPredicate($predicate); + } + } + } + + /** + * Add predicate to set + * + * @param PredicateInterface $predicate + * @param string $combination + * @return self Provides a fluent interface + */ + public function addPredicate(PredicateInterface $predicate, $combination = null) + { + if ($combination === null || ! in_array($combination, [self::OP_AND, self::OP_OR])) { + $combination = $this->defaultCombination; + } + + if ($combination == self::OP_OR) { + $this->orPredicate($predicate); + return $this; + } + + $this->andPredicate($predicate); + return $this; + } + + /** + * Add predicates to set + * + * @param PredicateInterface|\Closure|string|array $predicates + * @param string $combination + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function addPredicates($predicates, $combination = self::OP_AND) + { + if ($predicates === null) { + throw new Exception\InvalidArgumentException('Predicate cannot be null'); + } + if ($predicates instanceof PredicateInterface) { + $this->addPredicate($predicates, $combination); + return $this; + } + if ($predicates instanceof \Closure) { + $predicates($this); + return $this; + } + if (is_string($predicates)) { + // String $predicate should be passed as an expression + $predicate = (strpos($predicates, Expression::PLACEHOLDER) !== false) + ? new Expression($predicates) : new Literal($predicates); + $this->addPredicate($predicate, $combination); + return $this; + } + if (is_array($predicates)) { + foreach ($predicates as $pkey => $pvalue) { + // loop through predicates + if (is_string($pkey)) { + if (strpos($pkey, '?') !== false) { + // First, process strings that the abstraction replacement character ? + // as an Expression predicate + $predicate = new Expression($pkey, $pvalue); + } elseif ($pvalue === null) { + // Otherwise, if still a string, do something intelligent with the PHP type provided + // map PHP null to SQL IS NULL expression + $predicate = new IsNull($pkey); + } elseif (is_array($pvalue)) { + // if the value is an array, assume IN() is desired + $predicate = new In($pkey, $pvalue); + } elseif ($pvalue instanceof PredicateInterface) { + throw new Exception\InvalidArgumentException( + 'Using Predicate must not use string keys' + ); + } else { + // otherwise assume that array('foo' => 'bar') means "foo" = 'bar' + $predicate = new Operator($pkey, Operator::OP_EQ, $pvalue); + } + } elseif ($pvalue instanceof PredicateInterface) { + // Predicate type is ok + $predicate = $pvalue; + } else { + // must be an array of expressions (with int-indexed array) + $predicate = (strpos($pvalue, Expression::PLACEHOLDER) !== false) + ? new Expression($pvalue) : new Literal($pvalue); + } + $this->addPredicate($predicate, $combination); + } + } + return $this; + } + + /** + * Return the predicates + * + * @return PredicateInterface[] + */ + public function getPredicates() + { + return $this->predicates; + } + + /** + * Add predicate using OR operator + * + * @param PredicateInterface $predicate + * @return self Provides a fluent interface + */ + public function orPredicate(PredicateInterface $predicate) + { + $this->predicates[] = [self::OP_OR, $predicate]; + return $this; + } + + /** + * Add predicate using AND operator + * + * @param PredicateInterface $predicate + * @return self Provides a fluent interface + */ + public function andPredicate(PredicateInterface $predicate) + { + $this->predicates[] = [self::OP_AND, $predicate]; + return $this; + } + + /** + * Get predicate parts for where statement + * + * @return array + */ + public function getExpressionData() + { + $parts = []; + for ($i = 0, $count = count($this->predicates); $i < $count; $i++) { + /** @var $predicate PredicateInterface */ + $predicate = $this->predicates[$i][1]; + + if ($predicate instanceof PredicateSet) { + $parts[] = '('; + } + + $parts = array_merge($parts, $predicate->getExpressionData()); + + if ($predicate instanceof PredicateSet) { + $parts[] = ')'; + } + + if (isset($this->predicates[$i + 1])) { + $parts[] = sprintf(' %s ', $this->predicates[$i + 1][0]); + } + } + return $parts; + } + + /** + * Get count of attached predicates + * + * @return int + */ + public function count() + { + return count($this->predicates); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/PreparableSqlInterface.php b/bundled-libs/laminas/laminas-db/src/Sql/PreparableSqlInterface.php new file mode 100644 index 000000000..3d2a2a8de --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/PreparableSqlInterface.php @@ -0,0 +1,23 @@ + '%1$s', + self::SELECT => [ + 'SELECT %1$s FROM %2$s' => [ + [1 => '%1$s', 2 => '%1$s AS %2$s', 'combinedby' => ', '], + null + ], + 'SELECT %1$s %2$s FROM %3$s' => [ + null, + [1 => '%1$s', 2 => '%1$s AS %2$s', 'combinedby' => ', '], + null + ], + 'SELECT %1$s' => [ + [1 => '%1$s', 2 => '%1$s AS %2$s', 'combinedby' => ', '], + ], + ], + self::JOINS => [ + '%1$s' => [ + [3 => '%1$s JOIN %2$s ON %3$s', 'combinedby' => ' '] + ] + ], + self::WHERE => 'WHERE %1$s', + self::GROUP => [ + 'GROUP BY %1$s' => [ + [1 => '%1$s', 'combinedby' => ', '] + ] + ], + self::HAVING => 'HAVING %1$s', + self::ORDER => [ + 'ORDER BY %1$s' => [ + [1 => '%1$s', 2 => '%1$s %2$s', 'combinedby' => ', '] + ] + ], + self::LIMIT => 'LIMIT %1$s', + self::OFFSET => 'OFFSET %1$s', + 'statementEnd' => '%1$s', + self::COMBINE => '%1$s ( %2$s )', + ]; + + /** + * @var bool + */ + protected $tableReadOnly = false; + + /** + * @var bool + */ + protected $prefixColumnsWithTable = true; + + /** + * @var string|array|TableIdentifier + */ + protected $table = null; + + /** + * @var null|string|Expression + */ + protected $quantifier = null; + + /** + * @var array + */ + protected $columns = [self::SQL_STAR]; + + /** + * @var null|Join + */ + protected $joins = null; + + /** + * @var Where + */ + protected $where = null; + + /** + * @var array + */ + protected $order = []; + + /** + * @var null|array + */ + protected $group = null; + + /** + * @var null|string|array + */ + protected $having = null; + + /** + * @var int|null + */ + protected $limit = null; + + /** + * @var int|null + */ + protected $offset = null; + + /** + * @var array + */ + protected $combine = []; + + /** + * Constructor + * + * @param null|string|array|TableIdentifier $table + */ + public function __construct($table = null) + { + if ($table) { + $this->from($table); + $this->tableReadOnly = true; + } + + $this->where = new Where; + $this->joins = new Join; + $this->having = new Having; + } + + /** + * Create from clause + * + * @param string|array|TableIdentifier $table + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function from($table) + { + if ($this->tableReadOnly) { + throw new Exception\InvalidArgumentException( + 'Since this object was created with a table and/or schema in the constructor, it is read only.' + ); + } + + if (! is_string($table) && ! is_array($table) && ! $table instanceof TableIdentifier) { + throw new Exception\InvalidArgumentException( + '$table must be a string, array, or an instance of TableIdentifier' + ); + } + + if (is_array($table) && (! is_string(key($table)) || count($table) !== 1)) { + throw new Exception\InvalidArgumentException( + 'from() expects $table as an array is a single element associative array' + ); + } + + $this->table = $table; + return $this; + } + + /** + * @param string|Expression $quantifier DISTINCT|ALL + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function quantifier($quantifier) + { + if (! is_string($quantifier) && ! $quantifier instanceof ExpressionInterface) { + throw new Exception\InvalidArgumentException( + 'Quantifier must be one of DISTINCT, ALL, or some platform specific object implementing ' + . 'ExpressionInterface' + ); + } + $this->quantifier = $quantifier; + return $this; + } + + /** + * Specify columns from which to select + * + * Possible valid states: + * + * array(*) + * + * array(value, ...) + * value can be strings or Expression objects + * + * array(string => value, ...) + * key string will be use as alias, + * value can be string or Expression objects + * + * @param array $columns + * @param bool $prefixColumnsWithTable + * @return self Provides a fluent interface + */ + public function columns(array $columns, $prefixColumnsWithTable = true) + { + $this->columns = $columns; + $this->prefixColumnsWithTable = (bool) $prefixColumnsWithTable; + return $this; + } + + /** + * Create join clause + * + * @param string|array|TableIdentifier $name + * @param string|Predicate\Expression $on + * @param string|array $columns + * @param string $type one of the JOIN_* constants + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function join($name, $on, $columns = self::SQL_STAR, $type = self::JOIN_INNER) + { + $this->joins->join($name, $on, $columns, $type); + + return $this; + } + + /** + * Create where clause + * + * @param Where|\Closure|string|array|Predicate\PredicateInterface $predicate + * @param string $combination One of the OP_* constants from Predicate\PredicateSet + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function where($predicate, $combination = Predicate\PredicateSet::OP_AND) + { + if ($predicate instanceof Where) { + $this->where = $predicate; + } else { + $this->where->addPredicates($predicate, $combination); + } + return $this; + } + + /** + * @param mixed $group + * @return self Provides a fluent interface + */ + public function group($group) + { + if (is_array($group)) { + foreach ($group as $o) { + $this->group[] = $o; + } + } else { + $this->group[] = $group; + } + return $this; + } + + /** + * Create having clause + * + * @param Where|\Closure|string|array $predicate + * @param string $combination One of the OP_* constants from Predicate\PredicateSet + * @return self Provides a fluent interface + */ + public function having($predicate, $combination = Predicate\PredicateSet::OP_AND) + { + if ($predicate instanceof Having) { + $this->having = $predicate; + } else { + $this->having->addPredicates($predicate, $combination); + } + return $this; + } + + /** + * @param string|array|Expression $order + * @return self Provides a fluent interface + */ + public function order($order) + { + if (is_string($order)) { + if (strpos($order, ',') !== false) { + $order = preg_split('#,\s+#', $order); + } else { + $order = (array) $order; + } + } elseif (! is_array($order)) { + $order = [$order]; + } + foreach ($order as $k => $v) { + if (is_string($k)) { + $this->order[$k] = $v; + } else { + $this->order[] = $v; + } + } + return $this; + } + + /** + * @param int $limit + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function limit($limit) + { + if (! is_numeric($limit)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects parameter to be numeric, "%s" given', + __METHOD__, + (is_object($limit) ? get_class($limit) : gettype($limit)) + )); + } + + $this->limit = $limit; + return $this; + } + + /** + * @param int $offset + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function offset($offset) + { + if (! is_numeric($offset)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects parameter to be numeric, "%s" given', + __METHOD__, + (is_object($offset) ? get_class($offset) : gettype($offset)) + )); + } + + $this->offset = $offset; + return $this; + } + + /** + * @param Select $select + * @param string $type + * @param string $modifier + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function combine(Select $select, $type = self::COMBINE_UNION, $modifier = '') + { + if ($this->combine !== []) { + throw new Exception\InvalidArgumentException( + 'This Select object is already combined and cannot be combined with multiple Selects objects' + ); + } + $this->combine = [ + 'select' => $select, + 'type' => $type, + 'modifier' => $modifier + ]; + return $this; + } + + /** + * @param string $part + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function reset($part) + { + switch ($part) { + case self::TABLE: + if ($this->tableReadOnly) { + throw new Exception\InvalidArgumentException( + 'Since this object was created with a table and/or schema in the constructor, it is read only.' + ); + } + $this->table = null; + break; + case self::QUANTIFIER: + $this->quantifier = null; + break; + case self::COLUMNS: + $this->columns = []; + break; + case self::JOINS: + $this->joins = new Join; + break; + case self::WHERE: + $this->where = new Where; + break; + case self::GROUP: + $this->group = null; + break; + case self::HAVING: + $this->having = new Having; + break; + case self::LIMIT: + $this->limit = null; + break; + case self::OFFSET: + $this->offset = null; + break; + case self::ORDER: + $this->order = []; + break; + case self::COMBINE: + $this->combine = []; + break; + } + return $this; + } + + /** + * @param $index + * @param $specification + * @return self Provides a fluent interface + */ + public function setSpecification($index, $specification) + { + if (! method_exists($this, 'process' . $index)) { + throw new Exception\InvalidArgumentException('Not a valid specification name.'); + } + $this->specifications[$index] = $specification; + return $this; + } + + public function getRawState($key = null) + { + $rawState = [ + self::TABLE => $this->table, + self::QUANTIFIER => $this->quantifier, + self::COLUMNS => $this->columns, + self::JOINS => $this->joins, + self::WHERE => $this->where, + self::ORDER => $this->order, + self::GROUP => $this->group, + self::HAVING => $this->having, + self::LIMIT => $this->limit, + self::OFFSET => $this->offset, + self::COMBINE => $this->combine + ]; + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + /** + * Returns whether the table is read only or not. + * + * @return bool + */ + public function isTableReadOnly() + { + return $this->tableReadOnly; + } + + protected function processStatementStart( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->combine !== []) { + return ['(']; + } + } + + protected function processStatementEnd( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->combine !== []) { + return [')']; + } + } + + /** + * Process the select part + * + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @return null|array + */ + protected function processSelect( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + $expr = 1; + + list($table, $fromTable) = $this->resolveTable($this->table, $platform, $driver, $parameterContainer); + // process table columns + $columns = []; + foreach ($this->columns as $columnIndexOrAs => $column) { + if ($column === self::SQL_STAR) { + $columns[] = [$fromTable . self::SQL_STAR]; + continue; + } + + $columnName = $this->resolveColumnValue( + [ + 'column' => $column, + 'fromTable' => $fromTable, + 'isIdentifier' => true, + ], + $platform, + $driver, + $parameterContainer, + (is_string($columnIndexOrAs) ? $columnIndexOrAs : 'column') + ); + // process As portion + if (is_string($columnIndexOrAs)) { + $columnAs = $platform->quoteIdentifier($columnIndexOrAs); + } elseif (stripos($columnName, ' as ') === false) { + $columnAs = (is_string($column)) ? $platform->quoteIdentifier($column) : 'Expression' . $expr++; + } + $columns[] = (isset($columnAs)) ? [$columnName, $columnAs] : [$columnName]; + } + + // process join columns + foreach ($this->joins->getJoins() as $join) { + $joinName = (is_array($join['name'])) ? key($join['name']) : $join['name']; + $joinName = parent::resolveTable($joinName, $platform, $driver, $parameterContainer); + + foreach ($join['columns'] as $jKey => $jColumn) { + $jColumns = []; + $jFromTable = is_scalar($jColumn) + ? $joinName . $platform->getIdentifierSeparator() + : ''; + $jColumns[] = $this->resolveColumnValue( + [ + 'column' => $jColumn, + 'fromTable' => $jFromTable, + 'isIdentifier' => true, + ], + $platform, + $driver, + $parameterContainer, + (is_string($jKey) ? $jKey : 'column') + ); + if (is_string($jKey)) { + $jColumns[] = $platform->quoteIdentifier($jKey); + } elseif ($jColumn !== self::SQL_STAR) { + $jColumns[] = $platform->quoteIdentifier($jColumn); + } + $columns[] = $jColumns; + } + } + + if ($this->quantifier) { + $quantifier = ($this->quantifier instanceof ExpressionInterface) + ? $this->processExpression($this->quantifier, $platform, $driver, $parameterContainer, 'quantifier') + : $this->quantifier; + } + + if (! isset($table)) { + return [$columns]; + } elseif (isset($quantifier)) { + return [$quantifier, $columns, $table]; + } else { + return [$columns, $table]; + } + } + + protected function processJoins( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + return $this->processJoin($this->joins, $platform, $driver, $parameterContainer); + } + + protected function processWhere( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->where->count() == 0) { + return; + } + return [ + $this->processExpression($this->where, $platform, $driver, $parameterContainer, 'where') + ]; + } + + protected function processGroup( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->group === null) { + return; + } + // process table columns + $groups = []; + foreach ($this->group as $column) { + $groups[] = $this->resolveColumnValue( + [ + 'column' => $column, + 'isIdentifier' => true, + ], + $platform, + $driver, + $parameterContainer, + 'group' + ); + } + return [$groups]; + } + + protected function processHaving( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->having->count() == 0) { + return; + } + return [ + $this->processExpression($this->having, $platform, $driver, $parameterContainer, 'having') + ]; + } + + protected function processOrder( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if (empty($this->order)) { + return; + } + $orders = []; + foreach ($this->order as $k => $v) { + if ($v instanceof ExpressionInterface) { + $orders[] = [ + $this->processExpression($v, $platform, $driver, $parameterContainer) + ]; + continue; + } + if (is_int($k)) { + if (strpos($v, ' ') !== false) { + list($k, $v) = preg_split('# #', $v, 2); + } else { + $k = $v; + $v = self::ORDER_ASCENDING; + } + } + if (strcasecmp(trim($v), self::ORDER_DESCENDING) === 0) { + $orders[] = [$platform->quoteIdentifierInFragment($k), self::ORDER_DESCENDING]; + } else { + $orders[] = [$platform->quoteIdentifierInFragment($k), self::ORDER_ASCENDING]; + } + } + return [$orders]; + } + + protected function processLimit( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->limit === null) { + return; + } + if ($parameterContainer) { + $paramPrefix = $this->processInfo['paramPrefix']; + $parameterContainer->offsetSet($paramPrefix . 'limit', $this->limit, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName($paramPrefix . 'limit')]; + } + return [$platform->quoteValue($this->limit)]; + } + + protected function processOffset( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->offset === null) { + return; + } + if ($parameterContainer) { + $paramPrefix = $this->processInfo['paramPrefix']; + $parameterContainer->offsetSet($paramPrefix . 'offset', $this->offset, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName($paramPrefix . 'offset')]; + } + + return [$platform->quoteValue($this->offset)]; + } + + protected function processCombine( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->combine == []) { + return; + } + + $type = $this->combine['type']; + if ($this->combine['modifier']) { + $type .= ' ' . $this->combine['modifier']; + } + + return [ + strtoupper($type), + $this->processSubSelect($this->combine['select'], $platform, $driver, $parameterContainer), + ]; + } + + /** + * Variable overloading + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'where': + return $this->where; + case 'having': + return $this->having; + case 'joins': + return $this->joins; + default: + throw new Exception\InvalidArgumentException('Not a valid magic property for this object'); + } + } + + /** + * __clone + * + * Resets the where object each time the Select is cloned. + * + * @return void + */ + public function __clone() + { + $this->where = clone $this->where; + $this->joins = clone $this->joins; + $this->having = clone $this->having; + } + + /** + * @param string|TableIdentifier|Select $table + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @return string + */ + protected function resolveTable( + $table, + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + $alias = null; + + if (is_array($table)) { + $alias = key($table); + $table = current($table); + } + + $table = parent::resolveTable($table, $platform, $driver, $parameterContainer); + + if ($alias) { + $fromTable = $platform->quoteIdentifier($alias); + $table = $this->renderTable($table, $fromTable); + } else { + $fromTable = $table; + } + + if ($this->prefixColumnsWithTable && $fromTable) { + $fromTable .= $platform->getIdentifierSeparator(); + } else { + $fromTable = ''; + } + + return [ + $table, + $fromTable + ]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Sql.php b/bundled-libs/laminas/laminas-db/src/Sql/Sql.php new file mode 100644 index 000000000..250135301 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Sql.php @@ -0,0 +1,173 @@ +adapter = $adapter; + if ($table) { + $this->setTable($table); + } + $this->sqlPlatform = $sqlPlatform ?: new Platform\Platform($adapter); + } + + /** + * @return null|\Laminas\Db\Adapter\AdapterInterface + */ + public function getAdapter() + { + return $this->adapter; + } + + public function hasTable() + { + return ($this->table !== null); + } + + /** + * @param string|array|TableIdentifier $table + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function setTable($table) + { + if (is_string($table) || is_array($table) || $table instanceof TableIdentifier) { + $this->table = $table; + } else { + throw new Exception\InvalidArgumentException( + 'Table must be a string, array or instance of TableIdentifier.' + ); + } + return $this; + } + + public function getTable() + { + return $this->table; + } + + public function getSqlPlatform() + { + return $this->sqlPlatform; + } + + public function select($table = null) + { + if ($this->table !== null && $table !== null) { + throw new Exception\InvalidArgumentException(sprintf( + 'This Sql object is intended to work with only the table "%s" provided at construction time.', + $this->table + )); + } + return new Select(($table) ?: $this->table); + } + + public function insert($table = null) + { + if ($this->table !== null && $table !== null) { + throw new Exception\InvalidArgumentException(sprintf( + 'This Sql object is intended to work with only the table "%s" provided at construction time.', + $this->table + )); + } + return new Insert(($table) ?: $this->table); + } + + public function update($table = null) + { + if ($this->table !== null && $table !== null) { + throw new Exception\InvalidArgumentException(sprintf( + 'This Sql object is intended to work with only the table "%s" provided at construction time.', + $this->table + )); + } + return new Update(($table) ?: $this->table); + } + + public function delete($table = null) + { + if ($this->table !== null && $table !== null) { + throw new Exception\InvalidArgumentException(sprintf( + 'This Sql object is intended to work with only the table "%s" provided at construction time.', + $this->table + )); + } + return new Delete(($table) ?: $this->table); + } + + /** + * @param PreparableSqlInterface $sqlObject + * @param StatementInterface $statement + * @param AdapterInterface $adapter + * + * @return StatementInterface + */ + public function prepareStatementForSqlObject( + PreparableSqlInterface $sqlObject, + StatementInterface $statement = null, + AdapterInterface $adapter = null + ) { + $adapter = $adapter ?: $this->adapter; + $statement = $statement ?: $adapter->getDriver()->createStatement(); + + return $this->sqlPlatform->setSubject($sqlObject)->prepareStatement($adapter, $statement); + } + + /** + * Get sql string using platform or sql object + * + * @param SqlInterface $sqlObject + * @param PlatformInterface|null $platform + * + * @return string + * + * @deprecated Deprecated in 2.4. Use buildSqlString() instead + */ + public function getSqlStringForSqlObject(SqlInterface $sqlObject, PlatformInterface $platform = null) + { + $platform = ($platform) ?: $this->adapter->getPlatform(); + return $this->sqlPlatform->setSubject($sqlObject)->getSqlString($platform); + } + + /** + * @param SqlInterface $sqlObject + * @param AdapterInterface $adapter + * + * @return string + * + * @throws Exception\InvalidArgumentException + */ + public function buildSqlString(SqlInterface $sqlObject, AdapterInterface $adapter = null) + { + return $this + ->sqlPlatform + ->setSubject($sqlObject) + ->getSqlString($adapter ? $adapter->getPlatform() : $this->adapter->getPlatform()); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/SqlInterface.php b/bundled-libs/laminas/laminas-db/src/Sql/SqlInterface.php new file mode 100644 index 000000000..b45edb304 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/SqlInterface.php @@ -0,0 +1,23 @@ +table = (string) $table; + + if ('' === $this->table) { + throw new Exception\InvalidArgumentException('$table must be a valid table name, empty string given'); + } + + if (null === $schema) { + $this->schema = null; + } else { + if (! (is_string($schema) || is_callable([$schema, '__toString']))) { + throw new Exception\InvalidArgumentException(sprintf( + '$schema must be a valid schema name, parameter of type %s given', + is_object($schema) ? get_class($schema) : gettype($schema) + )); + } + + $this->schema = (string) $schema; + + if ('' === $this->schema) { + throw new Exception\InvalidArgumentException( + '$schema must be a valid schema name or null, empty string given' + ); + } + } + } + + /** + * @param string $table + * + * @deprecated please use the constructor and build a new {@see TableIdentifier} instead + */ + public function setTable($table) + { + $this->table = $table; + } + + /** + * @return string + */ + public function getTable() + { + return $this->table; + } + + /** + * @return bool + */ + public function hasSchema() + { + return ($this->schema !== null); + } + + /** + * @param $schema + * + * @deprecated please use the constructor and build a new {@see TableIdentifier} instead + */ + public function setSchema($schema) + { + $this->schema = $schema; + } + + /** + * @return null|string + */ + public function getSchema() + { + return $this->schema; + } + + public function getTableAndSchema() + { + return [$this->table, $this->schema]; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Update.php b/bundled-libs/laminas/laminas-db/src/Sql/Update.php new file mode 100644 index 000000000..3ad1d9b54 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Update.php @@ -0,0 +1,272 @@ + 'UPDATE %1$s', + self::SPECIFICATION_JOIN => [ + '%1$s' => [ + [3 => '%1$s JOIN %2$s ON %3$s', 'combinedby' => ' '] + ] + ], + self::SPECIFICATION_SET => 'SET %1$s', + self::SPECIFICATION_WHERE => 'WHERE %1$s', + ]; + + /** + * @var string|TableIdentifier + */ + protected $table = ''; + + /** + * @var bool + */ + protected $emptyWhereProtection = true; + + /** + * @var PriorityList + */ + protected $set; + + /** + * @var string|Where + */ + protected $where = null; + + /** + * @var null|Join + */ + protected $joins = null; + + /** + * Constructor + * + * @param null|string|TableIdentifier $table + */ + public function __construct($table = null) + { + if ($table) { + $this->table($table); + } + $this->where = new Where(); + $this->joins = new Join(); + $this->set = new PriorityList(); + $this->set->isLIFO(false); + } + + /** + * Specify table for statement + * + * @param string|TableIdentifier $table + * @return self Provides a fluent interface + */ + public function table($table) + { + $this->table = $table; + return $this; + } + + /** + * Set key/value pairs to update + * + * @param array $values Associative array of key values + * @param string $flag One of the VALUES_* constants + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function set(array $values, $flag = self::VALUES_SET) + { + if ($flag == self::VALUES_SET) { + $this->set->clear(); + } + $priority = is_numeric($flag) ? $flag : 0; + foreach ($values as $k => $v) { + if (! is_string($k)) { + throw new Exception\InvalidArgumentException('set() expects a string for the value key'); + } + $this->set->insert($k, $v, $priority); + } + return $this; + } + + /** + * Create where clause + * + * @param Where|\Closure|string|array $predicate + * @param string $combination One of the OP_* constants from Predicate\PredicateSet + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function where($predicate, $combination = Predicate\PredicateSet::OP_AND) + { + if ($predicate instanceof Where) { + $this->where = $predicate; + } else { + $this->where->addPredicates($predicate, $combination); + } + return $this; + } + + /** + * Create join clause + * + * @param string|array $name + * @param string $on + * @param string $type one of the JOIN_* constants + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException + */ + public function join($name, $on, $type = Join::JOIN_INNER) + { + $this->joins->join($name, $on, [], $type); + + return $this; + } + + public function getRawState($key = null) + { + $rawState = [ + 'emptyWhereProtection' => $this->emptyWhereProtection, + 'table' => $this->table, + 'set' => $this->set->toArray(), + 'where' => $this->where, + 'joins' => $this->joins + ]; + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + protected function processUpdate( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + return sprintf( + $this->specifications[static::SPECIFICATION_UPDATE], + $this->resolveTable($this->table, $platform, $driver, $parameterContainer) + ); + } + + protected function processSet( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + $setSql = []; + $i = 0; + foreach ($this->set as $column => $value) { + $prefix = $this->resolveColumnValue( + [ + 'column' => $column, + 'fromTable' => '', + 'isIdentifier' => true, + ], + $platform, + $driver, + $parameterContainer, + 'column' + ); + $prefix .= ' = '; + if (is_scalar($value) && $parameterContainer) { + // use incremental value instead of column name for PDO + // @see https://github.com/zendframework/zend-db/issues/35 + if ($driver instanceof Pdo) { + $column = 'c_' . $i++; + } + $setSql[] = $prefix . $driver->formatParameterName($column); + $parameterContainer->offsetSet($column, $value); + } else { + $setSql[] = $prefix . $this->resolveColumnValue( + $value, + $platform, + $driver, + $parameterContainer + ); + } + } + + return sprintf( + $this->specifications[static::SPECIFICATION_SET], + implode(', ', $setSql) + ); + } + + protected function processWhere( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + if ($this->where->count() == 0) { + return; + } + return sprintf( + $this->specifications[static::SPECIFICATION_WHERE], + $this->processExpression($this->where, $platform, $driver, $parameterContainer, 'where') + ); + } + + protected function processJoins( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + return $this->processJoin($this->joins, $platform, $driver, $parameterContainer); + } + + /** + * Variable overloading + * + * Proxies to "where" only + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (strtolower($name) == 'where') { + return $this->where; + } + } + + /** + * __clone + * + * Resets the where object each time the Update is cloned. + * + * @return void + */ + public function __clone() + { + $this->where = clone $this->where; + $this->set = clone $this->set; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/Sql/Where.php b/bundled-libs/laminas/laminas-db/src/Sql/Where.php new file mode 100644 index 000000000..634bc279e --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/Sql/Where.php @@ -0,0 +1,13 @@ +isInitialized; + } + + /** + * Initialize + * + * @throws Exception\RuntimeException + * @return null + */ + public function initialize() + { + if ($this->isInitialized) { + return; + } + + if (! $this->featureSet instanceof Feature\FeatureSet) { + $this->featureSet = new Feature\FeatureSet; + } + + $this->featureSet->setTableGateway($this); + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_PRE_INITIALIZE, []); + + if (! $this->adapter instanceof AdapterInterface) { + throw new Exception\RuntimeException('This table does not have an Adapter setup'); + } + + if (! is_string($this->table) && ! $this->table instanceof TableIdentifier && ! is_array($this->table)) { + throw new Exception\RuntimeException('This table object does not have a valid table set.'); + } + + if (! $this->resultSetPrototype instanceof ResultSetInterface) { + $this->resultSetPrototype = new ResultSet; + } + + if (! $this->sql instanceof Sql) { + $this->sql = new Sql($this->adapter, $this->table); + } + + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_POST_INITIALIZE, []); + + $this->isInitialized = true; + } + + /** + * Get table name + * + * @return string + */ + public function getTable() + { + return $this->table; + } + + /** + * Get adapter + * + * @return AdapterInterface + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * @return Feature\FeatureSet + */ + public function getFeatureSet() + { + return $this->featureSet; + } + + /** + * Get select result prototype + * + * @return ResultSetInterface + */ + public function getResultSetPrototype() + { + return $this->resultSetPrototype; + } + + /** + * @return Sql + */ + public function getSql() + { + return $this->sql; + } + + /** + * Select + * + * @param Where|\Closure|string|array $where + * @return ResultSetInterface + */ + public function select($where = null) + { + if (! $this->isInitialized) { + $this->initialize(); + } + + $select = $this->sql->select(); + + if ($where instanceof \Closure) { + $where($select); + } elseif ($where !== null) { + $select->where($where); + } + + return $this->selectWith($select); + } + + /** + * @param Select $select + * @return ResultSetInterface + */ + public function selectWith(Select $select) + { + if (! $this->isInitialized) { + $this->initialize(); + } + return $this->executeSelect($select); + } + + /** + * @param Select $select + * @return ResultSetInterface + * @throws Exception\RuntimeException + */ + protected function executeSelect(Select $select) + { + $selectState = $select->getRawState(); + if (isset($selectState['table']) + && $selectState['table'] != $this->table + && (is_array($selectState['table']) + && end($selectState['table']) != $this->table) + ) { + throw new Exception\RuntimeException( + 'The table name of the provided Select object must match that of the table' + ); + } + + if (isset($selectState['columns']) + && $selectState['columns'] == [Select::SQL_STAR] + && $this->columns !== []) { + $select->columns($this->columns); + } + + // apply preSelect features + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_PRE_SELECT, [$select]); + + // prepare and execute + $statement = $this->sql->prepareStatementForSqlObject($select); + $result = $statement->execute(); + + // build result set + $resultSet = clone $this->resultSetPrototype; + $resultSet->initialize($result); + + // apply postSelect features + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_POST_SELECT, [$statement, $result, $resultSet]); + + return $resultSet; + } + + /** + * Insert + * + * @param array $set + * @return int + */ + public function insert($set) + { + if (! $this->isInitialized) { + $this->initialize(); + } + $insert = $this->sql->insert(); + $insert->values($set); + return $this->executeInsert($insert); + } + + /** + * @param Insert $insert + * @return int + */ + public function insertWith(Insert $insert) + { + if (! $this->isInitialized) { + $this->initialize(); + } + return $this->executeInsert($insert); + } + + /** + * @todo add $columns support + * + * @param Insert $insert + * @return int + * @throws Exception\RuntimeException + */ + protected function executeInsert(Insert $insert) + { + $insertState = $insert->getRawState(); + if ($insertState['table'] != $this->table) { + throw new Exception\RuntimeException( + 'The table name of the provided Insert object must match that of the table' + ); + } + + // apply preInsert features + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_PRE_INSERT, [$insert]); + + // Most RDBMS solutions do not allow using table aliases in INSERTs + // See https://github.com/zendframework/zf2/issues/7311 + $unaliasedTable = false; + if (is_array($insertState['table'])) { + $tableData = array_values($insertState['table']); + $unaliasedTable = array_shift($tableData); + $insert->into($unaliasedTable); + } + + $statement = $this->sql->prepareStatementForSqlObject($insert); + $result = $statement->execute(); + $this->lastInsertValue = $this->adapter->getDriver()->getConnection()->getLastGeneratedValue(); + + // apply postInsert features + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_POST_INSERT, [$statement, $result]); + + // Reset original table information in Insert instance, if necessary + if ($unaliasedTable) { + $insert->into($insertState['table']); + } + + return $result->getAffectedRows(); + } + + /** + * Update + * + * @param array $set + * @param string|array|\Closure $where + * @param null|array $joins + * @return int + */ + public function update($set, $where = null, array $joins = null) + { + if (! $this->isInitialized) { + $this->initialize(); + } + $sql = $this->sql; + $update = $sql->update(); + $update->set($set); + if ($where !== null) { + $update->where($where); + } + + if ($joins) { + foreach ($joins as $join) { + $type = isset($join['type']) ? $join['type'] : Join::JOIN_INNER; + $update->join($join['name'], $join['on'], $type); + } + } + + return $this->executeUpdate($update); + } + + /** + * @param \Laminas\Db\Sql\Update $update + * @return int + */ + public function updateWith(Update $update) + { + if (! $this->isInitialized) { + $this->initialize(); + } + return $this->executeUpdate($update); + } + + /** + * @todo add $columns support + * + * @param Update $update + * @return int + * @throws Exception\RuntimeException + */ + protected function executeUpdate(Update $update) + { + $updateState = $update->getRawState(); + if ($updateState['table'] != $this->table) { + throw new Exception\RuntimeException( + 'The table name of the provided Update object must match that of the table' + ); + } + + // apply preUpdate features + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_PRE_UPDATE, [$update]); + + $unaliasedTable = false; + if (is_array($updateState['table'])) { + $tableData = array_values($updateState['table']); + $unaliasedTable = array_shift($tableData); + $update->table($unaliasedTable); + } + + $statement = $this->sql->prepareStatementForSqlObject($update); + $result = $statement->execute(); + + // apply postUpdate features + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_POST_UPDATE, [$statement, $result]); + + // Reset original table information in Update instance, if necessary + if ($unaliasedTable) { + $update->table($updateState['table']); + } + + return $result->getAffectedRows(); + } + + /** + * Delete + * + * @param Where|\Closure|string|array $where + * @return int + */ + public function delete($where) + { + if (! $this->isInitialized) { + $this->initialize(); + } + $delete = $this->sql->delete(); + if ($where instanceof \Closure) { + $where($delete); + } else { + $delete->where($where); + } + return $this->executeDelete($delete); + } + + /** + * @param Delete $delete + * @return int + */ + public function deleteWith(Delete $delete) + { + $this->initialize(); + return $this->executeDelete($delete); + } + + /** + * @todo add $columns support + * + * @param Delete $delete + * @return int + * @throws Exception\RuntimeException + */ + protected function executeDelete(Delete $delete) + { + $deleteState = $delete->getRawState(); + if ($deleteState['table'] != $this->table) { + throw new Exception\RuntimeException( + 'The table name of the provided Delete object must match that of the table' + ); + } + + // pre delete update + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_PRE_DELETE, [$delete]); + + $unaliasedTable = false; + if (is_array($deleteState['table'])) { + $tableData = array_values($deleteState['table']); + $unaliasedTable = array_shift($tableData); + $delete->from($unaliasedTable); + } + + $statement = $this->sql->prepareStatementForSqlObject($delete); + $result = $statement->execute(); + + // apply postDelete features + $this->featureSet->apply(EventFeatureEventsInterface::EVENT_POST_DELETE, [$statement, $result]); + + // Reset original table information in Delete instance, if necessary + if ($unaliasedTable) { + $delete->from($deleteState['table']); + } + + return $result->getAffectedRows(); + } + + /** + * Get last insert value + * + * @return int + */ + public function getLastInsertValue() + { + return $this->lastInsertValue; + } + + /** + * __get + * + * @param string $property + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function __get($property) + { + switch (strtolower($property)) { + case 'lastinsertvalue': + return $this->lastInsertValue; + case 'adapter': + return $this->adapter; + case 'table': + return $this->table; + } + if ($this->featureSet->canCallMagicGet($property)) { + return $this->featureSet->callMagicGet($property); + } + throw new Exception\InvalidArgumentException('Invalid magic property access in ' . __CLASS__ . '::__get()'); + } + + /** + * @param string $property + * @param mixed $value + * @return mixed + * @throws Exception\InvalidArgumentException + */ + public function __set($property, $value) + { + if ($this->featureSet->canCallMagicSet($property)) { + return $this->featureSet->callMagicSet($property, $value); + } + throw new Exception\InvalidArgumentException('Invalid magic property access in ' . __CLASS__ . '::__set()'); + } + + /** + * @param $method + * @param $arguments + * @return mixed + * @throws Exception\InvalidArgumentException + */ + public function __call($method, $arguments) + { + if ($this->featureSet->canCallMagicCall($method)) { + return $this->featureSet->callMagicCall($method, $arguments); + } + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid method (%s) called, caught by %s::__call()', + $method, + __CLASS__ + )); + } + + /** + * __clone + */ + public function __clone() + { + $this->resultSetPrototype = (isset($this->resultSetPrototype)) ? clone $this->resultSetPrototype : null; + $this->sql = clone $this->sql; + if (is_object($this->table)) { + $this->table = clone $this->table; + } elseif (is_array($this->table) + && count($this->table) == 1 + && is_object(reset($this->table)) + ) { + foreach ($this->table as $alias => &$tableObject) { + $tableObject = clone $tableObject; + } + } + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/Exception/ExceptionInterface.php b/bundled-libs/laminas/laminas-db/src/TableGateway/Exception/ExceptionInterface.php new file mode 100644 index 000000000..dd4861e2e --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/Exception/ExceptionInterface.php @@ -0,0 +1,15 @@ +tableGateway = $tableGateway; + } + + public function initialize() + { + throw new Exception\RuntimeException('This method is not intended to be called on this object.'); + } + + public function getMagicMethodSpecifications() + { + return []; + } + + + /* + public function preInitialize(); + public function postInitialize(); + public function preSelect(Select $select); + public function postSelect(StatementInterface $statement, ResultInterface $result, ResultSetInterface $resultSet); + public function preInsert(Insert $insert); + public function postInsert(StatementInterface $statement, ResultInterface $result); + public function preUpdate(Update $update); + public function postUpdate(StatementInterface $statement, ResultInterface $result); + public function preDelete(Delete $delete); + public function postDelete(StatementInterface $statement, ResultInterface $result); + */ +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/EventFeature.php b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/EventFeature.php new file mode 100644 index 000000000..b52ce42a9 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/EventFeature.php @@ -0,0 +1,256 @@ +eventManager = ($eventManager instanceof EventManagerInterface) + ? $eventManager + : new EventManager; + + $this->eventManager->addIdentifiers([ + 'Laminas\Db\TableGateway\TableGateway', + ]); + + $this->event = ($tableGatewayEvent) ?: new EventFeature\TableGatewayEvent(); + } + + /** + * Retrieve composed event manager instance + * + * @return EventManagerInterface + */ + public function getEventManager() + { + return $this->eventManager; + } + + /** + * Retrieve composed event instance + * + * @return EventFeature\TableGatewayEvent + */ + public function getEvent() + { + return $this->event; + } + + /** + * Initialize feature and trigger "preInitialize" event + * + * Ensures that the composed TableGateway has identifiers based on the + * class name, and that the event target is set to the TableGateway + * instance. It then triggers the "preInitialize" event. + * + * @return void + */ + public function preInitialize() + { + if (get_class($this->tableGateway) != 'Laminas\Db\TableGateway\TableGateway') { + $this->eventManager->addIdentifiers([get_class($this->tableGateway)]); + } + + $this->event->setTarget($this->tableGateway); + $this->event->setName(static::EVENT_PRE_INITIALIZE); + $this->eventManager->triggerEvent($this->event); + } + + /** + * Trigger the "postInitialize" event + * + * @return void + */ + public function postInitialize() + { + $this->event->setName(static::EVENT_POST_INITIALIZE); + $this->eventManager->triggerEvent($this->event); + } + + /** + * Trigger the "preSelect" event + * + * Triggers the "preSelect" event mapping the following parameters: + * - $select as "select" + * + * @param Select $select + * @return void + */ + public function preSelect(Select $select) + { + $this->event->setName(static::EVENT_PRE_SELECT); + $this->event->setParams(['select' => $select]); + $this->eventManager->triggerEvent($this->event); + } + + /** + * Trigger the "postSelect" event + * + * Triggers the "postSelect" event mapping the following parameters: + * - $statement as "statement" + * - $result as "result" + * - $resultSet as "result_set" + * + * @param StatementInterface $statement + * @param ResultInterface $result + * @param ResultSetInterface $resultSet + * @return void + */ + public function postSelect(StatementInterface $statement, ResultInterface $result, ResultSetInterface $resultSet) + { + $this->event->setName(static::EVENT_POST_SELECT); + $this->event->setParams([ + 'statement' => $statement, + 'result' => $result, + 'result_set' => $resultSet + ]); + $this->eventManager->triggerEvent($this->event); + } + + /** + * Trigger the "preInsert" event + * + * Triggers the "preInsert" event mapping the following parameters: + * - $insert as "insert" + * + * @param Insert $insert + * @return void + */ + public function preInsert(Insert $insert) + { + $this->event->setName(static::EVENT_PRE_INSERT); + $this->event->setParams(['insert' => $insert]); + $this->eventManager->triggerEvent($this->event); + } + + /** + * Trigger the "postInsert" event + * + * Triggers the "postInsert" event mapping the following parameters: + * - $statement as "statement" + * - $result as "result" + * + * @param StatementInterface $statement + * @param ResultInterface $result + * @return void + */ + public function postInsert(StatementInterface $statement, ResultInterface $result) + { + $this->event->setName(static::EVENT_POST_INSERT); + $this->event->setParams([ + 'statement' => $statement, + 'result' => $result, + ]); + $this->eventManager->triggerEvent($this->event); + } + + /** + * Trigger the "preUpdate" event + * + * Triggers the "preUpdate" event mapping the following parameters: + * - $update as "update" + * + * @param Update $update + * @return void + */ + public function preUpdate(Update $update) + { + $this->event->setName(static::EVENT_PRE_UPDATE); + $this->event->setParams(['update' => $update]); + $this->eventManager->triggerEvent($this->event); + } + + /** + * Trigger the "postUpdate" event + * + * Triggers the "postUpdate" event mapping the following parameters: + * - $statement as "statement" + * - $result as "result" + * + * @param StatementInterface $statement + * @param ResultInterface $result + * @return void + */ + public function postUpdate(StatementInterface $statement, ResultInterface $result) + { + $this->event->setName(static::EVENT_POST_UPDATE); + $this->event->setParams([ + 'statement' => $statement, + 'result' => $result, + ]); + $this->eventManager->triggerEvent($this->event); + } + + /** + * Trigger the "preDelete" event + * + * Triggers the "preDelete" event mapping the following parameters: + * - $delete as "delete" + * + * @param Delete $delete + * @return void + */ + public function preDelete(Delete $delete) + { + $this->event->setName(static::EVENT_PRE_DELETE); + $this->event->setParams(['delete' => $delete]); + $this->eventManager->triggerEvent($this->event); + } + + /** + * Trigger the "postDelete" event + * + * Triggers the "postDelete" event mapping the following parameters: + * - $statement as "statement" + * - $result as "result" + * + * @param StatementInterface $statement + * @param ResultInterface $result + * @return void + */ + public function postDelete(StatementInterface $statement, ResultInterface $result) + { + $this->event->setName(static::EVENT_POST_DELETE); + $this->event->setParams([ + 'statement' => $statement, + 'result' => $result, + ]); + $this->eventManager->triggerEvent($this->event); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php new file mode 100644 index 000000000..0310c4674 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php @@ -0,0 +1,138 @@ +name; + } + + /** + * Get target/context from which event was triggered + * + * @return null|string|object + */ + public function getTarget() + { + return $this->target; + } + + /** + * Get parameters passed to the event + * + * @return array|\ArrayAccess + */ + public function getParams() + { + return $this->params; + } + + /** + * Get a single parameter by name + * + * @param string $name + * @param mixed $default Default value to return if parameter does not exist + * @return mixed + */ + public function getParam($name, $default = null) + { + return (isset($this->params[$name]) ? $this->params[$name] : $default); + } + + /** + * Set the event name + * + * @param string $name + * @return void + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Set the event target/context + * + * @param null|string|object $target + * @return void + */ + public function setTarget($target) + { + $this->target = $target; + } + + /** + * Set event parameters + * + * @param string $params + * @return void + */ + public function setParams($params) + { + $this->params = $params; + } + + /** + * Set a single parameter by key + * + * @param string $name + * @param mixed $value + * @return void + */ + public function setParam($name, $value) + { + $this->params[$name] = $value; + } + + /** + * Indicate whether or not the parent EventManagerInterface should stop propagating events + * + * @param bool $flag + * @return void + */ + public function stopPropagation($flag = true) + { + return; + } + + /** + * Has this event indicated event propagation should stop? + * + * @return bool + */ + public function propagationIsStopped() + { + return false; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/EventFeatureEventsInterface.php b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/EventFeatureEventsInterface.php new file mode 100644 index 000000000..107f374bb --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/EventFeatureEventsInterface.php @@ -0,0 +1,35 @@ +addFeatures($features); + } + } + + /** + * @param AbstractTableGateway $tableGateway + * @return self Provides a fluent interface + */ + public function setTableGateway(AbstractTableGateway $tableGateway) + { + $this->tableGateway = $tableGateway; + foreach ($this->features as $feature) { + $feature->setTableGateway($this->tableGateway); + } + return $this; + } + + public function getFeatureByClassName($featureClassName) + { + $feature = false; + foreach ($this->features as $potentialFeature) { + if ($potentialFeature instanceof $featureClassName) { + $feature = $potentialFeature; + break; + } + } + return $feature; + } + + /** + * @param array $features + * @return self Provides a fluent interface + */ + public function addFeatures(array $features) + { + foreach ($features as $feature) { + $this->addFeature($feature); + } + return $this; + } + + /** + * @param AbstractFeature $feature + * @return self Provides a fluent interface + */ + public function addFeature(AbstractFeature $feature) + { + if ($this->tableGateway instanceof TableGatewayInterface) { + $feature->setTableGateway($this->tableGateway); + } + $this->features[] = $feature; + return $this; + } + + public function apply($method, $args) + { + foreach ($this->features as $feature) { + if (method_exists($feature, $method)) { + $return = call_user_func_array([$feature, $method], $args); + if ($return === self::APPLY_HALT) { + break; + } + } + } + } + + /** + * @param string $property + * @return bool + */ + public function canCallMagicGet($property) + { + return false; + } + + /** + * @param string $property + * @return mixed + */ + public function callMagicGet($property) + { + $return = null; + return $return; + } + + /** + * @param string $property + * @return bool + */ + public function canCallMagicSet($property) + { + return false; + } + + /** + * @param $property + * @param $value + * @return mixed + */ + public function callMagicSet($property, $value) + { + $return = null; + return $return; + } + + /** + * Is the method requested available in one of the added features + * @param string $method + * @return bool + */ + public function canCallMagicCall($method) + { + if (! empty($this->features)) { + foreach ($this->features as $feature) { + if (method_exists($feature, $method)) { + return true; + } + } + } + return false; + } + + /** + * Call method of on added feature as though it were a local method + * @param string $method + * @param array $arguments + * @return mixed + */ + public function callMagicCall($method, $arguments) + { + foreach ($this->features as $feature) { + if (method_exists($feature, $method)) { + return $feature->$method($arguments); + } + } + + return; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/GlobalAdapterFeature.php b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/GlobalAdapterFeature.php new file mode 100644 index 000000000..5fb814461 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/GlobalAdapterFeature.php @@ -0,0 +1,66 @@ +tableGateway->adapter = self::getStaticAdapter(); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/MasterSlaveFeature.php b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/MasterSlaveFeature.php new file mode 100644 index 000000000..204073da9 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/MasterSlaveFeature.php @@ -0,0 +1,90 @@ +slaveAdapter = $slaveAdapter; + if ($slaveSql) { + $this->slaveSql = $slaveSql; + } + } + + public function getSlaveAdapter() + { + return $this->slaveAdapter; + } + + /** + * @return Sql + */ + public function getSlaveSql() + { + return $this->slaveSql; + } + + /** + * after initialization, retrieve the original adapter as "master" + */ + public function postInitialize() + { + $this->masterSql = $this->tableGateway->sql; + if ($this->slaveSql === null) { + $this->slaveSql = new Sql( + $this->slaveAdapter, + $this->tableGateway->sql->getTable(), + $this->tableGateway->sql->getSqlPlatform() + ); + } + } + + /** + * preSelect() + * Replace adapter with slave temporarily + */ + public function preSelect() + { + $this->tableGateway->sql = $this->slaveSql; + } + + /** + * postSelect() + * Ensure to return to the master adapter + */ + public function postSelect() + { + $this->tableGateway->sql = $this->masterSql; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/MetadataFeature.php b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/MetadataFeature.php new file mode 100644 index 000000000..cb0197ad5 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/MetadataFeature.php @@ -0,0 +1,95 @@ +metadata = $metadata; + } + $this->sharedData['metadata'] = [ + 'primaryKey' => null, + 'columns' => [] + ]; + } + + public function postInitialize() + { + if ($this->metadata === null) { + $this->metadata = SourceFactory::createSourceFromAdapter($this->tableGateway->adapter); + } + + // localize variable for brevity + $t = $this->tableGateway; + $m = $this->metadata; + + $tableGatewayTable = is_array($t->table) ? current($t->table) : $t->table; + + if ($tableGatewayTable instanceof TableIdentifier) { + $table = $tableGatewayTable->getTable(); + $schema = $tableGatewayTable->getSchema(); + } else { + $table = $tableGatewayTable; + $schema = null; + } + + // get column named + $columns = $m->getColumnNames($table, $schema); + $t->columns = $columns; + + // set locally + $this->sharedData['metadata']['columns'] = $columns; + + // process primary key only if table is a table; there are no PK constraints on views + if (! ($m->getTable($table, $schema) instanceof TableObject)) { + return; + } + + $pkc = null; + + foreach ($m->getConstraints($table, $schema) as $constraint) { + /** @var $constraint \Laminas\Db\Metadata\Object\ConstraintObject */ + if ($constraint->getType() == 'PRIMARY KEY') { + $pkc = $constraint; + break; + } + } + + if ($pkc === null) { + throw new Exception\RuntimeException('A primary key for this column could not be found in the metadata.'); + } + + $pkcColumns = $pkc->getColumns(); + if (count($pkcColumns) === 1) { + $primaryKey = $pkcColumns[0]; + } else { + $primaryKey = $pkcColumns; + } + + $this->sharedData['metadata']['primaryKey'] = $primaryKey; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/RowGatewayFeature.php b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/RowGatewayFeature.php new file mode 100644 index 000000000..2c10715e9 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/RowGatewayFeature.php @@ -0,0 +1,77 @@ +constructorArguments = func_get_args(); + } + + public function postInitialize() + { + $args = $this->constructorArguments; + + /** @var $resultSetPrototype ResultSet */ + $resultSetPrototype = $this->tableGateway->resultSetPrototype; + + if (! $this->tableGateway->resultSetPrototype instanceof ResultSet) { + throw new Exception\RuntimeException( + 'This feature ' . __CLASS__ . ' expects the ResultSet to be an instance of ' . ResultSet::class + ); + } + + if (isset($args[0])) { + if (is_string($args[0])) { + $primaryKey = $args[0]; + $rowGatewayPrototype = new RowGateway( + $primaryKey, + $this->tableGateway->table, + $this->tableGateway->adapter + ); + $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); + } elseif ($args[0] instanceof RowGatewayInterface) { + $rowGatewayPrototype = $args[0]; + $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); + } + } else { + // get from metadata feature + $metadata = $this->tableGateway->featureSet->getFeatureByClassName( + 'Laminas\Db\TableGateway\Feature\MetadataFeature' + ); + if ($metadata === false || ! isset($metadata->sharedData['metadata'])) { + throw new Exception\RuntimeException( + 'No information was provided to the RowGatewayFeature and/or no MetadataFeature could be consulted ' + . 'to find the primary key necessary for RowGateway object creation.' + ); + } + $primaryKey = $metadata->sharedData['metadata']['primaryKey']; + $rowGatewayPrototype = new RowGateway( + $primaryKey, + $this->tableGateway->table, + $this->tableGateway->adapter + ); + $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); + } + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/SequenceFeature.php b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/SequenceFeature.php new file mode 100644 index 000000000..cec94b3e5 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/Feature/SequenceFeature.php @@ -0,0 +1,132 @@ +primaryKeyField = $primaryKeyField; + $this->sequenceName = $sequenceName; + } + + /** + * @param Insert $insert + * @return Insert + */ + public function preInsert(Insert $insert) + { + $columns = $insert->getRawState('columns'); + $values = $insert->getRawState('values'); + $key = array_search($this->primaryKeyField, $columns); + if ($key !== false) { + $this->sequenceValue = isset($values[$key]) ? $values[$key] : null; + return $insert; + } + + $this->sequenceValue = $this->nextSequenceId(); + if ($this->sequenceValue === null) { + return $insert; + } + + $insert->values([$this->primaryKeyField => $this->sequenceValue], Insert::VALUES_MERGE); + return $insert; + } + + /** + * @param StatementInterface $statement + * @param ResultInterface $result + */ + public function postInsert(StatementInterface $statement, ResultInterface $result) + { + if ($this->sequenceValue !== null) { + $this->tableGateway->lastInsertValue = $this->sequenceValue; + } + } + + /** + * Generate a new value from the specified sequence in the database, and return it. + * @return int + */ + public function nextSequenceId() + { + $platform = $this->tableGateway->adapter->getPlatform(); + $platformName = $platform->getName(); + + switch ($platformName) { + case 'Oracle': + $sql = 'SELECT ' . $platform->quoteIdentifier($this->sequenceName) . '.NEXTVAL as "nextval" FROM dual'; + break; + case 'PostgreSQL': + $sql = 'SELECT NEXTVAL(\'"' . $this->sequenceName . '"\')'; + break; + default: + return; + } + + $statement = $this->tableGateway->adapter->createStatement(); + $statement->prepare($sql); + $result = $statement->execute(); + $sequence = $result->current(); + unset($statement, $result); + return $sequence['nextval']; + } + + /** + * Return the most recent value from the specified sequence in the database. + * @return int + */ + public function lastSequenceId() + { + $platform = $this->tableGateway->adapter->getPlatform(); + $platformName = $platform->getName(); + + switch ($platformName) { + case 'Oracle': + $sql = 'SELECT ' . $platform->quoteIdentifier($this->sequenceName) . '.CURRVAL as "currval" FROM dual'; + break; + case 'PostgreSQL': + $sql = 'SELECT CURRVAL(\'' . $this->sequenceName . '\')'; + break; + default: + return; + } + + $statement = $this->tableGateway->adapter->createStatement(); + $statement->prepare($sql); + $result = $statement->execute(); + $sequence = $result->current(); + unset($statement, $result); + return $sequence['currval']; + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/TableGateway.php b/bundled-libs/laminas/laminas-db/src/TableGateway/TableGateway.php new file mode 100644 index 000000000..08e6aaf0f --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/TableGateway.php @@ -0,0 +1,82 @@ +table = $table; + + // adapter + $this->adapter = $adapter; + + // process features + if ($features !== null) { + if ($features instanceof Feature\AbstractFeature) { + $features = [$features]; + } + if (is_array($features)) { + $this->featureSet = new Feature\FeatureSet($features); + } elseif ($features instanceof Feature\FeatureSet) { + $this->featureSet = $features; + } else { + throw new Exception\InvalidArgumentException( + 'TableGateway expects $feature to be an instance of an AbstractFeature or a FeatureSet, or an ' + . 'array of AbstractFeatures' + ); + } + } else { + $this->featureSet = new Feature\FeatureSet(); + } + + // result prototype + $this->resultSetPrototype = ($resultSetPrototype) ?: new ResultSet; + + // Sql object (factory for select, insert, update, delete) + $this->sql = ($sql) ?: new Sql($this->adapter, $this->table); + + // check sql object bound to same table + if ($this->sql->getTable() != $this->table) { + throw new Exception\InvalidArgumentException( + 'The table inside the provided Sql object must match the table of this TableGateway' + ); + } + + $this->initialize(); + } +} diff --git a/bundled-libs/laminas/laminas-db/src/TableGateway/TableGatewayInterface.php b/bundled-libs/laminas/laminas-db/src/TableGateway/TableGatewayInterface.php new file mode 100644 index 000000000..bb5312232 --- /dev/null +++ b/bundled-libs/laminas/laminas-db/src/TableGateway/TableGatewayInterface.php @@ -0,0 +1,18 @@ +setFromArray($options); + } + } + + /** + * Set one or more configuration properties + * + * @param array|Traversable|AbstractOptions $options + * @throws Exception\InvalidArgumentException + * @return AbstractOptions Provides fluent interface + */ + public function setFromArray($options) + { + if ($options instanceof self) { + $options = $options->toArray(); + } + + if (! is_array($options) && ! $options instanceof Traversable) { + throw new Exception\InvalidArgumentException( + sprintf( + 'Parameter provided to %s must be an %s, %s or %s', + __METHOD__, + 'array', + 'Traversable', + 'Laminas\Stdlib\AbstractOptions' + ) + ); + } + + foreach ($options as $key => $value) { + $this->__set($key, $value); + } + + return $this; + } + + /** + * Cast to array + * + * @return array + */ + public function toArray() + { + $array = []; + $transform = function ($letters) { + $letter = array_shift($letters); + return '_' . strtolower($letter); + }; + foreach ($this as $key => $value) { + if ($key === '__strictMode__') { + continue; + } + $normalizedKey = preg_replace_callback('/([A-Z])/', $transform, $key); + $array[$normalizedKey] = $value; + } + return $array; + } + + /** + * Set a configuration property + * + * @see ParameterObject::__set() + * @param string $key + * @param mixed $value + * @throws Exception\BadMethodCallException + * @return void + */ + public function __set($key, $value) + { + $setter = 'set' . str_replace('_', '', $key); + + if (is_callable([$this, $setter])) { + $this->{$setter}($value); + + return; + } + + if ($this->__strictMode__) { + throw new Exception\BadMethodCallException(sprintf( + 'The option "%s" does not have a callable "%s" ("%s") setter method which must be defined', + $key, + 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))), + $setter + )); + } + } + + /** + * Get a configuration property + * + * @see ParameterObject::__get() + * @param string $key + * @throws Exception\BadMethodCallException + * @return mixed + */ + public function __get($key) + { + $getter = 'get' . str_replace('_', '', $key); + + if (is_callable([$this, $getter])) { + return $this->{$getter}(); + } + + throw new Exception\BadMethodCallException(sprintf( + 'The option "%s" does not have a callable "%s" getter method which must be defined', + $key, + 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))) + )); + } + + /** + * Test if a configuration property is null + * @see ParameterObject::__isset() + * @param string $key + * @return bool + */ + public function __isset($key) + { + $getter = 'get' . str_replace('_', '', $key); + + return method_exists($this, $getter) && null !== $this->__get($key); + } + + /** + * Set a configuration property to NULL + * + * @see ParameterObject::__unset() + * @param string $key + * @throws Exception\InvalidArgumentException + * @return void + */ + public function __unset($key) + { + try { + $this->__set($key, null); + } catch (Exception\BadMethodCallException $e) { + throw new Exception\InvalidArgumentException( + 'The class property $' . $key . ' cannot be unset as' + . ' NULL is an invalid value for it', + 0, + $e + ); + } + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/ArrayObject.php b/bundled-libs/laminas/laminas-stdlib/src/ArrayObject.php new file mode 100644 index 000000000..2f7b3ef35 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/ArrayObject.php @@ -0,0 +1,451 @@ +setFlags($flags); + $this->storage = $input; + $this->setIteratorClass($iteratorClass); + $this->protectedProperties = array_keys(get_object_vars($this)); + } + + /** + * Returns whether the requested key exists + * + * @param mixed $key + * @return bool + */ + public function __isset($key) + { + if ($this->flag == self::ARRAY_AS_PROPS) { + return $this->offsetExists($key); + } + if (in_array($key, $this->protectedProperties)) { + throw new Exception\InvalidArgumentException('$key is a protected property, use a different key'); + } + + return isset($this->$key); + } + + /** + * Sets the value at the specified key to value + * + * @param mixed $key + * @param mixed $value + * @return void + */ + public function __set($key, $value) + { + if ($this->flag == self::ARRAY_AS_PROPS) { + return $this->offsetSet($key, $value); + } + if (in_array($key, $this->protectedProperties)) { + throw new Exception\InvalidArgumentException('$key is a protected property, use a different key'); + } + $this->$key = $value; + } + + /** + * Unsets the value at the specified key + * + * @param mixed $key + * @return void + */ + public function __unset($key) + { + if ($this->flag == self::ARRAY_AS_PROPS) { + return $this->offsetUnset($key); + } + if (in_array($key, $this->protectedProperties)) { + throw new Exception\InvalidArgumentException('$key is a protected property, use a different key'); + } + unset($this->$key); + } + + /** + * Returns the value at the specified key by reference + * + * @param mixed $key + * @return mixed + */ + public function &__get($key) + { + $ret = null; + if ($this->flag == self::ARRAY_AS_PROPS) { + $ret =& $this->offsetGet($key); + + return $ret; + } + if (in_array($key, $this->protectedProperties)) { + throw new Exception\InvalidArgumentException('$key is a protected property, use a different key'); + } + + return $this->$key; + } + + /** + * Appends the value + * + * @param mixed $value + * @return void + */ + public function append($value) + { + $this->storage[] = $value; + } + + /** + * Sort the entries by value + * + * @return void + */ + public function asort() + { + asort($this->storage); + } + + /** + * Get the number of public properties in the ArrayObject + * + * @return int + */ + public function count() + { + return count($this->storage); + } + + /** + * Exchange the array for another one. + * + * @param array|ArrayObject $data + * @return array + */ + public function exchangeArray($data) + { + if (! is_array($data) && ! is_object($data)) { + throw new Exception\InvalidArgumentException( + 'Passed variable is not an array or object, using empty array instead' + ); + } + + if (is_object($data) && ($data instanceof self || $data instanceof \ArrayObject)) { + $data = $data->getArrayCopy(); + } + if (! is_array($data)) { + $data = (array) $data; + } + + $storage = $this->storage; + + $this->storage = $data; + + return $storage; + } + + /** + * Creates a copy of the ArrayObject. + * + * @return array + */ + public function getArrayCopy() + { + return $this->storage; + } + + /** + * Gets the behavior flags. + * + * @return int + */ + public function getFlags() + { + return $this->flag; + } + + /** + * Create a new iterator from an ArrayObject instance + * + * @return \Iterator + */ + public function getIterator() + { + $class = $this->iteratorClass; + + return new $class($this->storage); + } + + /** + * Gets the iterator classname for the ArrayObject. + * + * @return string + */ + public function getIteratorClass() + { + return $this->iteratorClass; + } + + /** + * Sort the entries by key + * + * @return void + */ + public function ksort() + { + ksort($this->storage); + } + + /** + * Sort an array using a case insensitive "natural order" algorithm + * + * @return void + */ + public function natcasesort() + { + natcasesort($this->storage); + } + + /** + * Sort entries using a "natural order" algorithm + * + * @return void + */ + public function natsort() + { + natsort($this->storage); + } + + /** + * Returns whether the requested key exists + * + * @param mixed $key + * @return bool + */ + public function offsetExists($key) + { + return isset($this->storage[$key]); + } + + /** + * Returns the value at the specified key + * + * @param mixed $key + * @return mixed + */ + public function &offsetGet($key) + { + $ret = null; + if (! $this->offsetExists($key)) { + return $ret; + } + $ret =& $this->storage[$key]; + + return $ret; + } + + /** + * Sets the value at the specified key to value + * + * @param mixed $key + * @param mixed $value + * @return void + */ + public function offsetSet($key, $value) + { + $this->storage[$key] = $value; + } + + /** + * Unsets the value at the specified key + * + * @param mixed $key + * @return void + */ + public function offsetUnset($key) + { + if ($this->offsetExists($key)) { + unset($this->storage[$key]); + } + } + + /** + * Serialize an ArrayObject + * + * @return string + */ + public function serialize() + { + return serialize(get_object_vars($this)); + } + + /** + * Sets the behavior flags + * + * @param int $flags + * @return void + */ + public function setFlags($flags) + { + $this->flag = $flags; + } + + /** + * Sets the iterator classname for the ArrayObject + * + * @param string $class + * @return void + */ + public function setIteratorClass($class) + { + if (class_exists($class)) { + $this->iteratorClass = $class; + + return ; + } + + if (strpos($class, '\\') === 0) { + $class = '\\' . $class; + if (class_exists($class)) { + $this->iteratorClass = $class; + + return ; + } + } + + throw new Exception\InvalidArgumentException('The iterator class does not exist'); + } + + /** + * Sort the entries with a user-defined comparison function and maintain key association + * + * @param callable $function + * @return void + */ + public function uasort($function) + { + if (is_callable($function)) { + uasort($this->storage, $function); + } + } + + /** + * Sort the entries by keys using a user-defined comparison function + * + * @param callable $function + * @return void + */ + public function uksort($function) + { + if (is_callable($function)) { + uksort($this->storage, $function); + } + } + + /** + * Unserialize an ArrayObject + * + * @param string $data + * @return void + */ + public function unserialize($data) + { + $ar = unserialize($data); + $this->protectedProperties = array_keys(get_object_vars($this)); + + $this->setFlags($ar['flag']); + $this->exchangeArray($ar['storage']); + $this->setIteratorClass($ar['iteratorClass']); + + foreach ($ar as $k => $v) { + switch ($k) { + case 'flag': + $this->setFlags($v); + break; + case 'storage': + $this->exchangeArray($v); + break; + case 'iteratorClass': + $this->setIteratorClass($v); + break; + case 'protectedProperties': + break; + default: + $this->__set($k, $v); + } + } + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/ArraySerializableInterface.php b/bundled-libs/laminas/laminas-stdlib/src/ArraySerializableInterface.php new file mode 100644 index 000000000..d0d95eef1 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/ArraySerializableInterface.php @@ -0,0 +1,27 @@ +getArrayCopy(); + return new ArrayIterator(array_reverse($array)); + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/ArrayUtils.php b/bundled-libs/laminas/laminas-stdlib/src/ArrayUtils.php new file mode 100644 index 000000000..446bd6df3 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/ArrayUtils.php @@ -0,0 +1,329 @@ + + * $list = array('a', 'b', 'c', 'd'); + * $list = array( + * 0 => 'foo', + * 1 => 'bar', + * 2 => array('foo' => 'baz'), + * ); + * + * + * @param mixed $value + * @param bool $allowEmpty Is an empty list a valid list? + * @return bool + */ + public static function isList($value, $allowEmpty = false) + { + if (! is_array($value)) { + return false; + } + + if (! $value) { + return $allowEmpty; + } + + return (array_values($value) === $value); + } + + /** + * Test whether an array is a hash table. + * + * An array is a hash table if: + * + * 1. Contains one or more non-integer keys, or + * 2. Integer keys are non-continuous or misaligned (not starting with 0) + * + * For example: + * + * $hash = array( + * 'foo' => 15, + * 'bar' => false, + * ); + * $hash = array( + * 1995 => 'Birth of PHP', + * 2009 => 'PHP 5.3.0', + * 2012 => 'PHP 5.4.0', + * ); + * $hash = array( + * 'formElement, + * 'options' => array( 'debug' => true ), + * ); + * + * + * @param mixed $value + * @param bool $allowEmpty Is an empty array() a valid hash table? + * @return bool + */ + public static function isHashTable($value, $allowEmpty = false) + { + if (! is_array($value)) { + return false; + } + + if (! $value) { + return $allowEmpty; + } + + return (array_values($value) !== $value); + } + + /** + * Checks if a value exists in an array. + * + * Due to "foo" == 0 === TRUE with in_array when strict = false, an option + * has been added to prevent this. When $strict = 0/false, the most secure + * non-strict check is implemented. if $strict = -1, the default in_array + * non-strict behaviour is used. + * + * @param mixed $needle + * @param array $haystack + * @param int|bool $strict + * @return bool + */ + public static function inArray($needle, array $haystack, $strict = false) + { + if (! $strict) { + if (is_int($needle) || is_float($needle)) { + $needle = (string) $needle; + } + if (is_string($needle)) { + foreach ($haystack as &$h) { + if (is_int($h) || is_float($h)) { + $h = (string) $h; + } + } + } + } + return in_array($needle, $haystack, $strict); + } + + /** + * Convert an iterator to an array. + * + * Converts an iterator to an array. The $recursive flag, on by default, + * hints whether or not you want to do so recursively. + * + * @param array|Traversable $iterator The array or Traversable object to convert + * @param bool $recursive Recursively check all nested structures + * @throws Exception\InvalidArgumentException if $iterator is not an array or a Traversable object + * @return array + */ + public static function iteratorToArray($iterator, $recursive = true) + { + if (! is_array($iterator) && ! $iterator instanceof Traversable) { + throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable object'); + } + + if (! $recursive) { + if (is_array($iterator)) { + return $iterator; + } + + return iterator_to_array($iterator); + } + + if (is_object($iterator) && method_exists($iterator, 'toArray')) { + return $iterator->toArray(); + } + + $array = []; + foreach ($iterator as $key => $value) { + if (is_scalar($value)) { + $array[$key] = $value; + continue; + } + + if ($value instanceof Traversable) { + $array[$key] = static::iteratorToArray($value, $recursive); + continue; + } + + if (is_array($value)) { + $array[$key] = static::iteratorToArray($value, $recursive); + continue; + } + + $array[$key] = $value; + } + + return $array; + } + + /** + * Merge two arrays together. + * + * If an integer key exists in both arrays and preserveNumericKeys is false, the value + * from the second array will be appended to the first array. If both values are arrays, they + * are merged together, else the value of the second array overwrites the one of the first array. + * + * @param array $a + * @param array $b + * @param bool $preserveNumericKeys + * @return array + */ + public static function merge(array $a, array $b, $preserveNumericKeys = false) + { + foreach ($b as $key => $value) { + if ($value instanceof MergeReplaceKeyInterface) { + $a[$key] = $value->getData(); + } elseif (isset($a[$key]) || array_key_exists($key, $a)) { + if ($value instanceof MergeRemoveKey) { + unset($a[$key]); + } elseif (! $preserveNumericKeys && is_int($key)) { + $a[] = $value; + } elseif (is_array($value) && is_array($a[$key])) { + $a[$key] = static::merge($a[$key], $value, $preserveNumericKeys); + } else { + $a[$key] = $value; + } + } else { + if (! $value instanceof MergeRemoveKey) { + $a[$key] = $value; + } + } + } + + return $a; + } + + /** + * @deprecated Since 3.2.0; use the native array_filter methods + * + * @param array $data + * @param callable $callback + * @param null|int $flag + * @return array + * @throws Exception\InvalidArgumentException + */ + public static function filter(array $data, $callback, $flag = null) + { + if (! is_callable($callback)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Second parameter of %s must be callable', + __METHOD__ + )); + } + + return array_filter($data, $callback, $flag); + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/ArrayUtils/MergeRemoveKey.php b/bundled-libs/laminas/laminas-stdlib/src/ArrayUtils/MergeRemoveKey.php new file mode 100644 index 000000000..8c9d56e69 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/ArrayUtils/MergeRemoveKey.php @@ -0,0 +1,13 @@ +data = $data; + } + + /** + * {@inheritDoc} + */ + public function getData() + { + return $this->data; + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/ArrayUtils/MergeReplaceKeyInterface.php b/bundled-libs/laminas/laminas-stdlib/src/ArrayUtils/MergeReplaceKeyInterface.php new file mode 100644 index 000000000..54c244382 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/ArrayUtils/MergeReplaceKeyInterface.php @@ -0,0 +1,20 @@ +message`, + * `message`) + * - Write output to a specified stream, optionally with colorization. + * - Write a line of output to a specified stream, optionally with + * colorization, using the system EOL sequence.. + * - Write an error message to STDERR. + * + * Colorization will only occur when expected sequences are discovered, and + * then, only if the console terminal allows it. + * + * Essentially, provides the bare minimum to allow you to provide messages to + * the current console. + */ +class ConsoleHelper +{ + const COLOR_GREEN = "\033[32m"; + const COLOR_RED = "\033[31m"; + const COLOR_RESET = "\033[0m"; + + const HIGHLIGHT_INFO = 'info'; + const HIGHLIGHT_ERROR = 'error'; + + private $highlightMap = [ + self::HIGHLIGHT_INFO => self::COLOR_GREEN, + self::HIGHLIGHT_ERROR => self::COLOR_RED, + ]; + + /** + * @var string Exists only for testing. + */ + private $eol = PHP_EOL; + + /** + * @var resource Exists only for testing. + */ + private $stderr = STDERR; + + /** + * @var bool + */ + private $supportsColor; + + /** + * @param resource $resource + */ + public function __construct($resource = STDOUT) + { + $this->supportsColor = $this->detectColorCapabilities($resource); + } + + /** + * Colorize a string for use with the terminal. + * + * Takes strings formatted as `string` and formats them per the + * $highlightMap; if color support is disabled, simply removes the formatting + * tags. + * + * @param string $string + * @return string + */ + public function colorize($string) + { + $reset = $this->supportsColor ? self::COLOR_RESET : ''; + foreach ($this->highlightMap as $key => $color) { + $pattern = sprintf('#<%s>(.*?)#s', $key, $key); + $color = $this->supportsColor ? $color : ''; + $string = preg_replace($pattern, $color . '$1' . $reset, $string); + } + return $string; + } + + /** + * @param string $string + * @param bool $colorize Whether or not to colorize the string + * @param resource $resource Defaults to STDOUT + * @return void + */ + public function write($string, $colorize = true, $resource = STDOUT) + { + if ($colorize) { + $string = $this->colorize($string); + } + + $string = $this->formatNewlines($string); + + fwrite($resource, $string); + } + + /** + * @param string $string + * @param bool $colorize Whether or not to colorize the line + * @param resource $resource Defaults to STDOUT + * @return void + */ + public function writeLine($string, $colorize = true, $resource = STDOUT) + { + $this->write($string . $this->eol, $colorize, $resource); + } + + /** + * Emit an error message. + * + * Wraps the message in ``, and passes it to `writeLine()`, + * using STDERR as the resource; emits an additional empty line when done, + * also to STDERR. + * + * @param string $message + * @return void + */ + public function writeErrorMessage($message) + { + $this->writeLine(sprintf('%s', $message), true, $this->stderr); + $this->writeLine('', false, $this->stderr); + } + + /** + * @param resource $resource + * @return bool + */ + private function detectColorCapabilities($resource = STDOUT) + { + if ('\\' === DIRECTORY_SEPARATOR) { + // Windows + return false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); + } + + return function_exists('posix_isatty') && posix_isatty($resource); + } + + /** + * Ensure newlines are appropriate for the current terminal. + * + * @param string + * @return string + */ + private function formatNewlines($string) + { + $string = str_replace($this->eol, "\0PHP_EOL\0", $string); + $string = preg_replace("/(\r\n|\n|\r)/", $this->eol, $string); + return str_replace("\0PHP_EOL\0", $this->eol, $string); + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/DispatchableInterface.php b/bundled-libs/laminas/laminas-stdlib/src/DispatchableInterface.php new file mode 100644 index 000000000..62b813af2 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/DispatchableInterface.php @@ -0,0 +1,21 @@ +values[$priority][] = $value; + if (! isset($this->priorities[$priority])) { + $this->priorities[$priority] = $priority; + $this->maxPriority = $this->maxPriority === null ? $priority : max($priority, $this->maxPriority); + } + ++$this->count; + } + + /** + * Extract an element in the queue according to the priority and the + * order of insertion + * + * @return mixed + */ + public function extract() + { + if (! $this->valid()) { + return false; + } + $value = $this->current(); + $this->nextAndRemove(); + return $value; + } + + /** + * Remove an item from the queue + * + * This is different than {@link extract()}; its purpose is to dequeue an + * item. + * + * Note: this removes the first item matching the provided item found. If + * the same item has been added multiple times, it will not remove other + * instances. + * + * @param mixed $datum + * @return bool False if the item was not found, true otherwise. + */ + public function remove($datum) + { + $currentIndex = $this->index; + $currentSubIndex = $this->subIndex; + $currentPriority = $this->maxPriority; + + $this->rewind(); + while ($this->valid()) { + if (current($this->values[$this->maxPriority]) === $datum) { + $index = key($this->values[$this->maxPriority]); + unset($this->values[$this->maxPriority][$index]); + + // The `next()` method advances the internal array pointer, so we need to use the `reset()` function, + // otherwise we would lose all elements before the place the pointer points. + reset($this->values[$this->maxPriority]); + + $this->index = $currentIndex; + $this->subIndex = $currentSubIndex; + + // If the array is empty we need to destroy the unnecessary priority, + // otherwise we would end up with an incorrect value of `$this->count` + // {@see \Laminas\Stdlib\FastPriorityQueue::nextAndRemove()}. + if (empty($this->values[$this->maxPriority])) { + unset($this->values[$this->maxPriority]); + unset($this->priorities[$this->maxPriority]); + if ($this->maxPriority === $currentPriority) { + $this->subIndex = 0; + } + } + + $this->maxPriority = empty($this->priorities) ? null : max($this->priorities); + --$this->count; + return true; + } + $this->next(); + } + return false; + } + + /** + * Get the total number of elements in the queue + * + * @return integer + */ + public function count() + { + return $this->count; + } + + /** + * Get the current element in the queue + * + * @return mixed + */ + public function current() + { + switch ($this->extractFlag) { + case self::EXTR_DATA: + return current($this->values[$this->maxPriority]); + case self::EXTR_PRIORITY: + return $this->maxPriority; + case self::EXTR_BOTH: + return [ + 'data' => current($this->values[$this->maxPriority]), + 'priority' => $this->maxPriority + ]; + } + } + + /** + * Get the index of the current element in the queue + * + * @return integer + */ + public function key() + { + return $this->index; + } + + /** + * Set the iterator pointer to the next element in the queue + * removing the previous element + */ + protected function nextAndRemove() + { + $key = key($this->values[$this->maxPriority]); + + if (false === next($this->values[$this->maxPriority])) { + unset($this->priorities[$this->maxPriority]); + unset($this->values[$this->maxPriority]); + $this->maxPriority = empty($this->priorities) ? null : max($this->priorities); + $this->subIndex = -1; + } else { + unset($this->values[$this->maxPriority][$key]); + } + ++$this->index; + ++$this->subIndex; + --$this->count; + } + + /** + * Set the iterator pointer to the next element in the queue + * without removing the previous element + */ + public function next() + { + if (false === next($this->values[$this->maxPriority])) { + unset($this->subPriorities[$this->maxPriority]); + reset($this->values[$this->maxPriority]); + $this->maxPriority = empty($this->subPriorities) ? null : max($this->subPriorities); + $this->subIndex = -1; + } + ++$this->index; + ++$this->subIndex; + } + + /** + * Check if the current iterator is valid + * + * @return boolean + */ + public function valid() + { + return isset($this->values[$this->maxPriority]); + } + + /** + * Rewind the current iterator + */ + public function rewind() + { + $this->subPriorities = $this->priorities; + $this->maxPriority = empty($this->priorities) ? 0 : max($this->priorities); + $this->index = 0; + $this->subIndex = 0; + } + + /** + * Serialize to an array + * + * Array will be priority => data pairs + * + * @return array + */ + public function toArray() + { + $array = []; + foreach (clone $this as $item) { + $array[] = $item; + } + return $array; + } + + /** + * Serialize + * + * @return string + */ + public function serialize() + { + $clone = clone $this; + $clone->setExtractFlags(self::EXTR_BOTH); + + $data = []; + foreach ($clone as $item) { + $data[] = $item; + } + + return serialize($data); + } + + /** + * Deserialize + * + * @param string $data + * @return void + */ + public function unserialize($data) + { + foreach (unserialize($data) as $item) { + $this->insert($item['data'], $item['priority']); + } + } + + /** + * Set the extract flag + * + * @param integer $flag + */ + public function setExtractFlags($flag) + { + switch ($flag) { + case self::EXTR_DATA: + case self::EXTR_PRIORITY: + case self::EXTR_BOTH: + $this->extractFlag = $flag; + break; + default: + throw new Exception\InvalidArgumentException("The extract flag specified is not valid"); + } + } + + /** + * Check if the queue is empty + * + * @return boolean + */ + public function isEmpty() + { + return empty($this->values); + } + + /** + * Does the queue contain the given datum? + * + * @param mixed $datum + * @return bool + */ + public function contains($datum) + { + foreach ($this->values as $values) { + if (in_array($datum, $values)) { + return true; + } + } + return false; + } + + /** + * Does the queue have an item with the given priority? + * + * @param int $priority + * @return bool + */ + public function hasPriority($priority) + { + return isset($this->values[$priority]); + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/Glob.php b/bundled-libs/laminas/laminas-stdlib/src/Glob.php new file mode 100644 index 000000000..9b59a3536 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/Glob.php @@ -0,0 +1,217 @@ + GLOB_MARK, + self::GLOB_NOSORT => GLOB_NOSORT, + self::GLOB_NOCHECK => GLOB_NOCHECK, + self::GLOB_NOESCAPE => GLOB_NOESCAPE, + self::GLOB_BRACE => defined('GLOB_BRACE') ? GLOB_BRACE : 0, + self::GLOB_ONLYDIR => GLOB_ONLYDIR, + self::GLOB_ERR => GLOB_ERR, + ]; + + $globFlags = 0; + + foreach ($flagMap as $internalFlag => $globFlag) { + if ($flags & $internalFlag) { + $globFlags |= $globFlag; + } + } + } else { + $globFlags = 0; + } + + ErrorHandler::start(); + $res = glob($pattern, $globFlags); + $err = ErrorHandler::stop(); + if ($res === false) { + throw new Exception\RuntimeException("glob('{$pattern}', {$globFlags}) failed", 0, $err); + } + return $res; + } + + /** + * Expand braces manually, then use the system glob. + * + * @param string $pattern + * @param int $flags + * @return array + * @throws Exception\RuntimeException + */ + protected static function fallbackGlob($pattern, $flags) + { + if (! $flags & self::GLOB_BRACE) { + return static::systemGlob($pattern, $flags); + } + + $flags &= ~self::GLOB_BRACE; + $length = strlen($pattern); + $paths = []; + + if ($flags & self::GLOB_NOESCAPE) { + $begin = strpos($pattern, '{'); + } else { + $begin = 0; + + while (true) { + if ($begin === $length) { + $begin = false; + break; + } elseif ($pattern[$begin] === '\\' && ($begin + 1) < $length) { + $begin++; + } elseif ($pattern[$begin] === '{') { + break; + } + + $begin++; + } + } + + if ($begin === false) { + return static::systemGlob($pattern, $flags); + } + + $next = static::nextBraceSub($pattern, $begin + 1, $flags); + + if ($next === null) { + return static::systemGlob($pattern, $flags); + } + + $rest = $next; + + while ($pattern[$rest] !== '}') { + $rest = static::nextBraceSub($pattern, $rest + 1, $flags); + + if ($rest === null) { + return static::systemGlob($pattern, $flags); + } + } + + $p = $begin + 1; + + while (true) { + $subPattern = substr($pattern, 0, $begin) + . substr($pattern, $p, $next - $p) + . substr($pattern, $rest + 1); + + $result = static::fallbackGlob($subPattern, $flags | self::GLOB_BRACE); + + if ($result) { + $paths = array_merge($paths, $result); + } + + if ($pattern[$next] === '}') { + break; + } + + $p = $next + 1; + $next = static::nextBraceSub($pattern, $p, $flags); + } + + return array_unique($paths); + } + + /** + * Find the end of the sub-pattern in a brace expression. + * + * @param string $pattern + * @param int $begin + * @param int $flags + * @return int|null + */ + protected static function nextBraceSub($pattern, $begin, $flags) + { + $length = strlen($pattern); + $depth = 0; + $current = $begin; + + while ($current < $length) { + if (! $flags & self::GLOB_NOESCAPE && $pattern[$current] === '\\') { + if (++$current === $length) { + break; + } + + $current++; + } else { + if (($pattern[$current] === '}' && $depth-- === 0) || ($pattern[$current] === ',' && $depth === 0)) { + break; + } elseif ($pattern[$current++] === '{') { + $depth++; + } + } + } + + return ($current < $length ? $current : null); + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/Guard/AllGuardsTrait.php b/bundled-libs/laminas/laminas-stdlib/src/Guard/AllGuardsTrait.php new file mode 100644 index 000000000..e701c176b --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/Guard/AllGuardsTrait.php @@ -0,0 +1,19 @@ +metadata[$spec] = $value; + return $this; + } + if (! is_array($spec) && ! $spec instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected a string, array, or Traversable argument in first position; received "%s"', + (is_object($spec) ? get_class($spec) : gettype($spec)) + )); + } + foreach ($spec as $key => $value) { + $this->metadata[$key] = $value; + } + return $this; + } + + /** + * Retrieve all metadata or a single metadatum as specified by key + * + * @param null|string|int $key + * @param null|mixed $default + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function getMetadata($key = null, $default = null) + { + if (null === $key) { + return $this->metadata; + } + + if (! is_scalar($key)) { + throw new Exception\InvalidArgumentException('Non-scalar argument provided for key'); + } + + if (array_key_exists($key, $this->metadata)) { + return $this->metadata[$key]; + } + + return $default; + } + + /** + * Set message content + * + * @param mixed $value + * @return Message + */ + public function setContent($value) + { + $this->content = $value; + return $this; + } + + /** + * Get message content + * + * @return mixed + */ + public function getContent() + { + return $this->content; + } + + /** + * @return string + */ + public function toString() + { + $request = ''; + foreach ($this->getMetadata() as $key => $value) { + $request .= sprintf( + "%s: %s\r\n", + (string) $key, + (string) $value + ); + } + $request .= "\r\n" . $this->getContent(); + return $request; + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/MessageInterface.php b/bundled-libs/laminas/laminas-stdlib/src/MessageInterface.php new file mode 100644 index 000000000..76f089a02 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/MessageInterface.php @@ -0,0 +1,43 @@ +exchangeArray($values); + } + + /** + * Populate from query string + * + * @param string $string + * @return void + */ + public function fromString($string) + { + $array = []; + parse_str($string, $array); + $this->fromArray($array); + } + + /** + * Serialize to native PHP array + * + * @return array + */ + public function toArray() + { + return $this->getArrayCopy(); + } + + /** + * Serialize to query string + * + * @return string + */ + public function toString() + { + return http_build_query($this->toArray()); + } + + /** + * Retrieve by key + * + * Returns null if the key does not exist. + * + * @param string $name + * @return mixed + */ + public function offsetGet($name) + { + if ($this->offsetExists($name)) { + return parent::offsetGet($name); + } + return; + } + + /** + * @param string $name + * @param mixed $default optional default value + * @return mixed + */ + public function get($name, $default = null) + { + if ($this->offsetExists($name)) { + return parent::offsetGet($name); + } + return $default; + } + + /** + * @param string $name + * @param mixed $value + * @return Parameters + */ + public function set($name, $value) + { + $this[$name] = $value; + return $this; + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/ParametersInterface.php b/bundled-libs/laminas/laminas-stdlib/src/ParametersInterface.php new file mode 100644 index 000000000..0c62df3a5 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/ParametersInterface.php @@ -0,0 +1,85 @@ +items[$name])) { + $this->count++; + } + + $this->sorted = false; + + $this->items[$name] = [ + 'data' => $value, + 'priority' => (int) $priority, + 'serial' => $this->serial++, + ]; + } + + /** + * @param string $name + * @param int $priority + * + * @return $this + * + * @throws \Exception + */ + public function setPriority($name, $priority) + { + if (! isset($this->items[$name])) { + throw new \Exception("item $name not found"); + } + + $this->items[$name]['priority'] = (int) $priority; + $this->sorted = false; + + return $this; + } + + /** + * Remove a item. + * + * @param string $name + * @return void + */ + public function remove($name) + { + if (isset($this->items[$name])) { + $this->count--; + } + + unset($this->items[$name]); + } + + /** + * Remove all items. + * + * @return void + */ + public function clear() + { + $this->items = []; + $this->serial = 0; + $this->count = 0; + $this->sorted = false; + } + + /** + * Get a item. + * + * @param string $name + * @return mixed + */ + public function get($name) + { + if (! isset($this->items[$name])) { + return; + } + + return $this->items[$name]['data']; + } + + /** + * Sort all items. + * + * @return void + */ + protected function sort() + { + if (! $this->sorted) { + uasort($this->items, [$this, 'compare']); + $this->sorted = true; + } + } + + /** + * Compare the priority of two items. + * + * @param array $item1, + * @param array $item2 + * @return int + */ + protected function compare(array $item1, array $item2) + { + return ($item1['priority'] === $item2['priority']) + ? ($item1['serial'] > $item2['serial'] ? -1 : 1) * $this->isLIFO + : ($item1['priority'] > $item2['priority'] ? -1 : 1); + } + + /** + * Get/Set serial order mode + * + * @param bool|null $flag + * + * @return bool + */ + public function isLIFO($flag = null) + { + if ($flag !== null) { + $isLifo = $flag === true ? 1 : -1; + + if ($isLifo !== $this->isLIFO) { + $this->isLIFO = $isLifo; + $this->sorted = false; + } + } + + return 1 === $this->isLIFO; + } + + /** + * {@inheritDoc} + */ + public function rewind() + { + $this->sort(); + reset($this->items); + } + + /** + * {@inheritDoc} + */ + public function current() + { + $this->sorted || $this->sort(); + $node = current($this->items); + + return $node ? $node['data'] : false; + } + + /** + * {@inheritDoc} + */ + public function key() + { + $this->sorted || $this->sort(); + return key($this->items); + } + + /** + * {@inheritDoc} + */ + public function next() + { + $node = next($this->items); + + return $node ? $node['data'] : false; + } + + /** + * {@inheritDoc} + */ + public function valid() + { + return current($this->items) !== false; + } + + /** + * @return self + */ + public function getIterator() + { + return clone $this; + } + + /** + * {@inheritDoc} + */ + public function count() + { + return $this->count; + } + + /** + * Return list as array + * + * @param int $flag + * + * @return array + */ + public function toArray($flag = self::EXTR_DATA) + { + $this->sort(); + + if ($flag == self::EXTR_BOTH) { + return $this->items; + } + + return array_map( + function ($item) use ($flag) { + return ($flag == PriorityList::EXTR_PRIORITY) ? $item['priority'] : $item['data']; + }, + $this->items + ); + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/PriorityQueue.php b/bundled-libs/laminas/laminas-stdlib/src/PriorityQueue.php new file mode 100644 index 000000000..f506d4696 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/PriorityQueue.php @@ -0,0 +1,334 @@ +items[] = [ + 'data' => $data, + 'priority' => $priority, + ]; + $this->getQueue()->insert($data, $priority); + return $this; + } + + /** + * Remove an item from the queue + * + * This is different than {@link extract()}; its purpose is to dequeue an + * item. + * + * This operation is potentially expensive, as it requires + * re-initialization and re-population of the inner queue. + * + * Note: this removes the first item matching the provided item found. If + * the same item has been added multiple times, it will not remove other + * instances. + * + * @param mixed $datum + * @return bool False if the item was not found, true otherwise. + */ + public function remove($datum) + { + $found = false; + foreach ($this->items as $key => $item) { + if ($item['data'] === $datum) { + $found = true; + break; + } + } + if ($found) { + unset($this->items[$key]); + $this->queue = null; + + if (! $this->isEmpty()) { + $queue = $this->getQueue(); + foreach ($this->items as $item) { + $queue->insert($item['data'], $item['priority']); + } + } + return true; + } + return false; + } + + /** + * Is the queue empty? + * + * @return bool + */ + public function isEmpty() + { + return (0 === $this->count()); + } + + /** + * How many items are in the queue? + * + * @return int + */ + public function count() + { + return count($this->items); + } + + /** + * Peek at the top node in the queue, based on priority. + * + * @return mixed + */ + public function top() + { + return $this->getIterator()->top(); + } + + /** + * Extract a node from the inner queue and sift up + * + * @return mixed + */ + public function extract() + { + $value = $this->getQueue()->extract(); + + $keyToRemove = null; + $highestPriority = null; + foreach ($this->items as $key => $item) { + if ($item['data'] !== $value) { + continue; + } + + if (null === $highestPriority) { + $highestPriority = $item['priority']; + $keyToRemove = $key; + continue; + } + + if ($highestPriority >= $item['priority']) { + continue; + } + + $highestPriority = $item['priority']; + $keyToRemove = $key; + } + + if ($keyToRemove !== null) { + unset($this->items[$keyToRemove]); + } + + return $value; + } + + /** + * Retrieve the inner iterator + * + * SplPriorityQueue acts as a heap, which typically implies that as items + * are iterated, they are also removed. This does not work for situations + * where the queue may be iterated multiple times. As such, this class + * aggregates the values, and also injects an SplPriorityQueue. This method + * retrieves the inner queue object, and clones it for purposes of + * iteration. + * + * @return SplPriorityQueue + */ + public function getIterator() + { + $queue = $this->getQueue(); + return clone $queue; + } + + /** + * Serialize the data structure + * + * @return string + */ + public function serialize() + { + return serialize($this->items); + } + + /** + * Unserialize a string into a PriorityQueue object + * + * Serialization format is compatible with {@link Laminas\Stdlib\SplPriorityQueue} + * + * @param string $data + * @return void + */ + public function unserialize($data) + { + foreach (unserialize($data) as $item) { + $this->insert($item['data'], $item['priority']); + } + } + + /** + * Serialize to an array + * + * By default, returns only the item data, and in the order registered (not + * sorted). You may provide one of the EXTR_* flags as an argument, allowing + * the ability to return priorities or both data and priority. + * + * @param int $flag + * @return array + */ + public function toArray($flag = self::EXTR_DATA) + { + switch ($flag) { + case self::EXTR_BOTH: + return $this->items; + case self::EXTR_PRIORITY: + return array_map(function ($item) { + return $item['priority']; + }, $this->items); + case self::EXTR_DATA: + default: + return array_map(function ($item) { + return $item['data']; + }, $this->items); + } + } + + /** + * Specify the internal queue class + * + * Please see {@link getIterator()} for details on the necessity of an + * internal queue class. The class provided should extend SplPriorityQueue. + * + * @param string $class + * @return PriorityQueue + */ + public function setInternalQueueClass($class) + { + $this->queueClass = (string) $class; + return $this; + } + + /** + * Does the queue contain the given datum? + * + * @param mixed $datum + * @return bool + */ + public function contains($datum) + { + foreach ($this->items as $item) { + if ($item['data'] === $datum) { + return true; + } + } + return false; + } + + /** + * Does the queue have an item with the given priority? + * + * @param int $priority + * @return bool + */ + public function hasPriority($priority) + { + foreach ($this->items as $item) { + if ($item['priority'] === $priority) { + return true; + } + } + return false; + } + + /** + * Get the inner priority queue instance + * + * @throws Exception\DomainException + * @return SplPriorityQueue + */ + protected function getQueue() + { + if (null === $this->queue) { + $this->queue = new $this->queueClass(); + if (! $this->queue instanceof \SplPriorityQueue) { + throw new Exception\DomainException(sprintf( + 'PriorityQueue expects an internal queue of type SplPriorityQueue; received "%s"', + get_class($this->queue) + )); + } + } + return $this->queue; + } + + /** + * Add support for deep cloning + * + * @return void + */ + public function __clone() + { + if (null !== $this->queue) { + $this->queue = clone $this->queue; + } + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/Request.php b/bundled-libs/laminas/laminas-stdlib/src/Request.php new file mode 100644 index 000000000..a593a480f --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/Request.php @@ -0,0 +1,14 @@ +serial--]; + } + parent::insert($datum, $priority); + } + + /** + * Serialize to an array + * + * Array will be priority => data pairs + * + * @return array + */ + public function toArray() + { + $array = []; + foreach (clone $this as $item) { + $array[] = $item; + } + return $array; + } + + /** + * Serialize + * + * @return string + */ + public function serialize() + { + $clone = clone $this; + $clone->setExtractFlags(self::EXTR_BOTH); + + $data = []; + foreach ($clone as $item) { + $data[] = $item; + } + + return serialize($data); + } + + /** + * Deserialize + * + * @param string $data + * @return void + */ + public function unserialize($data) + { + $this->serial = PHP_INT_MAX; + foreach (unserialize($data) as $item) { + $this->serial--; + $this->insert($item['data'], $item['priority']); + } + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/SplQueue.php b/bundled-libs/laminas/laminas-stdlib/src/SplQueue.php new file mode 100644 index 000000000..09d3707d3 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/SplQueue.php @@ -0,0 +1,57 @@ +toArray()); + } + + /** + * Unserialize + * + * @param string $data + * @return void + */ + public function unserialize($data) + { + foreach (unserialize($data) as $item) { + $this->push($item); + } + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/SplStack.php b/bundled-libs/laminas/laminas-stdlib/src/SplStack.php new file mode 100644 index 000000000..a83b9c871 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/SplStack.php @@ -0,0 +1,57 @@ +toArray()); + } + + /** + * Unserialize + * + * @param string $data + * @return void + */ + public function unserialize($data) + { + foreach (unserialize($data) as $item) { + $this->unshift($item); + } + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/StringUtils.php b/bundled-libs/laminas/laminas-stdlib/src/StringUtils.php new file mode 100644 index 000000000..849b2dd2f --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/StringUtils.php @@ -0,0 +1,194 @@ +setEncoding($encoding, $convertEncoding); + return $wrapper; + } + } + + throw new Exception\RuntimeException( + 'No wrapper found supporting "' . $encoding . '"' + . (($convertEncoding !== null) ? ' and "' . $convertEncoding . '"' : '') + ); + } + + /** + * Get a list of all known single-byte character encodings + * + * @return string[] + */ + public static function getSingleByteEncodings() + { + return static::$singleByteEncodings; + } + + /** + * Check if a given encoding is a known single-byte character encoding + * + * @param string $encoding + * @return bool + */ + public static function isSingleByteEncoding($encoding) + { + return in_array(strtoupper($encoding), static::$singleByteEncodings); + } + + /** + * Check if a given string is valid UTF-8 encoded + * + * @param string $str + * @return bool + */ + public static function isValidUtf8($str) + { + return is_string($str) && ($str === '' || preg_match('/^./su', $str) == 1); + } + + /** + * Is PCRE compiled with Unicode support? + * + * @return bool + */ + public static function hasPcreUnicodeSupport() + { + if (static::$hasPcreUnicodeSupport === null) { + ErrorHandler::start(); + static::$hasPcreUnicodeSupport = defined('PREG_BAD_UTF8_OFFSET_ERROR') && preg_match('/\pL/u', 'a') == 1; + ErrorHandler::stop(); + } + return static::$hasPcreUnicodeSupport; + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/AbstractStringWrapper.php b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/AbstractStringWrapper.php new file mode 100644 index 000000000..b0dd9d232 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/AbstractStringWrapper.php @@ -0,0 +1,280 @@ +convertEncoding = $convertEncodingUpper; + } else { + $this->convertEncoding = null; + } + $this->encoding = $encodingUpper; + + return $this; + } + + /** + * Get the defined character encoding to work with + * + * @return string + * @throws Exception\LogicException If no encoding was defined + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Get the defined character encoding to convert to + * + * @return string|null + */ + public function getConvertEncoding() + { + return $this->convertEncoding; + } + + /** + * Convert a string from defined character encoding to the defined convert encoding + * + * @param string $str + * @param bool $reverse + * @return string|false + */ + public function convert($str, $reverse = false) + { + $encoding = $this->getEncoding(); + $convertEncoding = $this->getConvertEncoding(); + if ($convertEncoding === null) { + throw new Exception\LogicException( + 'No convert encoding defined' + ); + } + + if ($encoding === $convertEncoding) { + return $str; + } + + $from = $reverse ? $convertEncoding : $encoding; + $to = $reverse ? $encoding : $convertEncoding; + throw new Exception\RuntimeException(sprintf( + 'Converting from "%s" to "%s" isn\'t supported by this string wrapper', + $from, + $to + )); + } + + /** + * Wraps a string to a given number of characters + * + * @param string $string + * @param int $width + * @param string $break + * @param bool $cut + * @return string|false + */ + public function wordWrap($string, $width = 75, $break = "\n", $cut = false) + { + $string = (string) $string; + if ($string === '') { + return ''; + } + + $break = (string) $break; + if ($break === '') { + throw new Exception\InvalidArgumentException('Break string cannot be empty'); + } + + $width = (int) $width; + if ($width === 0 && $cut) { + throw new Exception\InvalidArgumentException('Cannot force cut when width is zero'); + } + + if (StringUtils::isSingleByteEncoding($this->getEncoding())) { + return wordwrap($string, $width, $break, $cut); + } + + $stringWidth = $this->strlen($string); + $breakWidth = $this->strlen($break); + + $result = ''; + $lastStart = $lastSpace = 0; + + for ($current = 0; $current < $stringWidth; $current++) { + $char = $this->substr($string, $current, 1); + + $possibleBreak = $char; + if ($breakWidth !== 1) { + $possibleBreak = $this->substr($string, $current, $breakWidth); + } + + if ($possibleBreak === $break) { + $result .= $this->substr($string, $lastStart, $current - $lastStart + $breakWidth); + $current += $breakWidth - 1; + $lastStart = $lastSpace = $current + 1; + continue; + } + + if ($char === ' ') { + if ($current - $lastStart >= $width) { + $result .= $this->substr($string, $lastStart, $current - $lastStart) . $break; + $lastStart = $current + 1; + } + + $lastSpace = $current; + continue; + } + + if ($current - $lastStart >= $width && $cut && $lastStart >= $lastSpace) { + $result .= $this->substr($string, $lastStart, $current - $lastStart) . $break; + $lastStart = $lastSpace = $current; + continue; + } + + if ($current - $lastStart >= $width && $lastStart < $lastSpace) { + $result .= $this->substr($string, $lastStart, $lastSpace - $lastStart) . $break; + $lastStart = $lastSpace = $lastSpace + 1; + continue; + } + } + + if ($lastStart !== $current) { + $result .= $this->substr($string, $lastStart, $current - $lastStart); + } + + return $result; + } + + /** + * Pad a string to a certain length with another string + * + * @param string $input + * @param int $padLength + * @param string $padString + * @param int $padType + * @return string + */ + public function strPad($input, $padLength, $padString = ' ', $padType = STR_PAD_RIGHT) + { + if (StringUtils::isSingleByteEncoding($this->getEncoding())) { + return str_pad($input, $padLength, $padString, $padType); + } + + $lengthOfPadding = $padLength - $this->strlen($input); + if ($lengthOfPadding <= 0) { + return $input; + } + + $padStringLength = $this->strlen($padString); + if ($padStringLength === 0) { + return $input; + } + + $repeatCount = floor($lengthOfPadding / $padStringLength); + + if ($padType === STR_PAD_BOTH) { + $repeatCountLeft = $repeatCountRight = ($repeatCount - $repeatCount % 2) / 2; + + $lastStringLength = $lengthOfPadding - 2 * $repeatCountLeft * $padStringLength; + $lastStringLeftLength = $lastStringRightLength = floor($lastStringLength / 2); + $lastStringRightLength += $lastStringLength % 2; + + $lastStringLeft = $this->substr($padString, 0, $lastStringLeftLength); + $lastStringRight = $this->substr($padString, 0, $lastStringRightLength); + + return str_repeat($padString, $repeatCountLeft) . $lastStringLeft + . $input + . str_repeat($padString, $repeatCountRight) . $lastStringRight; + } + + $lastString = $this->substr($padString, 0, $lengthOfPadding % $padStringLength); + + if ($padType === STR_PAD_LEFT) { + return str_repeat($padString, $repeatCount) . $lastString . $input; + } + + return $input . str_repeat($padString, $repeatCount) . $lastString; + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/Iconv.php b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/Iconv.php new file mode 100644 index 000000000..a58b4d166 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/Iconv.php @@ -0,0 +1,294 @@ +getEncoding()); + } + + /** + * Returns the portion of string specified by the start and length parameters + * + * @param string $str + * @param int $offset + * @param int|null $length + * @return string|false + */ + public function substr($str, $offset = 0, $length = null) + { + return iconv_substr($str, $offset, $length, $this->getEncoding()); + } + + /** + * Find the position of the first occurrence of a substring in a string + * + * @param string $haystack + * @param string $needle + * @param int $offset + * @return int|false + */ + public function strpos($haystack, $needle, $offset = 0) + { + return iconv_strpos($haystack, $needle, $offset, $this->getEncoding()); + } + + /** + * Convert a string from defined encoding to the defined convert encoding + * + * @param string $str + * @param bool $reverse + * @return string|false + */ + public function convert($str, $reverse = false) + { + $encoding = $this->getEncoding(); + $convertEncoding = $this->getConvertEncoding(); + if ($convertEncoding === null) { + throw new Exception\LogicException( + 'No convert encoding defined' + ); + } + + if ($encoding === $convertEncoding) { + return $str; + } + + $fromEncoding = $reverse ? $convertEncoding : $encoding; + $toEncoding = $reverse ? $encoding : $convertEncoding; + + // automatically add "//IGNORE" to not stop converting on invalid characters + // invalid characters triggers a notice anyway + return iconv($fromEncoding, $toEncoding . '//IGNORE', $str); + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/Intl.php b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/Intl.php new file mode 100644 index 000000000..bb2d7be01 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/Intl.php @@ -0,0 +1,92 @@ +getEncoding()); + } + + /** + * Returns the portion of string specified by the start and length parameters + * + * @param string $str + * @param int $offset + * @param int|null $length + * @return string|false + */ + public function substr($str, $offset = 0, $length = null) + { + return mb_substr($str, $offset, $length, $this->getEncoding()); + } + + /** + * Find the position of the first occurrence of a substring in a string + * + * @param string $haystack + * @param string $needle + * @param int $offset + * @return int|false + */ + public function strpos($haystack, $needle, $offset = 0) + { + return mb_strpos($haystack, $needle, $offset, $this->getEncoding()); + } + + /** + * Convert a string from defined encoding to the defined convert encoding + * + * @param string $str + * @param bool $reverse + * @return string|false + */ + public function convert($str, $reverse = false) + { + $encoding = $this->getEncoding(); + $convertEncoding = $this->getConvertEncoding(); + + if ($convertEncoding === null) { + throw new Exception\LogicException( + 'No convert encoding defined' + ); + } + + if ($encoding === $convertEncoding) { + return $str; + } + + $fromEncoding = $reverse ? $convertEncoding : $encoding; + $toEncoding = $reverse ? $encoding : $convertEncoding; + return mb_convert_encoding($str, $toEncoding, $fromEncoding); + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/Native.php b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/Native.php new file mode 100644 index 000000000..10166e2e2 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/Native.php @@ -0,0 +1,139 @@ +convertEncoding = $encodingUpper; + } + + if ($convertEncoding !== null) { + if ($encodingUpper !== strtoupper($convertEncoding)) { + throw new Exception\InvalidArgumentException( + 'Wrapper doesn\'t support to convert between character encodings' + ); + } + + $this->convertEncoding = $encodingUpper; + } else { + $this->convertEncoding = null; + } + $this->encoding = $encodingUpper; + + return $this; + } + + /** + * Returns the length of the given string + * + * @param string $str + * @return int|false + */ + public function strlen($str) + { + return strlen($str); + } + + /** + * Returns the portion of string specified by the start and length parameters + * + * @param string $str + * @param int $offset + * @param int|null $length + * @return string|false + */ + public function substr($str, $offset = 0, $length = null) + { + return substr($str, $offset, $length); + } + + /** + * Find the position of the first occurrence of a substring in a string + * + * @param string $haystack + * @param string $needle + * @param int $offset + * @return int|false + */ + public function strpos($haystack, $needle, $offset = 0) + { + return strpos($haystack, $needle, $offset); + } +} diff --git a/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/StringWrapperInterface.php b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/StringWrapperInterface.php new file mode 100644 index 000000000..ea26e2853 --- /dev/null +++ b/bundled-libs/laminas/laminas-stdlib/src/StringWrapper/StringWrapperInterface.php @@ -0,0 +1,112 @@ + laminas-project.flf. + +## 0.3.5 - 2019-11-06 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#25](https://github.com/laminas/laminas-zendframework-bridge/pull/25) adds entries for ZendHttp and ZendModule, which are file name segments in files from the zend-feed and zend-config-aggregator-module packages, respectively. + +## 0.3.4 - 2019-11-06 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#24](https://github.com/laminas/laminas-zendframework-bridge/pull/24) adds a rule to never rewrite the string `Doctrine\Zend`. + +- [#23](https://github.com/laminas/laminas-zendframework-bridge/pull/23) adds a missing map for each of ZendAcl and ZendRbac, which occur in the zend-expressive-authorization-acl and zend-expressive-authorization-rbac packages, respectively. + +## 0.3.3 - 2019-11-06 + +### Added + +- [#22](https://github.com/laminas/laminas-zendframework-bridge/pull/22) adds configuration post-processing features, exposed both as a laminas-config-aggregator post processor (for use with Expressive applications) and as a laminas-modulemanager `EVENT_MERGE_CONFIG` listener (for use with MVC applications). When registered, it will post-process the configuration, replacing known Zend Framework-specific strings with their Laminas replacements. A ruleset is provided that ensures dependency configuration is rewritten in a safe manner, routing configuration is skipped, and certain top-level configuration keys are matched exactly (instead of potentially as substrings or word stems). A later release of laminas-migration will auto-register these tools in applications when possible. + +### Changed + +- [#22](https://github.com/laminas/laminas-zendframework-bridge/pull/22) removes support for PHP versions prior to PHP 5.6. We have decided to only support supported PHP versions, whether that support is via php.net or commercial. The lowest supported PHP version we have found is 5.6. Users wishing to migrate to Laminas must at least update to PHP 5.6 before doing so. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 0.3.2 - 2019-10-30 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- [#21](https://github.com/laminas/laminas-zendframework-bridge/pull/21) removes rewriting of the Amazon library, as it is not moving to Laminas. + +- [#21](https://github.com/laminas/laminas-zendframework-bridge/pull/21) removes rewriting of the GCM and APNS libraries, as they are not moving to Laminas. + +### Fixed + +- [#21](https://github.com/laminas/laminas-zendframework-bridge/pull/21) fixes how the recaptcha and twitter library package and namespaces are rewritten. + +## 0.3.1 - 2019-04-25 + +### Added + +- [#20](https://github.com/laminas/laminas-zendframework-bridge/pull/20) provides an additional autoloader that is _prepended_ to the autoloader + stack. This new autoloader will create class aliases for interfaces, classes, + and traits referenced in type hints and class declarations, ensuring PHP is + able to resolve them correctly during class_alias operations. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 0.3.0 - 2019-04-12 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- [#16](https://github.com/laminas/laminas-zendframework-bridge/pull/16) removes the `RewriteRules::classRewrite()` method, as it is no longer + needed due to internal refactoring. + +### Fixed + +- [#16](https://github.com/laminas/laminas-zendframework-bridge/pull/16) fixes how the rewrite rules detect the word `Zend` in subnamespaces and + class names to be both more robust and simpler. + +## 0.2.5 - 2019-04-11 + +### Added + +- [#12](https://github.com/laminas/laminas-zendframework-bridge/pull/12) adds functionality for ensuring we alias namespaces and classes that + include the word `Zend` in them; e.g., `Zend\Expressive\ZendView\ZendViewRendererFactory` + will now alias to `Expressive\LaminasView\LaminasViewRendererFactory`. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 0.2.4 - 2019-04-11 + +### Added + +- [#11](https://github.com/laminas/laminas-zendframework-bridge/pull/11) adds maps for the Expressive router adapter packages. + +- [#10](https://github.com/laminas/laminas-zendframework-bridge/pull/10) adds a map for the Psr7Bridge package, as it used `Zend` within a subnamespace. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 0.2.3 - 2019-04-10 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#9](https://github.com/laminas/laminas-zendframework-bridge/pull/9) fixes the mapping for the Problem Details package. + +## 0.2.2 - 2019-04-10 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Added a check that the discovered alias exists as a class, interface, or trait + before attempting to call `class_alias()`. + +## 0.2.1 - 2019-04-10 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#8](https://github.com/laminas/laminas-zendframework-bridge/pull/8) fixes mappings for each of zend-expressive-authentication-zendauthentication, + zend-expressive-zendrouter, and zend-expressive-zendviewrenderer. + +## 0.2.0 - 2019-04-01 + +### Added + +- Nothing. + +### Changed + +- [#4](https://github.com/laminas/laminas-zendframework-bridge/pull/4) rewrites the autoloader to be class-based, via the class + `Laminas\ZendFrameworkBridge\Autoloader`. Additionally, the new approach + provides a performance boost by using a balanced tree algorithm, ensuring + matches occur faster. + +### Deprecated + +- Nothing. + +### Removed + +- [#4](https://github.com/laminas/laminas-zendframework-bridge/pull/4) removes function aliasing. Function aliasing will move to the packages that + provide functions. + +### Fixed + +- Nothing. + +## 0.1.0 - 2019-03-27 + +### Added + +- Adds an autoloader file that registers with `spl_autoload_register` a routine + for aliasing legacy ZF class/interface/trait names to Laminas Project + equivalents. + +- Adds autoloader files for aliasing legacy ZF package functions to Laminas + Project equivalents. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/COPYRIGHT.md b/bundled-libs/laminas/laminas-zendframework-bridge/COPYRIGHT.md new file mode 100644 index 000000000..0a8cccc06 --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/COPYRIGHT.md @@ -0,0 +1 @@ +Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/) diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/LICENSE.md b/bundled-libs/laminas/laminas-zendframework-bridge/LICENSE.md new file mode 100644 index 000000000..10b40f142 --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/LICENSE.md @@ -0,0 +1,26 @@ +Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +- Neither the name of Laminas Foundation nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/README.md b/bundled-libs/laminas/laminas-zendframework-bridge/README.md new file mode 100644 index 000000000..f8e068e62 --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/README.md @@ -0,0 +1,24 @@ +# laminas-zendframework-bridge + +[![Build Status](https://github.com/laminas/laminas-zendframework-bridge/workflows/Continuous%20Integration/badge.svg)](https://github.com/laminas/laminas-zendframework-bridge/actions?query=workflow%3A"Continuous+Integration") + + +This library provides a custom autoloader that aliases legacy Zend Framework, +Apigility, and Expressive classes to their replacements under the Laminas +Project. + +This package should be installed only if you are also using the composer plugin +that installs Laminas packages to replace ZF/Apigility/Expressive packages. + +## Installation + +Run the following to install this library: + +```bash +$ composer require laminas/laminas-zendframework-bridge +``` + +## Support + +* [Issues](https://github.com/laminas/laminas-zendframework-bridge/issues/) +* [Forum](https://discourse.laminas.dev/) diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/composer.json b/bundled-libs/laminas/laminas-zendframework-bridge/composer.json new file mode 100644 index 000000000..d025ba0e1 --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/composer.json @@ -0,0 +1,61 @@ +{ + "name": "laminas/laminas-zendframework-bridge", + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "license": "BSD-3-Clause", + "keywords": [ + "autoloading", + "laminas", + "zf", + "zendframework" + ], + "support": { + "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues", + "source": "https://github.com/laminas/laminas-zendframework-bridge", + "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom", + "forum": "https://discourse.laminas.dev/" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", + "psalm/plugin-phpunit": "^0.15.1", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.6" + }, + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ZendFrameworkBridge\\": "src//" + } + }, + "autoload-dev": { + "files": [ + "test/classes.php" + ], + "psr-4": { + "LaminasTest\\ZendFrameworkBridge\\": "test/", + "LaminasTest\\ZendFrameworkBridge\\TestAsset\\": "test/TestAsset/classes/", + "Laminas\\ApiTools\\": "test/TestAsset/LaminasApiTools/", + "Mezzio\\": "test/TestAsset/Mezzio/", + "Laminas\\": "test/TestAsset/Laminas/" + } + }, + "extra": { + "laminas": { + "module": "Laminas\\ZendFrameworkBridge" + } + }, + "config": { + "sort-packages": true + }, + "scripts": { + "cs-check": "phpcs", + "cs-fix": "phpcbf", + "static-analysis": "psalm --shepherd --stats", + "test": "phpunit --colors=always", + "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" + } +} diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/config/replacements.php b/bundled-libs/laminas/laminas-zendframework-bridge/config/replacements.php new file mode 100644 index 000000000..f5344355f --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/config/replacements.php @@ -0,0 +1,372 @@ + 'zendframework/zendframework', + 'zend-developer-tools/toolbar/bjy' => 'zend-developer-tools/toolbar/bjy', + 'zend-developer-tools/toolbar/doctrine' => 'zend-developer-tools/toolbar/doctrine', + + // NAMESPACES + // Zend Framework components + 'Zend\\AuraDi\\Config' => 'Laminas\\AuraDi\\Config', + 'Zend\\Authentication' => 'Laminas\\Authentication', + 'Zend\\Barcode' => 'Laminas\\Barcode', + 'Zend\\Cache' => 'Laminas\\Cache', + 'Zend\\Captcha' => 'Laminas\\Captcha', + 'Zend\\Code' => 'Laminas\\Code', + 'ZendCodingStandard\\Sniffs' => 'LaminasCodingStandard\\Sniffs', + 'ZendCodingStandard\\Utils' => 'LaminasCodingStandard\\Utils', + 'Zend\\ComponentInstaller' => 'Laminas\\ComponentInstaller', + 'Zend\\Config' => 'Laminas\\Config', + 'Zend\\ConfigAggregator' => 'Laminas\\ConfigAggregator', + 'Zend\\ConfigAggregatorModuleManager' => 'Laminas\\ConfigAggregatorModuleManager', + 'Zend\\ConfigAggregatorParameters' => 'Laminas\\ConfigAggregatorParameters', + 'Zend\\Console' => 'Laminas\\Console', + 'Zend\\ContainerConfigTest' => 'Laminas\\ContainerConfigTest', + 'Zend\\Crypt' => 'Laminas\\Crypt', + 'Zend\\Db' => 'Laminas\\Db', + 'ZendDeveloperTools' => 'Laminas\\DeveloperTools', + 'Zend\\Di' => 'Laminas\\Di', + 'Zend\\Diactoros' => 'Laminas\\Diactoros', + 'ZendDiagnostics\\Check' => 'Laminas\\Diagnostics\\Check', + 'ZendDiagnostics\\Result' => 'Laminas\\Diagnostics\\Result', + 'ZendDiagnostics\\Runner' => 'Laminas\\Diagnostics\\Runner', + 'Zend\\Dom' => 'Laminas\\Dom', + 'Zend\\Escaper' => 'Laminas\\Escaper', + 'Zend\\EventManager' => 'Laminas\\EventManager', + 'Zend\\Feed' => 'Laminas\\Feed', + 'Zend\\File' => 'Laminas\\File', + 'Zend\\Filter' => 'Laminas\\Filter', + 'Zend\\Form' => 'Laminas\\Form', + 'Zend\\Http' => 'Laminas\\Http', + 'Zend\\HttpHandlerRunner' => 'Laminas\\HttpHandlerRunner', + 'Zend\\Hydrator' => 'Laminas\\Hydrator', + 'Zend\\I18n' => 'Laminas\\I18n', + 'Zend\\InputFilter' => 'Laminas\\InputFilter', + 'Zend\\Json' => 'Laminas\\Json', + 'Zend\\Ldap' => 'Laminas\\Ldap', + 'Zend\\Loader' => 'Laminas\\Loader', + 'Zend\\Log' => 'Laminas\\Log', + 'Zend\\Mail' => 'Laminas\\Mail', + 'Zend\\Math' => 'Laminas\\Math', + 'Zend\\Memory' => 'Laminas\\Memory', + 'Zend\\Mime' => 'Laminas\\Mime', + 'Zend\\ModuleManager' => 'Laminas\\ModuleManager', + 'Zend\\Mvc' => 'Laminas\\Mvc', + 'Zend\\Navigation' => 'Laminas\\Navigation', + 'Zend\\Paginator' => 'Laminas\\Paginator', + 'Zend\\Permissions' => 'Laminas\\Permissions', + 'Zend\\Pimple\\Config' => 'Laminas\\Pimple\\Config', + 'Zend\\ProblemDetails' => 'Mezzio\\ProblemDetails', + 'Zend\\ProgressBar' => 'Laminas\\ProgressBar', + 'Zend\\Psr7Bridge' => 'Laminas\\Psr7Bridge', + 'Zend\\Router' => 'Laminas\\Router', + 'Zend\\Serializer' => 'Laminas\\Serializer', + 'Zend\\Server' => 'Laminas\\Server', + 'Zend\\ServiceManager' => 'Laminas\\ServiceManager', + 'ZendService\\ReCaptcha' => 'Laminas\\ReCaptcha', + 'ZendService\\Twitter' => 'Laminas\\Twitter', + 'Zend\\Session' => 'Laminas\\Session', + 'Zend\\SkeletonInstaller' => 'Laminas\\SkeletonInstaller', + 'Zend\\Soap' => 'Laminas\\Soap', + 'Zend\\Stdlib' => 'Laminas\\Stdlib', + 'Zend\\Stratigility' => 'Laminas\\Stratigility', + 'Zend\\Tag' => 'Laminas\\Tag', + 'Zend\\Test' => 'Laminas\\Test', + 'Zend\\Text' => 'Laminas\\Text', + 'Zend\\Uri' => 'Laminas\\Uri', + 'Zend\\Validator' => 'Laminas\\Validator', + 'Zend\\View' => 'Laminas\\View', + 'ZendXml' => 'Laminas\\Xml', + 'Zend\\Xml2Json' => 'Laminas\\Xml2Json', + 'Zend\\XmlRpc' => 'Laminas\\XmlRpc', + 'ZendOAuth' => 'Laminas\\OAuth', + + // class ZendAcl in zend-expressive-authorization-acl + 'ZendAcl' => 'LaminasAcl', + 'Zend\\Expressive\\Authorization\\Acl\\ZendAcl' => 'Mezzio\\Authorization\\Acl\\LaminasAcl', + // class ZendHttpClientDecorator in zend-feed + 'ZendHttp' => 'LaminasHttp', + // class ZendModuleProvider in zend-config-aggregator-modulemanager + 'ZendModule' => 'LaminasModule', + // class ZendRbac in zend-expressive-authorization-rbac + 'ZendRbac' => 'LaminasRbac', + 'Zend\\Expressive\\Authorization\\Rbac\\ZendRbac' => 'Mezzio\\Authorization\\Rbac\\LaminasRbac', + // class ZendRouter in zend-expressive-router-zendrouter + 'ZendRouter' => 'LaminasRouter', + 'Zend\\Expressive\\Router\\ZendRouter' => 'Mezzio\\Router\\LaminasRouter', + // class ZendViewRenderer in zend-expressive-zendviewrenderer + 'ZendViewRenderer' => 'LaminasViewRenderer', + 'Zend\\Expressive\\ZendView\\ZendViewRenderer' => 'Mezzio\\LaminasView\\LaminasViewRenderer', + 'a\\Zend' => 'a\\Zend', + 'b\\Zend' => 'b\\Zend', + 'c\\Zend' => 'c\\Zend', + 'd\\Zend' => 'd\\Zend', + 'e\\Zend' => 'e\\Zend', + 'f\\Zend' => 'f\\Zend', + 'g\\Zend' => 'g\\Zend', + 'h\\Zend' => 'h\\Zend', + 'i\\Zend' => 'i\\Zend', + 'j\\Zend' => 'j\\Zend', + 'k\\Zend' => 'k\\Zend', + 'l\\Zend' => 'l\\Zend', + 'm\\Zend' => 'm\\Zend', + 'n\\Zend' => 'n\\Zend', + 'o\\Zend' => 'o\\Zend', + 'p\\Zend' => 'p\\Zend', + 'q\\Zend' => 'q\\Zend', + 'r\\Zend' => 'r\\Zend', + 's\\Zend' => 's\\Zend', + 't\\Zend' => 't\\Zend', + 'u\\Zend' => 'u\\Zend', + 'v\\Zend' => 'v\\Zend', + 'w\\Zend' => 'w\\Zend', + 'x\\Zend' => 'x\\Zend', + 'y\\Zend' => 'y\\Zend', + 'z\\Zend' => 'z\\Zend', + + // Expressive + 'Zend\\Expressive' => 'Mezzio', + 'ZendAuthentication' => 'LaminasAuthentication', + 'ZendAcl' => 'LaminasAcl', + 'ZendRbac' => 'LaminasRbac', + 'ZendRouter' => 'LaminasRouter', + 'ExpressiveUrlGenerator' => 'MezzioUrlGenerator', + 'ExpressiveInstaller' => 'MezzioInstaller', + + // Apigility + 'ZF\\Apigility' => 'Laminas\\ApiTools', + 'ZF\\ApiProblem' => 'Laminas\\ApiTools\\ApiProblem', + 'ZF\\AssetManager' => 'Laminas\\ApiTools\\AssetManager', + 'ZF\\ComposerAutoloading' => 'Laminas\\ComposerAutoloading', + 'ZF\\Configuration' => 'Laminas\\ApiTools\\Configuration', + 'ZF\\ContentNegotiation' => 'Laminas\\ApiTools\\ContentNegotiation', + 'ZF\\ContentValidation' => 'Laminas\\ApiTools\\ContentValidation', + 'ZF\\DevelopmentMode' => 'Laminas\\DevelopmentMode', + 'ZF\\Doctrine\\QueryBuilder' => 'Laminas\\ApiTools\\Doctrine\\QueryBuilder', + 'ZF\\Hal' => 'Laminas\\ApiTools\\Hal', + 'ZF\\HttpCache' => 'Laminas\\ApiTools\\HttpCache', + 'ZF\\MvcAuth' => 'Laminas\\ApiTools\\MvcAuth', + 'ZF\\OAuth2' => 'Laminas\\ApiTools\\OAuth2', + 'ZF\\Rest' => 'Laminas\\ApiTools\\Rest', + 'ZF\\Rpc' => 'Laminas\\ApiTools\\Rpc', + 'ZF\\Versioning' => 'Laminas\\ApiTools\\Versioning', + 'a\\ZF' => 'a\\ZF', + 'b\\ZF' => 'b\\ZF', + 'c\\ZF' => 'c\\ZF', + 'd\\ZF' => 'd\\ZF', + 'e\\ZF' => 'e\\ZF', + 'f\\ZF' => 'f\\ZF', + 'g\\ZF' => 'g\\ZF', + 'h\\ZF' => 'h\\ZF', + 'i\\ZF' => 'i\\ZF', + 'j\\ZF' => 'j\\ZF', + 'k\\ZF' => 'k\\ZF', + 'l\\ZF' => 'l\\ZF', + 'm\\ZF' => 'm\\ZF', + 'n\\ZF' => 'n\\ZF', + 'o\\ZF' => 'o\\ZF', + 'p\\ZF' => 'p\\ZF', + 'q\\ZF' => 'q\\ZF', + 'r\\ZF' => 'r\\ZF', + 's\\ZF' => 's\\ZF', + 't\\ZF' => 't\\ZF', + 'u\\ZF' => 'u\\ZF', + 'v\\ZF' => 'v\\ZF', + 'w\\ZF' => 'w\\ZF', + 'x\\ZF' => 'x\\ZF', + 'y\\ZF' => 'y\\ZF', + 'z\\ZF' => 'z\\ZF', + + 'ApigilityModuleInterface' => 'ApiToolsModuleInterface', + 'ApigilityProviderInterface' => 'ApiToolsProviderInterface', + 'ApigilityVersionController' => 'ApiToolsVersionController', + + // PACKAGES + // ZF components, MVC + 'zendframework/skeleton-application' => 'laminas/skeleton-application', + 'zendframework/zend-auradi-config' => 'laminas/laminas-auradi-config', + 'zendframework/zend-authentication' => 'laminas/laminas-authentication', + 'zendframework/zend-barcode' => 'laminas/laminas-barcode', + 'zendframework/zend-cache' => 'laminas/laminas-cache', + 'zendframework/zend-captcha' => 'laminas/laminas-captcha', + 'zendframework/zend-code' => 'laminas/laminas-code', + 'zendframework/zend-coding-standard' => 'laminas/laminas-coding-standard', + 'zendframework/zend-component-installer' => 'laminas/laminas-component-installer', + 'zendframework/zend-composer-autoloading' => 'laminas/laminas-composer-autoloading', + 'zendframework/zend-config-aggregator' => 'laminas/laminas-config-aggregator', + 'zendframework/zend-config' => 'laminas/laminas-config', + 'zendframework/zend-console' => 'laminas/laminas-console', + 'zendframework/zend-container-config-test' => 'laminas/laminas-container-config-test', + 'zendframework/zend-crypt' => 'laminas/laminas-crypt', + 'zendframework/zend-db' => 'laminas/laminas-db', + 'zendframework/zend-developer-tools' => 'laminas/laminas-developer-tools', + 'zendframework/zend-diactoros' => 'laminas/laminas-diactoros', + 'zendframework/zenddiagnostics' => 'laminas/laminas-diagnostics', + 'zendframework/zend-di' => 'laminas/laminas-di', + 'zendframework/zend-dom' => 'laminas/laminas-dom', + 'zendframework/zend-escaper' => 'laminas/laminas-escaper', + 'zendframework/zend-eventmanager' => 'laminas/laminas-eventmanager', + 'zendframework/zend-feed' => 'laminas/laminas-feed', + 'zendframework/zend-file' => 'laminas/laminas-file', + 'zendframework/zend-filter' => 'laminas/laminas-filter', + 'zendframework/zend-form' => 'laminas/laminas-form', + 'zendframework/zend-httphandlerrunner' => 'laminas/laminas-httphandlerrunner', + 'zendframework/zend-http' => 'laminas/laminas-http', + 'zendframework/zend-hydrator' => 'laminas/laminas-hydrator', + 'zendframework/zend-i18n' => 'laminas/laminas-i18n', + 'zendframework/zend-i18n-resources' => 'laminas/laminas-i18n-resources', + 'zendframework/zend-inputfilter' => 'laminas/laminas-inputfilter', + 'zendframework/zend-json' => 'laminas/laminas-json', + 'zendframework/zend-json-server' => 'laminas/laminas-json-server', + 'zendframework/zend-ldap' => 'laminas/laminas-ldap', + 'zendframework/zend-loader' => 'laminas/laminas-loader', + 'zendframework/zend-log' => 'laminas/laminas-log', + 'zendframework/zend-mail' => 'laminas/laminas-mail', + 'zendframework/zend-math' => 'laminas/laminas-math', + 'zendframework/zend-memory' => 'laminas/laminas-memory', + 'zendframework/zend-mime' => 'laminas/laminas-mime', + 'zendframework/zend-modulemanager' => 'laminas/laminas-modulemanager', + 'zendframework/zend-mvc' => 'laminas/laminas-mvc', + 'zendframework/zend-navigation' => 'laminas/laminas-navigation', + 'zendframework/zend-oauth' => 'laminas/laminas-oauth', + 'zendframework/zend-paginator' => 'laminas/laminas-paginator', + 'zendframework/zend-permissions-acl' => 'laminas/laminas-permissions-acl', + 'zendframework/zend-permissions-rbac' => 'laminas/laminas-permissions-rbac', + 'zendframework/zend-pimple-config' => 'laminas/laminas-pimple-config', + 'zendframework/zend-progressbar' => 'laminas/laminas-progressbar', + 'zendframework/zend-psr7bridge' => 'laminas/laminas-psr7bridge', + 'zendframework/zend-recaptcha' => 'laminas/laminas-recaptcha', + 'zendframework/zend-router' => 'laminas/laminas-router', + 'zendframework/zend-serializer' => 'laminas/laminas-serializer', + 'zendframework/zend-server' => 'laminas/laminas-server', + 'zendframework/zend-servicemanager' => 'laminas/laminas-servicemanager', + 'zendframework/zendservice-recaptcha' => 'laminas/laminas-recaptcha', + 'zendframework/zendservice-twitter' => 'laminas/laminas-twitter', + 'zendframework/zend-session' => 'laminas/laminas-session', + 'zendframework/zend-skeleton-installer' => 'laminas/laminas-skeleton-installer', + 'zendframework/zend-soap' => 'laminas/laminas-soap', + 'zendframework/zend-stdlib' => 'laminas/laminas-stdlib', + 'zendframework/zend-stratigility' => 'laminas/laminas-stratigility', + 'zendframework/zend-tag' => 'laminas/laminas-tag', + 'zendframework/zend-test' => 'laminas/laminas-test', + 'zendframework/zend-text' => 'laminas/laminas-text', + 'zendframework/zend-uri' => 'laminas/laminas-uri', + 'zendframework/zend-validator' => 'laminas/laminas-validator', + 'zendframework/zend-view' => 'laminas/laminas-view', + 'zendframework/zend-xml2json' => 'laminas/laminas-xml2json', + 'zendframework/zend-xml' => 'laminas/laminas-xml', + 'zendframework/zend-xmlrpc' => 'laminas/laminas-xmlrpc', + + // Expressive packages + 'zendframework/zend-expressive' => 'mezzio/mezzio', + 'zendframework/zend-expressive-zendrouter' => 'mezzio/mezzio-laminasrouter', + 'zendframework/zend-problem-details' => 'mezzio/mezzio-problem-details', + 'zendframework/zend-expressive-zendviewrenderer' => 'mezzio/mezzio-laminasviewrenderer', + + // Apigility packages + 'zfcampus/apigility-documentation' => 'laminas-api-tools/documentation', + 'zfcampus/statuslib-example' => 'laminas-api-tools/statuslib-example', + 'zfcampus/zf-apigility' => 'laminas-api-tools/api-tools', + 'zfcampus/zf-api-problem' => 'laminas-api-tools/api-tools-api-problem', + 'zfcampus/zf-asset-manager' => 'laminas-api-tools/api-tools-asset-manager', + 'zfcampus/zf-configuration' => 'laminas-api-tools/api-tools-configuration', + 'zfcampus/zf-content-negotiation' => 'laminas-api-tools/api-tools-content-negotiation', + 'zfcampus/zf-content-validation' => 'laminas-api-tools/api-tools-content-validation', + 'zfcampus/zf-development-mode' => 'laminas/laminas-development-mode', + 'zfcampus/zf-doctrine-querybuilder' => 'laminas-api-tools/api-tools-doctrine-querybuilder', + 'zfcampus/zf-hal' => 'laminas-api-tools/api-tools-hal', + 'zfcampus/zf-http-cache' => 'laminas-api-tools/api-tools-http-cache', + 'zfcampus/zf-mvc-auth' => 'laminas-api-tools/api-tools-mvc-auth', + 'zfcampus/zf-oauth2' => 'laminas-api-tools/api-tools-oauth2', + 'zfcampus/zf-rest' => 'laminas-api-tools/api-tools-rest', + 'zfcampus/zf-rpc' => 'laminas-api-tools/api-tools-rpc', + 'zfcampus/zf-versioning' => 'laminas-api-tools/api-tools-versioning', + + // CONFIG KEYS, SCRIPT NAMES, ETC + // ZF components + '::fromZend' => '::fromLaminas', // psr7bridge + '::toZend' => '::toLaminas', // psr7bridge + 'use_zend_loader' => 'use_laminas_loader', // zend-modulemanager + 'zend-config' => 'laminas-config', + 'zend-developer-tools/' => 'laminas-developer-tools/', + 'zend-tag-cloud' => 'laminas-tag-cloud', + 'zenddevelopertools' => 'laminas-developer-tools', + 'zendbarcode' => 'laminasbarcode', + 'ZendBarcode' => 'LaminasBarcode', + 'zendcache' => 'laminascache', + 'ZendCache' => 'LaminasCache', + 'zendconfig' => 'laminasconfig', + 'ZendConfig' => 'LaminasConfig', + 'zendfeed' => 'laminasfeed', + 'ZendFeed' => 'LaminasFeed', + 'zendfilter' => 'laminasfilter', + 'ZendFilter' => 'LaminasFilter', + 'zendform' => 'laminasform', + 'ZendForm' => 'LaminasForm', + 'zendi18n' => 'laminasi18n', + 'ZendI18n' => 'LaminasI18n', + 'zendinputfilter' => 'laminasinputfilter', + 'ZendInputFilter' => 'LaminasInputFilter', + 'zendlog' => 'laminaslog', + 'ZendLog' => 'LaminasLog', + 'zendmail' => 'laminasmail', + 'ZendMail' => 'LaminasMail', + 'zendmvc' => 'laminasmvc', + 'ZendMvc' => 'LaminasMvc', + 'zendpaginator' => 'laminaspaginator', + 'ZendPaginator' => 'LaminasPaginator', + 'zendserializer' => 'laminasserializer', + 'ZendSerializer' => 'LaminasSerializer', + 'zendtag' => 'laminastag', + 'ZendTag' => 'LaminasTag', + 'zendtext' => 'laminastext', + 'ZendText' => 'LaminasText', + 'zendvalidator' => 'laminasvalidator', + 'ZendValidator' => 'LaminasValidator', + 'zendview' => 'laminasview', + 'ZendView' => 'LaminasView', + 'zend-framework.flf' => 'laminas-project.flf', + + // Expressive-related + "'zend-expressive'" => "'mezzio'", + '"zend-expressive"' => '"mezzio"', + 'zend-expressive.' => 'mezzio.', + 'zend-expressive-authorization' => 'mezzio-authorization', + 'zend-expressive-hal' => 'mezzio-hal', + 'zend-expressive-session' => 'mezzio-session', + 'zend-expressive-swoole' => 'mezzio-swoole', + 'zend-expressive-tooling' => 'mezzio-tooling', + + // Apigility-related + "'zf-apigility'" => "'api-tools'", + '"zf-apigility"' => '"api-tools"', + 'zf-apigility/' => 'api-tools/', + 'zf-apigility-admin' => 'api-tools-admin', + 'zf-content-negotiation' => 'api-tools-content-negotiation', + 'zf-hal' => 'api-tools-hal', + 'zf-rest' => 'api-tools-rest', + 'zf-rpc' => 'api-tools-rpc', + 'zf-content-validation' => 'api-tools-content-validation', + 'zf-apigility-ui' => 'api-tools-ui', + 'zf-apigility-documentation-blueprint' => 'api-tools-documentation-blueprint', + 'zf-apigility-documentation-swagger' => 'api-tools-documentation-swagger', + 'zf-apigility-welcome' => 'api-tools-welcome', + 'zf-api-problem' => 'api-tools-api-problem', + 'zf-configuration' => 'api-tools-configuration', + 'zf-http-cache' => 'api-tools-http-cache', + 'zf-mvc-auth' => 'api-tools-mvc-auth', + 'zf-oauth2' => 'api-tools-oauth2', + 'zf-versioning' => 'api-tools-versioning', + 'ZfApigilityDoctrineQueryProviderManager' => 'LaminasApiToolsDoctrineQueryProviderManager', + 'ZfApigilityDoctrineQueryCreateFilterManager' => 'LaminasApiToolsDoctrineQueryCreateFilterManager', + 'zf-apigility-doctrine' => 'api-tools-doctrine', + 'zf-development-mode' => 'laminas-development-mode', + 'zf-doctrine-querybuilder' => 'api-tools-doctrine-querybuilder', + + // 3rd party Apigility packages + 'api-skeletons/zf-' => 'api-skeletons/zf-', // api-skeletons packages + 'zf-oauth2-' => 'zf-oauth2-', // api-skeletons OAuth2-related packages + 'ZF\\OAuth2\\Client' => 'ZF\\OAuth2\\Client', // api-skeletons/zf-oauth2-client + 'ZF\\OAuth2\\Doctrine' => 'ZF\\OAuth2\\Doctrine', // api-skeletons/zf-oauth2-doctrine +]; diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/psalm-baseline.xml b/bundled-libs/laminas/laminas-zendframework-bridge/psalm-baseline.xml new file mode 100644 index 000000000..eabf89c51 --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/psalm-baseline.xml @@ -0,0 +1,382 @@ + + + + + 'ZendAcl' => 'LaminasAcl' + 'ZendRbac' => 'LaminasRbac' + 'ZendRouter' => 'LaminasRouter' + + + + + $class + $class + + + include __DIR__ . '/../../../autoload.php' + + + load + + + $class + $class + $class + $class + $class + $class + $class + + + + $loaded[$class] + + + ClassLoader + + + $namespaces[$check] + $namespaces[$check] + + + include __DIR__ . '/../../../autoload.php' + include __DIR__ . '/../vendor/autoload.php' + include getenv('COMPOSER_VENDOR_DIR') . '/autoload.php' + + + getenv('COMPOSER_VENDOR_DIR') + getenv('COMPOSER_VENDOR_DIR') + + + include getenv('COMPOSER_VENDOR_DIR') . '/autoload.php' + + + + + $keys + + + $value + $value + $value + $value + + + function ($value) { + function ($value) { + function ($value, array $keys) { + function ($value, array $keys) { + + + replaceDependencyConfiguration + replaceDependencyFactories + replaceDependencyServices + + + $config + $newKey + $newKey + $newKey + $target + + + [$key] + + + $config[$key] + $config['aliases'][$alias] + $config['aliases'][$service] + $config['aliases'][$service] + + + $aliases[$name] + $config[$key] + $keys[$key] + $rewritten[$newKey] + $rewritten[$newKey] + $rewritten[$newKey] + $this->exactReplacements[$value] + + + $aliases[$name] + + + $a[$key] + $a[$key] + $a[] + $config + $config + $config[$key] + $config['factories'][$replacedService] + $config['services'][$replacedService] + $data + $factory + $factory + $key + $key + $name + $newKey + $newValue + $notIn[] + $result + $rewritten[$key] + $rewritten[$newKey] + $rewritten[$newKey][] + $serviceInstance + $serviceInstance + $target + $value + $value + + + + + init + onMergeConfig + + + ModuleEvent + ModuleManager + + + + + $replacement + $replacement + + + $original + $original + $original + + + $replacement + + + $this->replacements + $this->replacements + + + + + new $legacy() + + + testLegacyClassIsAliasToLaminas + testReverseAliasCreated + testTypeHint + + + testLegacyClassIsAliasToLaminas + + + self::assertSame($actual, get_class(new $legacy())) + self::assertTrue($isInterface ? interface_exists($legacy) : class_exists($legacy)) + self::assertTrue(class_exists($actual)) + self::assertTrue(class_exists($legacy)) + self::assertTrue(class_exists('Laminas\LegacyTypeHint')) + + + + + invalidServiceManagerConfiguration + testRewritesNestedKeys + testServiceManagerServiceInstancesCanBeHandled + testWillSkipInvalidConfigurations + + + $config + + + $config + $expected + + + iterable + + + $this->assertSame($expected, $processor($config)) + self::assertSame($config, $processor($config)) + self::assertSame($expected, $processor($config)) + + + require $configLocation + require $expectedResultLocation + + + + + $event + $moduleManager + + + testInitRegistersListenerWithEventManager + testOnMergeConfigProcessesAndReplacesConfigurationPulledFromListener + + + $config + + + $config + $expected + + + iterable + + + $this->assertNull($module->onMergeConfig($event)) + $this->assertSame($expected, $listener->getMergedConfig()) + + + require $configFile + require $expectationsFile + + + + + testEdgeCases + + + iterable + + + $this->assertSame($expected, $replacements->replace($string)) + + + + + ConfigAbstractFactory + ConfigAbstractFactory + InvokableFactory + InvokableFactory + + + + + 'Zend\Db\Adapter\Adapter' => 'ZF\Apigility\DbConnectedAdapter' + + + + + Factory\SlimRouterFactory + RouterInterface + + + + + LazyServiceFactory + + + + + CacheItemPoolInterface + ConfigFactory + ConfigFactory + ConfigFactory + ConfigFactory + ConfigFactory + ConfigFactory + Csp + DisplayPostHandler + Engine + EventDispatcherInterface + Factory\CachePoolFactory + Factory\EventDispatcherFactory + Factory\MailTransport + Factory\PlatesFunctionsDelegator + FeedReaderHttpClientInterface + Feed\HttpPlugClientFactory + Handler\ComicsPageHandler + Handler\ComicsPageHandlerFactory + Handler\HomePageHandler + Handler\HomePageHandlerFactory + Handler\PageHandlerFactory + Handler\PageHandlerFactory + Handler\ResumePageHandler + Handler\ResumePageHandler + Middleware\ContentSecurityPolicyMiddlewareFactory + Middleware\DisplayBlogPostHandlerDelegator + Middleware\RedirectAmpPagesMiddleware + Middleware\RedirectAmpPagesMiddlewareFactory + Middleware\RedirectsMiddleware + Middleware\RedirectsMiddleware + Middleware\XClacksOverheadMiddleware + Middleware\XClacksOverheadMiddleware + Middleware\XPoweredByMiddleware + Middleware\XPoweredByMiddleware + RequestFactory + RequestFactoryInterface + ResponseFactory + ResponseFactoryInterface + SessionCachePool + SessionCachePool + SessionCachePoolFactory + + + + + \Zend\Expressive\Router + + + + + OAuth2Client + + + + + setMergedConfig + + + + + $listener + + + $listeners + + + attach + + + $this->listeners[$eventName] + + + $this->listeners[$eventName] + + + $this->listeners[$eventName] + + + array + + + $this->listeners + + + + + $listener + + + ConfigListener + + + $this->listener + + + + + EventManager + + + $this->eventManager + + + $this->eventManager + + + $this->eventManager + + + diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/psalm.xml.dist b/bundled-libs/laminas/laminas-zendframework-bridge/psalm.xml.dist new file mode 100644 index 000000000..788bbc00d --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/psalm.xml.dist @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/src/Autoloader.php b/bundled-libs/laminas/laminas-zendframework-bridge/src/Autoloader.php new file mode 100644 index 000000000..6048766a2 --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/src/Autoloader.php @@ -0,0 +1,172 @@ +loadClass($class)) { + $legacy = $namespaces[$check] + . strtr(substr($class, strlen($check)), [ + 'ApiTools' => 'Apigility', + 'Mezzio' => 'Expressive', + 'Laminas' => 'Zend', + ]); + class_alias($class, $legacy); + } + }; + } + + /** + * @return callable + */ + private static function createAppendAutoloader(array $namespaces, ArrayObject $loaded) + { + /** + * @param string $class Class name to autoload + * @return void + */ + return static function ($class) use ($namespaces, $loaded) { + $segments = explode('\\', $class); + + if ($segments[0] === 'ZendService' && isset($segments[1])) { + $segments[0] .= '\\' . $segments[1]; + unset($segments[1]); + $segments = array_values($segments); + } + + $i = 0; + $check = ''; + + // We are checking segments of the namespace to match quicker + while (isset($segments[$i + 1], $namespaces[$check . $segments[$i] . '\\'])) { + $check .= $segments[$i] . '\\'; + ++$i; + } + + if ($check === '') { + return; + } + + $alias = $namespaces[$check] + . strtr(substr($class, strlen($check)), [ + 'Apigility' => 'ApiTools', + 'Expressive' => 'Mezzio', + 'Zend' => 'Laminas', + 'AbstractZendServer' => 'AbstractZendServer', + 'ZendServerDisk' => 'ZendServerDisk', + 'ZendServerShm' => 'ZendServerShm', + 'ZendMonitor' => 'ZendMonitor', + ]); + + $loaded[$alias] = true; + if (class_exists($alias) || interface_exists($alias) || trait_exists($alias)) { + class_alias($alias, $class); + } + }; + } +} diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php b/bundled-libs/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php new file mode 100644 index 000000000..bac7b9747 --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php @@ -0,0 +1,434 @@ + true, + 'factories' => true, + 'invokables' => true, + 'services' => true, + ]; + + /** @var array String keys => string values */ + private $exactReplacements = [ + 'zend-expressive' => 'mezzio', + 'zf-apigility' => 'api-tools', + ]; + + /** @var Replacements */ + private $replacements; + + /** @var callable[] */ + private $rulesets; + + public function __construct() + { + $this->replacements = new Replacements(); + + /* Define the rulesets for replacements. + * + * Each ruleset has the following signature: + * + * @param mixed $value + * @param string[] $keys Full nested key hierarchy leading to the value + * @return null|callable + * + * If no match is made, a null is returned, allowing it to fallback to + * the next ruleset in the list. If a match is made, a callback is returned, + * and that will be used to perform the replacement on the value. + * + * The callback should have the following signature: + * + * @param mixed $value + * @param string[] $keys + * @return mixed The transformed value + */ + $this->rulesets = [ + // Exact values + function ($value) { + return is_string($value) && isset($this->exactReplacements[$value]) + ? [$this, 'replaceExactValue'] + : null; + }, + + // Router (MVC applications) + // We do not want to rewrite these. + function ($value, array $keys) { + $key = array_pop($keys); + // Only worried about a top-level "router" key. + return $key === 'router' && count($keys) === 0 && is_array($value) + ? [$this, 'noopReplacement'] + : null; + }, + + // service- and pluginmanager handling + function ($value) { + return is_array($value) && array_intersect_key(self::SERVICE_MANAGER_KEYS_OF_INTEREST, $value) !== [] + ? [$this, 'replaceDependencyConfiguration'] + : null; + }, + + // Array values + function ($value, array $keys) { + return 0 !== count($keys) && is_array($value) + ? [$this, '__invoke'] + : null; + }, + ]; + } + + /** + * @param string[] $keys Hierarchy of keys, for determining location in + * nested configuration. + * @return array + */ + public function __invoke(array $config, array $keys = []) + { + $rewritten = []; + + foreach ($config as $key => $value) { + // Determine new key from replacements + $newKey = is_string($key) ? $this->replace($key, $keys) : $key; + + // Keep original values with original key, if the key has changed, but only at the top-level. + if (empty($keys) && $newKey !== $key) { + $rewritten[$key] = $value; + } + + // Perform value replacements, if any + $newValue = $this->replace($value, $keys, $newKey); + + // Key does not already exist and/or is not an array value + if (! array_key_exists($newKey, $rewritten) || ! is_array($rewritten[$newKey])) { + // Do not overwrite existing values with null values + $rewritten[$newKey] = array_key_exists($newKey, $rewritten) && null === $newValue + ? $rewritten[$newKey] + : $newValue; + continue; + } + + // New value is null; nothing to do. + if (null === $newValue) { + continue; + } + + // Key already exists as an array value, but $value is not an array + if (! is_array($newValue)) { + $rewritten[$newKey][] = $newValue; + continue; + } + + // Key already exists as an array value, and $value is also an array + $rewritten[$newKey] = static::merge($rewritten[$newKey], $newValue); + } + + return $rewritten; + } + + /** + * Perform substitutions as needed on an individual value. + * + * The $key is provided to allow fine-grained selection of rewrite rules. + * + * @param mixed $value + * @param string[] $keys Key hierarchy + * @param null|int|string $key + * @return mixed + */ + private function replace($value, array $keys, $key = null) + { + // Add new key to the list of keys. + // We do not need to remove it later, as we are working on a copy of the array. + array_push($keys, $key); + + // Identify rewrite strategy and perform replacements + $rewriteRule = $this->replacementRuleMatch($value, $keys); + return $rewriteRule($value, $keys); + } + + /** + * Merge two arrays together. + * + * If an integer key exists in both arrays, the value from the second array + * will be appended to the first array. If both values are arrays, they are + * merged together, else the value of the second array overwrites the one + * of the first array. + * + * Based on zend-stdlib Zend\Stdlib\ArrayUtils::merge + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * + * @return array + */ + public static function merge(array $a, array $b) + { + foreach ($b as $key => $value) { + if (! isset($a[$key]) && ! array_key_exists($key, $a)) { + $a[$key] = $value; + continue; + } + + if (null === $value && array_key_exists($key, $a)) { + // Leave as-is if value from $b is null + continue; + } + + if (is_int($key)) { + $a[] = $value; + continue; + } + + if (is_array($value) && is_array($a[$key])) { + $a[$key] = static::merge($a[$key], $value); + continue; + } + + $a[$key] = $value; + } + + return $a; + } + + /** + * @param mixed $value + * @param null|int|string $key + * @return callable Callable to invoke with value + */ + private function replacementRuleMatch($value, $key = null) + { + foreach ($this->rulesets as $ruleset) { + $result = $ruleset($value, $key); + if (is_callable($result)) { + return $result; + } + } + return [$this, 'fallbackReplacement']; + } + + /** + * Replace a value using the translation table, if the value is a string. + * + * @param mixed $value + * @return mixed + */ + private function fallbackReplacement($value) + { + return is_string($value) + ? $this->replacements->replace($value) + : $value; + } + + /** + * Replace a value matched exactly. + * + * @param mixed $value + * @return mixed + */ + private function replaceExactValue($value) + { + return $this->exactReplacements[$value]; + } + + private function replaceDependencyConfiguration(array $config) + { + $aliases = isset($config['aliases']) && is_array($config['aliases']) + ? $this->replaceDependencyAliases($config['aliases']) + : []; + + if ($aliases) { + $config['aliases'] = $aliases; + } + + $config = $this->replaceDependencyInvokables($config); + $config = $this->replaceDependencyFactories($config); + $config = $this->replaceDependencyServices($config); + + $keys = self::SERVICE_MANAGER_KEYS_OF_INTEREST; + foreach ($config as $key => $data) { + if (isset($keys[$key])) { + continue; + } + + $config[$key] = is_array($data) ? $this->__invoke($data, [$key]) : $data; + } + + return $config; + } + + /** + * Rewrite dependency aliases array + * + * In this case, we want to keep the alias as-is, but rewrite the target. + * + * We need also provide an additional alias if the alias key is a legacy class. + * + * @return array + */ + private function replaceDependencyAliases(array $aliases) + { + foreach ($aliases as $alias => $target) { + if (! is_string($alias) || ! is_string($target)) { + continue; + } + + $newTarget = $this->replacements->replace($target); + $newAlias = $this->replacements->replace($alias); + + $notIn = [$newTarget]; + $name = $newTarget; + while (isset($aliases[$name])) { + $notIn[] = $aliases[$name]; + $name = $aliases[$name]; + } + + if ($newAlias === $alias && ! in_array($alias, $notIn, true)) { + $aliases[$alias] = $newTarget; + continue; + } + + if (isset($aliases[$newAlias])) { + continue; + } + + if (! in_array($newAlias, $notIn, true)) { + $aliases[$alias] = $newAlias; + $aliases[$newAlias] = $newTarget; + } + } + + return $aliases; + } + + /** + * Rewrite dependency invokables array + * + * In this case, we want to keep the alias as-is, but rewrite the target. + * + * We need also provide an additional alias if invokable is defined with + * an alias which is a legacy class. + * + * @return array + */ + private function replaceDependencyInvokables(array $config) + { + if (empty($config['invokables']) || ! is_array($config['invokables'])) { + return $config; + } + + foreach ($config['invokables'] as $alias => $target) { + if (! is_string($alias)) { + continue; + } + + $newTarget = $this->replacements->replace($target); + $newAlias = $this->replacements->replace($alias); + + if ($alias === $target || isset($config['aliases'][$newAlias])) { + $config['invokables'][$alias] = $newTarget; + continue; + } + + $config['invokables'][$newAlias] = $newTarget; + + if ($newAlias === $alias) { + continue; + } + + $config['aliases'][$alias] = $newAlias; + + unset($config['invokables'][$alias]); + } + + return $config; + } + + /** + * @param mixed $value + * @return mixed Returns $value verbatim. + */ + private function noopReplacement($value) + { + return $value; + } + + private function replaceDependencyFactories(array $config) + { + if (empty($config['factories']) || ! is_array($config['factories'])) { + return $config; + } + + foreach ($config['factories'] as $service => $factory) { + if (! is_string($service)) { + continue; + } + + $replacedService = $this->replacements->replace($service); + $factory = is_string($factory) ? $this->replacements->replace($factory) : $factory; + $config['factories'][$replacedService] = $factory; + + if ($replacedService === $service) { + continue; + } + + unset($config['factories'][$service]); + if (isset($config['aliases'][$service])) { + continue; + } + + $config['aliases'][$service] = $replacedService; + } + + return $config; + } + + private function replaceDependencyServices(array $config) + { + if (empty($config['services']) || ! is_array($config['services'])) { + return $config; + } + + foreach ($config['services'] as $service => $serviceInstance) { + if (! is_string($service)) { + continue; + } + + $replacedService = $this->replacements->replace($service); + $serviceInstance = is_array($serviceInstance) ? $this->__invoke($serviceInstance) : $serviceInstance; + + $config['services'][$replacedService] = $serviceInstance; + + if ($service === $replacedService) { + continue; + } + + unset($config['services'][$service]); + + if (isset($config['aliases'][$service])) { + continue; + } + + $config['aliases'][$service] = $replacedService; + } + + return $config; + } +} diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/src/Module.php b/bundled-libs/laminas/laminas-zendframework-bridge/src/Module.php new file mode 100644 index 000000000..d10cb43dd --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/src/Module.php @@ -0,0 +1,54 @@ +getEventManager() + ->attach('mergeConfig', [$this, 'onMergeConfig']); + } + + /** + * Perform substitutions in the merged configuration. + * + * Rewrites keys and values matching known ZF classes, namespaces, and + * configuration keys to their Laminas equivalents. + * + * Type-hinting deliberately omitted to allow unit testing + * without dependencies on packages that do not exist yet. + * + * @param ModuleEvent $event + */ + public function onMergeConfig($event) + { + /** @var ConfigMergerInterface */ + $configMerger = $event->getConfigListener(); + $processor = new ConfigPostProcessor(); + $configMerger->setMergedConfig( + $processor( + $configMerger->getMergedConfig($returnAsObject = false) + ) + ); + } +} diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/src/Replacements.php b/bundled-libs/laminas/laminas-zendframework-bridge/src/Replacements.php new file mode 100644 index 000000000..ca445c01f --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/src/Replacements.php @@ -0,0 +1,46 @@ +replacements = array_merge( + require __DIR__ . '/../config/replacements.php', + $additionalReplacements + ); + + // Provide multiple variants of strings containing namespace separators + foreach ($this->replacements as $original => $replacement) { + if (false === strpos($original, '\\')) { + continue; + } + $this->replacements[str_replace('\\', '\\\\', $original)] = str_replace('\\', '\\\\', $replacement); + $this->replacements[str_replace('\\', '\\\\\\\\', $original)] = str_replace('\\', '\\\\\\\\', $replacement); + } + } + + /** + * @param string $value + * @return string + */ + public function replace($value) + { + return strtr($value, $this->replacements); + } +} diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/src/RewriteRules.php b/bundled-libs/laminas/laminas-zendframework-bridge/src/RewriteRules.php new file mode 100644 index 000000000..8dc999f45 --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/src/RewriteRules.php @@ -0,0 +1,79 @@ + 'Mezzio\\ProblemDetails\\', + 'Zend\\Expressive\\' => 'Mezzio\\', + + // Laminas + 'Zend\\' => 'Laminas\\', + 'ZF\\ComposerAutoloading\\' => 'Laminas\\ComposerAutoloading\\', + 'ZF\\DevelopmentMode\\' => 'Laminas\\DevelopmentMode\\', + + // Apigility + 'ZF\\Apigility\\' => 'Laminas\\ApiTools\\', + 'ZF\\' => 'Laminas\\ApiTools\\', + + // ZendXml, API wrappers, zend-http OAuth support, zend-diagnostics, ZendDeveloperTools + 'ZendXml\\' => 'Laminas\\Xml\\', + 'ZendOAuth\\' => 'Laminas\\OAuth\\', + 'ZendDiagnostics\\' => 'Laminas\\Diagnostics\\', + 'ZendService\\ReCaptcha\\' => 'Laminas\\ReCaptcha\\', + 'ZendService\\Twitter\\' => 'Laminas\\Twitter\\', + 'ZendDeveloperTools\\' => 'Laminas\\DeveloperTools\\', + ]; + } + + /** + * @return array + */ + public static function namespaceReverse() + { + return [ + // ZendXml, ZendOAuth, ZendDiagnostics, ZendDeveloperTools + 'Laminas\\Xml\\' => 'ZendXml\\', + 'Laminas\\OAuth\\' => 'ZendOAuth\\', + 'Laminas\\Diagnostics\\' => 'ZendDiagnostics\\', + 'Laminas\\DeveloperTools\\' => 'ZendDeveloperTools\\', + + // Zend Service + 'Laminas\\ReCaptcha\\' => 'ZendService\\ReCaptcha\\', + 'Laminas\\Twitter\\' => 'ZendService\\Twitter\\', + + // Zend + 'Laminas\\' => 'Zend\\', + + // Expressive + 'Mezzio\\ProblemDetails\\' => 'Zend\\ProblemDetails\\', + 'Mezzio\\' => 'Zend\\Expressive\\', + + // Laminas to ZfCampus + 'Laminas\\ComposerAutoloading\\' => 'ZF\\ComposerAutoloading\\', + 'Laminas\\DevelopmentMode\\' => 'ZF\\DevelopmentMode\\', + + // Apigility + 'Laminas\\ApiTools\\Admin\\' => 'ZF\\Apigility\\Admin\\', + 'Laminas\\ApiTools\\Doctrine\\' => 'ZF\\Apigility\\Doctrine\\', + 'Laminas\\ApiTools\\Documentation\\' => 'ZF\\Apigility\\Documentation\\', + 'Laminas\\ApiTools\\Example\\' => 'ZF\\Apigility\\Example\\', + 'Laminas\\ApiTools\\Provider\\' => 'ZF\\Apigility\\Provider\\', + 'Laminas\\ApiTools\\Welcome\\' => 'ZF\\Apiglity\\Welcome\\', + 'Laminas\\ApiTools\\' => 'ZF\\', + ]; + } +} diff --git a/bundled-libs/laminas/laminas-zendframework-bridge/src/autoload.php b/bundled-libs/laminas/laminas-zendframework-bridge/src/autoload.php new file mode 100644 index 000000000..9f2f2adf8 --- /dev/null +++ b/bundled-libs/laminas/laminas-zendframework-bridge/src/autoload.php @@ -0,0 +1,9 @@ +. + +The first draft of this specification was initially written in 2014 by +Facebook, Inc. + +This specification is distributed without any warranty. +========================================= +END OF php/php-langspec NOTICES AND INFORMATION diff --git a/bundled-libs/microsoft/tolerant-php-parser/composer.json b/bundled-libs/microsoft/tolerant-php-parser/composer.json new file mode 100644 index 000000000..9579f11d4 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/composer.json @@ -0,0 +1,21 @@ +{ + "name": "microsoft/tolerant-php-parser", + "description": "Tolerant PHP-to-AST parser designed for IDE usage scenarios", + "type": "library", + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "license": "MIT", + "authors": [ + { + "name": "Rob Lourens", + "email": "roblou@microsoft.com" + } + ], + "autoload": { + "psr-4": { "Microsoft\\PhpParser\\": ["src/"] } + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/phpstan.neon b/bundled-libs/microsoft/tolerant-php-parser/phpstan.neon new file mode 100644 index 000000000..98fff91f3 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + level: 2 + paths: + - src/ + ignoreErrors: + # phpstan issue, see: https://github.com/phpstan/phpstan/issues/1306 + - "/Variable .unaryExpression might not be defined./" + - "/Variable .prefix might not be defined./" diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/CharacterCodes.php b/bundled-libs/microsoft/tolerant-php-parser/src/CharacterCodes.php new file mode 100644 index 000000000..c411ed60d --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/CharacterCodes.php @@ -0,0 +1,116 @@ + + const _hash = 0x23; // # + const _lessThan = 0x3C; // < + const _minus = 0x2D; // - + const _openBrace = 0x7B; // { + const _openBracket = 0x5B; // [ + const _openParen = 0x28; // ( + const _percent = 0x25; // % + const _plus = 0x2B; // + + const _question = 0x3F; // ? + const _semicolon = 0x3B; // ; + const _singleQuote = 0x27; // ' + const _slash = 0x2F; // / + const _tilde = 0x7E; // ~ + + const _backspace = 0x08; // \b + const _formFeed = 0x0C; // \f + const _byteOrderMark = 0xFEFF; + const _space = 0x20; + const _newline = 0x0A; // \n + const _return = 0x0D; // \r + const _tab = 0x09; // \t + const _verticalTab = 0x0B; // \v +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/ClassLike.php b/bundled-libs/microsoft/tolerant-php-parser/src/ClassLike.php new file mode 100644 index 000000000..08b7fa8d9 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/ClassLike.php @@ -0,0 +1,12 @@ +kind = $kind; + $this->message = $message; + $this->start = $start; + $this->length = $length; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/DiagnosticKind.php b/bundled-libs/microsoft/tolerant-php-parser/src/DiagnosticKind.php new file mode 100644 index 000000000..012db9d0a --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/DiagnosticKind.php @@ -0,0 +1,12 @@ +getDiagnosticForNode(); + } + return null; + } + + /** + * @param Token $token + * @return Diagnostic|null + */ + private static function checkDiagnosticForUnexpectedToken($token) { + if ($token instanceof SkippedToken) { + // TODO - consider also attaching parse context information to skipped tokens + // this would allow us to provide more helpful error messages that inform users what to do + // about the problem rather than simply pointing out the mistake. + return new Diagnostic( + DiagnosticKind::Error, + "Unexpected '" . + (self::$tokenKindToText[$token->kind] + ?? Token::getTokenKindNameFromValue($token->kind)) . + "'", + $token->start, + $token->getEndPosition() - $token->start + ); + } elseif ($token instanceof MissingToken) { + return new Diagnostic( + DiagnosticKind::Error, + "'" . + (self::$tokenKindToText[$token->kind] + ?? Token::getTokenKindNameFromValue($token->kind)) . + "' expected.", + $token->start, + $token->getEndPosition() - $token->start + ); + } + return null; + } + + /** + * Traverses AST to generate diagnostics. + * @param \Microsoft\PhpParser\Node $n + * @return Diagnostic[] + */ + public static function getDiagnostics(Node $n) : array { + $diagnostics = []; + + /** + * @param \Microsoft\PhpParser\Node|\Microsoft\PhpParser\Token $node + */ + $n->walkDescendantNodesAndTokens(function($node) use (&$diagnostics) { + if (($diagnostic = self::checkDiagnostics($node)) !== null) { + $diagnostics[] = $diagnostic; + } + }); + + return $diagnostics; + } +} + +DiagnosticsProvider::initTokenKindToText(); diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/FilePositionMap.php b/bundled-libs/microsoft/tolerant-php-parser/src/FilePositionMap.php new file mode 100644 index 000000000..1cb056bc9 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/FilePositionMap.php @@ -0,0 +1,142 @@ +currentOffset (updated whenever currentOffset is updated) */ + private $lineForCurrentOffset; + + public function __construct(string $file_contents) { + $this->fileContents = $file_contents; + $this->fileContentsLength = \strlen($file_contents); + $this->currentOffset = 0; + $this->lineForCurrentOffset = 1; + } + + /** + * @param Node $node the node to get the start line for. + * TODO deprecate and merge this and getTokenStartLine into getStartLine + * if https://github.com/Microsoft/tolerant-php-parser/issues/166 is fixed, + * (i.e. if there is a consistent way to get the start offset) + */ + public function getNodeStartLine(Node $node) : int { + return $this->getLineNumberForOffset($node->getStart()); + } + + /** + * @param Token $token the token to get the start line for. + */ + public function getTokenStartLine(Token $token) : int { + return $this->getLineNumberForOffset($token->start); + } + + /** + * @param Node|Token $node + */ + public function getStartLine($node) : int { + if ($node instanceof Token) { + $offset = $node->start; + } else { + $offset = $node->getStart(); + } + return $this->getLineNumberForOffset($offset); + } + + /** + * @param Node|Token $node + * Similar to getStartLine but includes the column + */ + public function getStartLineCharacterPositionForOffset($node) : LineCharacterPosition { + if ($node instanceof Token) { + $offset = $node->start; + } else { + $offset = $node->getStart(); + } + return $this->getLineCharacterPositionForOffset($offset); + } + + /** @param Node|Token $node */ + public function getEndLine($node) : int { + return $this->getLineNumberForOffset($node->getEndPosition()); + } + + /** + * @param Node|Token $node + * Similar to getStartLine but includes the column + */ + public function getEndLineCharacterPosition($node) : LineCharacterPosition { + return $this->getLineCharacterPositionForOffset($node->getEndPosition()); + } + + /** + * @param int $offset + * Similar to getStartLine but includes both the line and the column + */ + public function getLineCharacterPositionForOffset(int $offset) : LineCharacterPosition { + $line = $this->getLineNumberForOffset($offset); + $character = $this->getColumnForOffset($offset); + return new LineCharacterPosition($line, $character); + } + + /** + * @param int $offset - A 0-based byte offset + * @return int - gets the 1-based line number for $offset + */ + public function getLineNumberForOffset(int $offset) : int { + if ($offset < 0) { + $offset = 0; + } elseif ($offset > $this->fileContentsLength) { + $offset = $this->fileContentsLength; + } + $currentOffset = $this->currentOffset; + if ($offset > $currentOffset) { + $this->lineForCurrentOffset += \substr_count($this->fileContents, "\n", $currentOffset, $offset - $currentOffset); + $this->currentOffset = $offset; + } elseif ($offset < $currentOffset) { + $this->lineForCurrentOffset -= \substr_count($this->fileContents, "\n", $offset, $currentOffset - $offset); + $this->currentOffset = $offset; + } + return $this->lineForCurrentOffset; + } + + /** + * @param int $offset - A 0-based byte offset + * @return int - gets the 1-based column number for $offset + */ + public function getColumnForOffset(int $offset) : int { + $length = $this->fileContentsLength; + if ($offset <= 1) { + return 1; + } elseif ($offset > $length) { + $offset = $length; + } + // Postcondition: offset >= 1, ($lastNewlinePos < $offset) + // If there was no previous newline, lastNewlinePos = 0 + + // Start strrpos check from the character before the current character, + // in case the current character is a newline. + $lastNewlinePos = \strrpos($this->fileContents, "\n", -$length + $offset - 1); + return 1 + $offset - ($lastNewlinePos === false ? 0 : $lastNewlinePos + 1); + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/FunctionLike.php b/bundled-libs/microsoft/tolerant-php-parser/src/FunctionLike.php new file mode 100644 index 000000000..5ef91b520 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/FunctionLike.php @@ -0,0 +1,17 @@ +line = $line; + $this->character = $character; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/MissingToken.php b/bundled-libs/microsoft/tolerant-php-parser/src/MissingToken.php new file mode 100644 index 000000000..be3e6e040 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/MissingToken.php @@ -0,0 +1,20 @@ + $this->getTokenKindNameFromValue(TokenKind::MissingToken)], + parent::jsonSerialize() + ); + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/ModifiedTypeInterface.php b/bundled-libs/microsoft/tolerant-php-parser/src/ModifiedTypeInterface.php new file mode 100644 index 000000000..f04ca2385 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/ModifiedTypeInterface.php @@ -0,0 +1,16 @@ +modifiers === null) { + return false; + } + + foreach ($this->modifiers as $modifier) { + if ($modifier->kind === $targetModifier) { + return true; + } + } + + return false; + } + + /** + * Convenience method to check for the existence of the "public" modifier. + * Does not necessarily need to be defined for that type. + * + * @return bool + */ + public function isPublic(): bool { + return $this->hasModifier(TokenKind::PublicKeyword); + } + + /** + * Convenience method to check for the existence of the "static" modifier. + * Does not necessarily need to be defined for that type. + * + * @return bool + */ + public function isStatic(): bool { + return $this->hasModifier(TokenKind::StaticKeyword); + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/NamespacedNameInterface.php b/bundled-libs/microsoft/tolerant-php-parser/src/NamespacedNameInterface.php new file mode 100644 index 000000000..d4431d503 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/NamespacedNameInterface.php @@ -0,0 +1,11 @@ +getNamespaceDefinition(); + $content = $this->getFileContents(); + if ($namespaceDefinition === null) { + // global namespace -> strip namespace\ prefix + return ResolvedName::buildName($this->getNameParts(), $content); + } + + if ($namespaceDefinition->name !== null) { + $resolvedName = ResolvedName::buildName($namespaceDefinition->name->nameParts, $content); + } else { + $resolvedName = ResolvedName::buildName([], $content); + } + if ( + !($this instanceof QualifiedName && ( + ($this->parent instanceof NamespaceDefinition) || + ($this->parent instanceof NamespaceUseDeclaration) || + ($this->parent instanceof NamespaceUseClause) || + ($this->parent instanceof NamespaceUseGroupClause))) + ) { + $resolvedName->addNameParts($this->getNameParts(), $content); + } + return $resolvedName; + } +} \ No newline at end of file diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node.php new file mode 100644 index 000000000..4b87d417e --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node.php @@ -0,0 +1,698 @@ +getChildNodesAndTokens()->current(); + if ($child instanceof Node) { + return $child->getStart(); + } elseif ($child instanceof Token) { + return $child->start; + } + throw new \Exception("Unknown type in AST"); + } + + /** + * Gets start position of Node, including leading comments and whitespace + * @return int + * @throws \Exception + */ + public function getFullStart() : int { + foreach($this::CHILD_NAMES as $name) { + + if (($child = $this->$name) !== null) { + + if (\is_array($child)) { + if(!isset($child[0])) { + continue; + } + $child = $child[0]; + } + + if ($child instanceof Node) { + return $child->getFullStart(); + } + + if ($child instanceof Token) { + return $child->fullStart; + } + + throw new \Exception("Unknown type in AST: " . \gettype($child)); + } + }; + + throw new \RuntimeException("Could not resolve full start position"); + } + + /** + * Gets parent of current node (returns null if has no parent) + * @return null|Node + */ + public function getParent() { + return $this->parent; + } + + /** + * Gets first ancestor that is an instance of one of the provided classes. + * Returns null if there is no match. + * + * @param string ...$classNames + * @return Node|null + */ + public function getFirstAncestor(...$classNames) { + $ancestor = $this; + while (($ancestor = $ancestor->parent) !== null) { + foreach ($classNames as $className) { + if ($ancestor instanceof $className) { + return $ancestor; + } + } + } + return null; + } + + /** + * Gets first child that is an instance of one of the provided classes. + * Returns null if there is no match. + * + * @param array ...$classNames + * @return Node|null + */ + public function getFirstChildNode(...$classNames) { + foreach ($this::CHILD_NAMES as $name) { + $val = $this->$name; + foreach ($classNames as $className) { + if (\is_array($val)) { + foreach ($val as $child) { + if ($child instanceof $className) { + return $child; + } + } + continue; + } elseif ($val instanceof $className) { + return $val; + } + } + } + return null; + } + + /** + * Gets first descendant node that is an instance of one of the provided classes. + * Returns null if there is no match. + * + * @param array ...$classNames + * @return Node|null + */ + public function getFirstDescendantNode(...$classNames) { + foreach ($this->getDescendantNodes() as $descendant) { + foreach ($classNames as $className) { + if ($descendant instanceof $className) { + return $descendant; + } + } + } + return null; + } + + /** + * Gets root of the syntax tree (returns self if has no parents) + * @return SourceFileNode (expect root to be SourceFileNode unless the tree was manipulated) + */ + public function getRoot() : Node { + $node = $this; + while ($node->parent !== null) { + $node = $node->parent; + } + return $node; + } + + /** + * Gets generator containing all descendant Nodes and Tokens. + * + * @param callable|null $shouldDescendIntoChildrenFn + * @return \Generator|Node[]|Token[] + */ + public function getDescendantNodesAndTokens(callable $shouldDescendIntoChildrenFn = null) { + // TODO - write unit tests to prove invariants + // (concatenating all descendant Tokens should produce document, concatenating all Nodes should produce document) + foreach ($this->getChildNodesAndTokens() as $child) { + // Check possible types of $child, most frequent first + if ($child instanceof Node) { + yield $child; + if ($shouldDescendIntoChildrenFn === null || $shouldDescendIntoChildrenFn($child)) { + yield from $child->getDescendantNodesAndTokens($shouldDescendIntoChildrenFn); + } + } elseif ($child instanceof Token) { + yield $child; + } + } + } + + /** + * Iterate over all descendant Nodes and Tokens, calling $callback. + * This can often be faster than getDescendantNodesAndTokens + * if you just need to call something and don't need a generator. + * + * @param callable $callback a callback that accepts Node|Token + * @param callable|null $shouldDescendIntoChildrenFn + * @return void + */ + public function walkDescendantNodesAndTokens(callable $callback, callable $shouldDescendIntoChildrenFn = null) { + // TODO - write unit tests to prove invariants + // (concatenating all descendant Tokens should produce document, concatenating all Nodes should produce document) + foreach (static::CHILD_NAMES as $name) { + $child = $this->$name; + // Check possible types of $child, most frequent first + if ($child instanceof Token) { + $callback($child); + } elseif ($child instanceof Node) { + $callback($child); + if ($shouldDescendIntoChildrenFn === null || $shouldDescendIntoChildrenFn($child)) { + $child->walkDescendantNodesAndTokens($callback, $shouldDescendIntoChildrenFn); + } + } elseif (\is_array($child)) { + foreach ($child as $childElement) { + if ($childElement instanceof Token) { + $callback($childElement); + } elseif ($childElement instanceof Node) { + $callback($childElement); + if ($shouldDescendIntoChildrenFn === null || $shouldDescendIntoChildrenFn($childElement)) { + $childElement->walkDescendantNodesAndTokens($callback, $shouldDescendIntoChildrenFn); + } + } + } + } + } + } + + /** + * Gets a generator containing all descendant Nodes. + * @param callable|null $shouldDescendIntoChildrenFn + * @return \Generator|Node[] + */ + public function getDescendantNodes(callable $shouldDescendIntoChildrenFn = null) { + foreach ($this->getChildNodes() as $child) { + yield $child; + if ($shouldDescendIntoChildrenFn === null || $shouldDescendIntoChildrenFn($child)) { + yield from $child->getDescendantNodes($shouldDescendIntoChildrenFn); + } + } + } + + /** + * Gets generator containing all descendant Tokens. + * @param callable|null $shouldDescendIntoChildrenFn + * @return \Generator|Token[] + */ + public function getDescendantTokens(callable $shouldDescendIntoChildrenFn = null) { + foreach ($this->getChildNodesAndTokens() as $child) { + if ($child instanceof Node) { + if ($shouldDescendIntoChildrenFn == null || $shouldDescendIntoChildrenFn($child)) { + yield from $child->getDescendantTokens($shouldDescendIntoChildrenFn); + } + } elseif ($child instanceof Token) { + yield $child; + } + } + } + + /** + * Gets generator containing all child Nodes and Tokens (direct descendants). + * Does not return null elements. + * + * @return \Generator|Token[]|Node[] + */ + public function getChildNodesAndTokens() : \Generator { + foreach ($this::CHILD_NAMES as $name) { + $val = $this->$name; + + if (\is_array($val)) { + foreach ($val as $child) { + if ($child !== null) { + yield $name => $child; + } + } + continue; + } + if ($val !== null) { + yield $name => $val; + } + } + } + + /** + * Gets generator containing all child Nodes (direct descendants) + * @return \Generator|Node[] + */ + public function getChildNodes() : \Generator { + foreach ($this::CHILD_NAMES as $name) { + $val = $this->$name; + if (\is_array($val)) { + foreach ($val as $child) { + if ($child instanceof Node) { + yield $child; + } + } + continue; + } elseif ($val instanceof Node) { + yield $val; + } + } + } + + /** + * Gets generator containing all child Tokens (direct descendants) + * + * @return \Generator|Token[] + */ + public function getChildTokens() { + foreach ($this::CHILD_NAMES as $name) { + $val = $this->$name; + if (\is_array($val)) { + foreach ($val as $child) { + if ($child instanceof Token) { + yield $child; + } + } + continue; + } elseif ($val instanceof Token) { + yield $val; + } + } + } + + /** + * Gets array of declared child names (cached). + * + * This is used as an optimization when iterating over nodes: For direct iteration + * PHP will create a properties hashtable on the object, thus doubling memory usage. + * We avoid this by iterating over just the names instead. + * + * @return string[] + */ + public function getChildNames() { + return $this::CHILD_NAMES; + } + + /** + * Gets width of a Node (not including comment / whitespace trivia) + * + * @return int + */ + public function getWidth() : int { + $first = $this->getStart(); + $last = $this->getEndPosition(); + + return $last - $first; + } + + /** + * Gets width of a Node (including comment / whitespace trivia) + * + * @return int + */ + public function getFullWidth() : int { + $first = $this->getFullStart(); + $last = $this->getEndPosition(); + + return $last - $first; + } + + /** + * Gets string representing Node text (not including leading comment + whitespace trivia) + * @return string + */ + public function getText() : string { + $start = $this->getStart(); + $end = $this->getEndPosition(); + + $fileContents = $this->getFileContents(); + return \substr($fileContents, $start, $end - $start); + } + + /** + * Gets full text of Node (including leading comment + whitespace trivia) + * @return string + */ + public function getFullText() : string { + $start = $this->getFullStart(); + $end = $this->getEndPosition(); + + $fileContents = $this->getFileContents(); + return \substr($fileContents, $start, $end - $start); + + } + + /** + * Gets string representing Node's leading comment and whitespace text. + * @return string + */ + public function getLeadingCommentAndWhitespaceText() : string { + // TODO re-tokenize comments and whitespace + $fileContents = $this->getFileContents(); + foreach ($this->getDescendantTokens() as $token) { + return $token->getLeadingCommentsAndWhitespaceText($fileContents); + } + return ''; + } + + protected function getChildrenKvPairs() { + $result = array(); + foreach ($this::CHILD_NAMES as $name) { + $result[$name] = $this->$name; + } + return $result; + } + + public function jsonSerialize() { + $kindName = $this->getNodeKindName(); + return ["$kindName" => $this->getChildrenKvPairs()]; + } + + /** + * Get the end index of a Node. + * @return int + * @throws \Exception + */ + public function getEndPosition() { + // TODO test invariant - start of next node is end of previous node + for ($i = \count($childKeys = $this::CHILD_NAMES) - 1; $i >= 0; $i--) { + $lastChildKey = $childKeys[$i]; + $lastChild = $this->$lastChildKey; + + if (\is_array($lastChild)) { + $lastChild = \end($lastChild); + } + + if ($lastChild instanceof Token) { + return $lastChild->fullStart + $lastChild->length; + } elseif ($lastChild instanceof Node) { + return $lastChild->getEndPosition(); + } + } + + throw new \Exception("Unhandled node type"); + } + + public function getFileContents() : string { + // TODO consider renaming to getSourceText + return $this->getRoot()->fileContents; + } + + public function getUri() : string { + return $this->getRoot()->uri; + } + + public function getLastChild() { + $a = iterator_to_array($this->getChildNodesAndTokens()); + return \end($a); + } + + /** + * Searches descendants to find a Node at the given position. + * + * @param int $pos + * @return Node + */ + public function getDescendantNodeAtPosition(int $pos) { + foreach ($this->getChildNodes() as $child) { + if ($child->containsPosition($pos)) { + $node = $child->getDescendantNodeAtPosition($pos); + if (!is_null($node)) { + return $node; + } + } + } + + return $this; + } + + /** + * Returns true if the given Node or Token contains the given position. + * @param int $pos + * @return bool + */ + private function containsPosition(int $pos): bool { + return $this->getStart() <= $pos && $pos <= $this->getEndPosition(); + } + + /** + * Gets leading PHP Doc Comment text corresponding to the current Node. + * Returns last doc comment in leading comment / whitespace trivia, + * and returns null if there is no preceding doc comment. + * + * @return string|null + */ + public function getDocCommentText() { + $leadingTriviaText = $this->getLeadingCommentAndWhitespaceText(); + $leadingTriviaTokens = PhpTokenizer::getTokensArrayFromContent( + $leadingTriviaText, ParseContext::SourceElements, $this->getFullStart(), false + ); + for ($i = \count($leadingTriviaTokens) - 1; $i >= 0; $i--) { + $token = $leadingTriviaTokens[$i]; + if ($token->kind === TokenKind::DocCommentToken) { + return $token->getText($this->getFileContents()); + } + } + return null; + } + + public function __toString() { + return $this->getText(); + } + + /** + * @return array|ResolvedName[][] + * @throws \Exception + */ + public function getImportTablesForCurrentScope() { + $namespaceDefinition = $this->getNamespaceDefinition(); + + // Use declarations can exist in either the global scope, or inside namespace declarations. + // http://php.net/manual/en/language.namespaces.importing.php#language.namespaces.importing.scope + // + // The only code allowed before a namespace declaration is a declare statement, and sub-namespaces are + // additionally unaffected by by import rules of higher-level namespaces. Therefore, we can make the assumption + // that we need not travel up the spine any further once we've found the current namespace. + // http://php.net/manual/en/language.namespaces.definition.php + if ($namespaceDefinition instanceof NamespaceDefinition) { + $topLevelNamespaceStatements = $namespaceDefinition->compoundStatementOrSemicolon instanceof Token + ? $namespaceDefinition->parent->statementList // we need to start from the namespace definition. + : $namespaceDefinition->compoundStatementOrSemicolon->statements; + $namespaceFullStart = $namespaceDefinition->getFullStart(); + } else { + $topLevelNamespaceStatements = $this->getRoot()->statementList; + $namespaceFullStart = 0; + } + + $nodeFullStart = $this->getFullStart(); + + // TODO optimize performance + // Currently we rebuild the import tables on every call (and therefore every name resolution operation) + // It is likely that a consumer will attempt many consecutive name resolution requests within the same file. + // Therefore, we can consider optimizing on the basis of the "most recently used" import table set. + // The idea: Keep a single set of import tables cached based on a unique root node id, and invalidate + // cache whenever we attempt to resolve a qualified name with a different root node. + // + // In order to make this work, it will probably make sense to change the way we parse namespace definitions. + // https://github.com/Microsoft/tolerant-php-parser/issues/81 + // + // Currently the namespace definition only includes a compound statement or semicolon token as one if it's children. + // Instead, we should move to a model where we parse future statements as a child rather than as a separate + // statement. This would enable us to retrieve all the information we would need to find the fully qualified + // name by simply traveling up the spine to find the first ancestor of type NamespaceDefinition. + $namespaceImportTable = $functionImportTable = $constImportTable = []; + $contents = $this->getFileContents(); + + foreach ($topLevelNamespaceStatements as $useDeclaration) { + if ($useDeclaration->getFullStart() <= $namespaceFullStart) { + continue; + } + if ($useDeclaration->getFullStart() > $nodeFullStart) { + break; + } elseif (!($useDeclaration instanceof NamespaceUseDeclaration)) { + continue; + } + + // TODO fix getValues + foreach ((isset($useDeclaration->useClauses) ? $useDeclaration->useClauses->getValues() : []) as $useClause) { + $namespaceNamePartsPrefix = $useClause->namespaceName !== null ? $useClause->namespaceName->nameParts : []; + + if ($useClause->groupClauses !== null && $useClause instanceof NamespaceUseClause) { + // use A\B\C\{D\E}; namespace import: ["E" => [A,B,C,D,E]] + // use A\B\C\{D\E as F}; namespace import: ["F" => [A,B,C,D,E]] + // use function A\B\C\{A, B} function import: ["A" => [A,B,C,A], "B" => [A,B,C]] + // use function A\B\C\{const A} const import: ["A" => [A,B,C,A]] + foreach ($useClause->groupClauses->children as $groupClause) { + if (!($groupClause instanceof NamespaceUseGroupClause)) { + continue; + } + $namespaceNameParts = \array_merge($namespaceNamePartsPrefix, $groupClause->namespaceName->nameParts); + $functionOrConst = $groupClause->functionOrConst ?? $useDeclaration->functionOrConst; + $alias = $groupClause->namespaceAliasingClause === null + ? $groupClause->namespaceName->getLastNamePart()->getText($contents) + : $groupClause->namespaceAliasingClause->name->getText($contents); + + $this->addToImportTable( + $alias, $functionOrConst, $namespaceNameParts, $contents, + $namespaceImportTable, $functionImportTable, $constImportTable + ); + } + } else { + // use A\B\C; namespace import: ["C" => [A,B,C]] + // use A\B\C as D; namespace import: ["D" => [A,B,C]] + // use function A\B\C as D function import: ["D" => [A,B,C]] + // use A\B, C\D; namespace import: ["B" => [A,B], "D" => [C,D]] + $alias = $useClause->namespaceAliasingClause === null + ? $useClause->namespaceName->getLastNamePart()->getText($contents) + : $useClause->namespaceAliasingClause->name->getText($contents); + $functionOrConst = $useDeclaration->functionOrConst; + $namespaceNameParts = $namespaceNamePartsPrefix; + + $this->addToImportTable( + $alias, $functionOrConst, $namespaceNameParts, $contents, + $namespaceImportTable, $functionImportTable, $constImportTable + ); + } + } + } + + return [$namespaceImportTable, $functionImportTable, $constImportTable]; + } + + /** + * Gets corresponding NamespaceDefinition for Node. Returns null if in global namespace. + * + * @return NamespaceDefinition|null + */ + public function getNamespaceDefinition() { + $namespaceDefinition = $this instanceof NamespaceDefinition + ? $this + : $this->getFirstAncestor(NamespaceDefinition::class, SourceFileNode::class); + + if ($namespaceDefinition instanceof NamespaceDefinition && !($namespaceDefinition->parent instanceof SourceFileNode)) { + $namespaceDefinition = $namespaceDefinition->getFirstAncestor(SourceFileNode::class); + } + + if ($namespaceDefinition === null) { + // TODO provide a way to throw errors without crashing consumer + throw new \Exception("Invalid tree - SourceFileNode must always exist at root of tree."); + } + + $fullStart = $this->getFullStart(); + $lastNamespaceDefinition = null; + if ($namespaceDefinition instanceof SourceFileNode) { + foreach ($namespaceDefinition->getChildNodes() as $childNode) { + if ($childNode instanceof NamespaceDefinition && $childNode->getFullStart() < $fullStart) { + $lastNamespaceDefinition = $childNode; + } + } + } + + if ($lastNamespaceDefinition !== null && $lastNamespaceDefinition->compoundStatementOrSemicolon instanceof Token) { + $namespaceDefinition = $lastNamespaceDefinition; + } elseif ($namespaceDefinition instanceof SourceFileNode) { + $namespaceDefinition = null; + } + + return $namespaceDefinition; + } + + public function getPreviousSibling() { + // TODO make more efficient + $parent = $this->parent; + if ($parent === null) { + return null; + } + + $prevSibling = null; + + foreach ($parent::CHILD_NAMES as $name) { + $val = $parent->$name; + if (\is_array($val)) { + foreach ($val as $sibling) { + if ($sibling === $this) { + return $prevSibling; + } elseif ($sibling instanceof Node) { + $prevSibling = $sibling; + } + } + continue; + } elseif ($val instanceof Node) { + if ($val === $this) { + return $prevSibling; + } + $prevSibling = $val; + } + } + return null; + } + + /** + * Add the alias and resolved name to the corresponding namespace, function, or const import table. + * If the alias already exists, it will get replaced by the most recent using. + * + * TODO - worth throwing an error here in stead? + */ + private function addToImportTable($alias, $functionOrConst, $namespaceNameParts, $contents, & $namespaceImportTable, & $functionImportTable, & $constImportTable):array + { + if ($alias !== null) { + if ($functionOrConst === null) { + // namespaces are case-insensitive +// $alias = \strtolower($alias); + $namespaceImportTable[$alias] = ResolvedName::buildName($namespaceNameParts, $contents); + return array($namespaceImportTable, $functionImportTable, $constImportTable); + } elseif ($functionOrConst->kind === TokenKind::FunctionKeyword) { + // functions are case-insensitive +// $alias = \strtolower($alias); + $functionImportTable[$alias] = ResolvedName::buildName($namespaceNameParts, $contents); + return array($namespaceImportTable, $functionImportTable, $constImportTable); + } elseif ($functionOrConst->kind === TokenKind::ConstKeyword) { + // constants are case-sensitive + $constImportTable[$alias] = ResolvedName::buildName($namespaceNameParts, $contents); + return array($namespaceImportTable, $functionImportTable, $constImportTable); + } + return array($namespaceImportTable, $functionImportTable, $constImportTable); + } + return array($namespaceImportTable, $functionImportTable, $constImportTable); + } + + /** + * This is overridden in subclasses + * @return Diagnostic|null - Callers should use DiagnosticsProvider::getDiagnostics instead + * @internal + */ + public function getDiagnosticForNode() { + return null; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/AnonymousFunctionUseClause.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/AnonymousFunctionUseClause.php new file mode 100644 index 000000000..26460ea61 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/AnonymousFunctionUseClause.php @@ -0,0 +1,33 @@ +name]; + } + + public function getName() { + return $this->name->getText($this->getFileContents()); + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/DeclareDirective.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/DeclareDirective.php new file mode 100644 index 000000000..55e9af218 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/DeclareDirective.php @@ -0,0 +1,25 @@ +children as $child) { + if ($child instanceof Node) { + yield $child; + } elseif ($child instanceof Token && !\in_array($child->kind, self::DELIMITERS)) { + yield $child; + } + } + } + + public function getValues() { + foreach ($this->children as $idx=>$value) { + if ($idx % 2 == 0) { + yield $value; + } + } + } + + public function addElement($node) { + if ($node === null) { + return; + } + if ($this->children === null) { + $this->children = [$node]; + return; + } + $this->children[] = $node; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/DelimitedList/ArgumentExpressionList.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/DelimitedList/ArgumentExpressionList.php new file mode 100644 index 000000000..0815ee5f7 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/DelimitedList/ArgumentExpressionList.php @@ -0,0 +1,12 @@ +` */ + public $arrowToken; + + /** @var Node|Token */ + public $resultExpression; + + const CHILD_NAMES = [ + 'attributes', + 'staticModifier', + + // FunctionHeader + 'functionKeyword', + 'byRefToken', + 'name', + 'openParen', + 'parameters', + 'closeParen', + + // FunctionReturnType + 'colonToken', + 'questionToken', + 'returnType', + 'otherReturnTypes', + + // body + 'arrowToken', + 'resultExpression', + ]; +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/Expression/AssignmentExpression.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Expression/AssignmentExpression.php new file mode 100644 index 000000000..dc1683bcd --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Expression/AssignmentExpression.php @@ -0,0 +1,32 @@ +name instanceof Token && + $name = ltrim($this->name->getText($this->getFileContents()), '$') + ) { + return $name; + } + return null; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/Expression/YieldExpression.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Expression/YieldExpression.php new file mode 100644 index 000000000..63da29568 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Expression/YieldExpression.php @@ -0,0 +1,21 @@ +name->getText($this->getFileContents()); + } + + /** + * @return Diagnostic|null - Callers should use DiagnosticsProvider::getDiagnostics instead + * @internal + * @override + */ + public function getDiagnosticForNode() { + foreach ($this->modifiers as $modifier) { + if ($modifier->kind === TokenKind::VarKeyword) { + return new Diagnostic( + DiagnosticKind::Error, + "Unexpected modifier '" . DiagnosticsProvider::getTextForTokenKind($modifier->kind) . "'", + $modifier->start, + $modifier->length + ); + } + } + return null; + } + + /** + * Returns the signature parts as an array. Use $this::getSignatureFormatted for a user-friendly string version. + * + * @return array + */ + private function getSignatureParts(): array { + $parts = []; + + foreach ($this->getChildNodesAndTokens() as $i => $child) { + if ($i === "compoundStatementOrSemicolon") { + return $parts; + } + + $parts[] = $child instanceof Token + ? $child->getText($this->getFileContents()) + : $child->getText(); + }; + + return $parts; + } + + /** + * Returns the signature of the method as a formatted string. + * + * @return string + */ + public function getSignatureFormatted(): string { + $signature = implode(" ", $this->getSignatureParts()); + return $signature; + } + + /** + * Returns the description part of the doc string. + * + * @return string + */ + public function getDescriptionFormatted(): string { + $comment = trim($this->getLeadingCommentAndWhitespaceText(), "\r\n"); + $commentParts = explode("\n", $comment); + + $description = []; + + foreach ($commentParts as $i => $part) { + $part = trim($part, "*\r\t /"); + + if (strlen($part) <= 0) { + continue; + } + + if ($part[0] === "@") { + break; + } + + $description[] = $part; + } + + $descriptionFormatted = implode(" ", $description); + return $descriptionFormatted; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/MissingDeclaration.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/MissingDeclaration.php new file mode 100644 index 000000000..82867e257 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/MissingDeclaration.php @@ -0,0 +1,23 @@ +byRefToken !== null; + } + + public function getName() { + if ( + $this->variableName instanceof Token && + $name = substr($this->variableName->getText($this->getFileContents()), 1) + ) { + return $name; + } + return null; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/PropertyDeclaration.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/PropertyDeclaration.php new file mode 100644 index 000000000..603d4955a --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/PropertyDeclaration.php @@ -0,0 +1,47 @@ +globalSpecifier); + } + + /** + * Checks whether a QualifiedName begins with a "namespace" keyword + * @return bool + */ + public function isRelativeName() : bool { + return isset($this->relativeSpecifier); + } + + /** + * Checks whether a Name includes at least one namespace separator (and is neither fully-qualified nor relative) + * @return bool + */ + public function isQualifiedName() : bool { + return + !$this->isFullyQualifiedName() && + !$this->isRelativeName() && + \count($this->nameParts) > 1; // at least one namespace separator + } + + /** + * Checks whether a name is does not include a namespace separator. + * @return bool + */ + public function isUnqualifiedName() : bool { + return !($this->isFullyQualifiedName() || $this->isRelativeName() || $this->isQualifiedName()); + } + + /** + * Gets resolved name based on name resolution rules defined in: + * http://php.net/manual/en/language.namespaces.rules.php + * + * Returns null if one of the following conditions is met: + * - name resolution is not valid for this element (e.g. part of the name in a namespace definition) + * - name cannot be resolved (unqualified namespaced function/constant names that are not explicitly imported.) + * + * @return null|string|ResolvedName + * @throws \Exception + */ + public function getResolvedName($namespaceDefinition = null) { + // Name resolution not applicable to constructs that define symbol names or aliases. + if (($this->parent instanceof Node\Statement\NamespaceDefinition && $this->parent->name->getStart() === $this->getStart()) || + $this->parent instanceof Node\Statement\NamespaceUseDeclaration || + $this->parent instanceof Node\NamespaceUseClause || + $this->parent instanceof Node\NamespaceUseGroupClause || + $this->parent->parent instanceof Node\TraitUseClause || + $this->parent instanceof Node\TraitSelectOrAliasClause || + ($this->parent instanceof TraitSelectOrAliasClause && + ($this->parent->asOrInsteadOfKeyword == null || $this->parent->asOrInsteadOfKeyword->kind === TokenKind::AsKeyword)) + ) { + return null; + } + + if (in_array($lowerText = strtolower($this->getText()), ["self", "static", "parent"])) { + return $lowerText; + } + + // FULLY QUALIFIED NAMES + // - resolve to the name without leading namespace separator. + if ($this->isFullyQualifiedName()) { + return ResolvedName::buildName($this->nameParts, $this->getFileContents()); + } + + // RELATIVE NAMES + // - resolve to the name with namespace replaced by the current namespace. + // - if current namespace is global, strip leading namespace\ prefix. + if ($this->isRelativeName()) { + return $this->getNamespacedName(); + } + + list($namespaceImportTable, $functionImportTable, $constImportTable) = $this->getImportTablesForCurrentScope(); + + // QUALIFIED NAMES + // - first segment of the name is translated according to the current class/namespace import table. + // - If no import rule applies, the current namespace is prepended to the name. + if ($this->isQualifiedName()) { + return $this->tryResolveFromImportTable($namespaceImportTable) ?? $this->getNamespacedName(); + } + + // UNQUALIFIED NAMES + // - translated according to the current import table for the respective symbol type. + // (class-like => namespace import table, constant => const import table, function => function import table) + // - if no import rule applies: + // - all symbol types: if current namespace is global, resolve to global namespace. + // - class-like symbols: resolve from current namespace. + // - function or const: resolved at runtime (from current namespace, with fallback to global namespace). + if ($this->isConstantName()) { + $resolvedName = $this->tryResolveFromImportTable($constImportTable, /* case-sensitive */ true); + $namespaceDefinition = $this->getNamespaceDefinition(); + if ($namespaceDefinition !== null && $namespaceDefinition->name === null) { + $resolvedName = $resolvedName ?? ResolvedName::buildName($this->nameParts, $this->getFileContents()); + } + return $resolvedName; + } elseif ($this->parent instanceof CallExpression) { + $resolvedName = $this->tryResolveFromImportTable($functionImportTable); + if (($namespaceDefinition = $this->getNamespaceDefinition()) === null || $namespaceDefinition->name === null) { + $resolvedName = $resolvedName ?? ResolvedName::buildName($this->nameParts, $this->getFileContents()); + } + return $resolvedName; + } + + return $this->tryResolveFromImportTable($namespaceImportTable) ?? $this->getNamespacedName(); + } + + public function getLastNamePart() { + $parts = $this->nameParts; + for ($i = \count($parts) - 1; $i >= 0; $i--) { + // TODO - also handle reserved word tokens + if ($parts[$i]->kind === TokenKind::Name) { + return $parts[$i]; + } + } + return null; + } + + /** + * @param ResolvedName[] $importTable + * @param bool $isCaseSensitive + * @return string|null + */ + private function tryResolveFromImportTable($importTable, bool $isCaseSensitive = false) { + $content = $this->getFileContents(); + $index = $this->nameParts[0]->getText($content); +// if (!$isCaseSensitive) { +// $index = strtolower($index); +// } + if(isset($importTable[$index])) { + $resolvedName = $importTable[$index]; + $resolvedName->addNameParts(\array_slice($this->nameParts, 1), $content); + return $resolvedName; + } + return null; + } + + private function isConstantName() : bool { + return + ($this->parent instanceof Node\Statement\ExpressionStatement || $this->parent instanceof Expression) && + !( + $this->parent instanceof Node\Expression\MemberAccessExpression || $this->parent instanceof CallExpression || + $this->parent instanceof ObjectCreationExpression || + $this->parent instanceof Node\Expression\ScopedPropertyAccessExpression || $this->parent instanceof AnonymousFunctionCreationExpression || + $this->parent instanceof ArrowFunctionCreationExpression || + ($this->parent instanceof Node\Expression\BinaryExpression && $this->parent->operator->kind === TokenKind::InstanceOfKeyword) + ); + } + + public function getNameParts() : array { + return $this->nameParts; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/RelativeSpecifier.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/RelativeSpecifier.php new file mode 100644 index 000000000..8c301edba --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/RelativeSpecifier.php @@ -0,0 +1,23 @@ +breakoutLevel === null) { + return null; + } + + $breakoutLevel = $this->breakoutLevel; + while ($breakoutLevel instanceof Node\Expression\ParenthesizedExpression) { + $breakoutLevel = $breakoutLevel->expression; + } + + if ( + $breakoutLevel instanceof Node\NumericLiteral + && $breakoutLevel->children->kind === TokenKind::IntegerLiteralToken + ) { + $literalString = $breakoutLevel->getText(); + $firstTwoChars = \substr($literalString, 0, 2); + + if ($firstTwoChars === '0b' || $firstTwoChars === '0B') { + if (\bindec(\substr($literalString, 2)) > 0) { + return null; + } + } + else if (\intval($literalString, 0) > 0) { + return null; + } + } + + if ($breakoutLevel instanceof Token) { + $start = $breakoutLevel->getStartPosition(); + } + else { + $start = $breakoutLevel->getStart(); + } + $end = $breakoutLevel->getEndPosition(); + + return new Diagnostic( + DiagnosticKind::Error, + "Positive integer literal expected.", + $start, + $end - $start + ); + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/ClassDeclaration.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/ClassDeclaration.php new file mode 100644 index 000000000..8a4e6df06 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/ClassDeclaration.php @@ -0,0 +1,56 @@ +name]; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/CompoundStatementNode.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/CompoundStatementNode.php new file mode 100644 index 000000000..739e5b38c --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/CompoundStatementNode.php @@ -0,0 +1,28 @@ +name]; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/FunctionStaticDeclaration.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/FunctionStaticDeclaration.php new file mode 100644 index 000000000..b96608deb --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/FunctionStaticDeclaration.php @@ -0,0 +1,29 @@ +name]; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/NamedLabelStatement.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/NamedLabelStatement.php new file mode 100644 index 000000000..453ff61dd --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/NamedLabelStatement.php @@ -0,0 +1,28 @@ +useClauses != null + && \count($this->useClauses->children) > 1 + ) { + foreach ($this->useClauses->children as $useClause) { + if($useClause instanceof Node\NamespaceUseClause && !is_null($useClause->openBrace)) { + return new Diagnostic( + DiagnosticKind::Error, + "; expected.", + $useClause->getEndPosition(), + 1 + ); + } + } + } + return null; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/ReturnStatement.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/ReturnStatement.php new file mode 100644 index 000000000..50353c168 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/ReturnStatement.php @@ -0,0 +1,26 @@ +name]; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/TryStatement.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/TryStatement.php new file mode 100644 index 000000000..b0e503ca7 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/Statement/TryStatement.php @@ -0,0 +1,30 @@ +startQuote)) { + foreach ($this->children as $child) { + $contents = $this->getFileContents(); + $stringContents .= $child->getFullText($contents); + } + } else { + // TODO ensure string consistency (all strings should have start / end quote) + $stringContents = trim($this->children->getText($this->getFileContents()), '"\''); + } + return $stringContents; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Node/TraitMembers.php b/bundled-libs/microsoft/tolerant-php-parser/src/Node/TraitMembers.php new file mode 100644 index 000000000..53c375c11 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Node/TraitMembers.php @@ -0,0 +1,27 @@ +variableName instanceof Token && + $name = substr($this->variableName->getText($this->getFileContents()), 1) + ) { + return $name; + } + return null; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/ParseContext.php b/bundled-libs/microsoft/tolerant-php-parser/src/ParseContext.php new file mode 100644 index 000000000..99d9b50ef --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/ParseContext.php @@ -0,0 +1,23 @@ +reservedWordTokens = \array_values(TokenStringMaps::RESERVED_WORDS); + $this->keywordTokens = \array_values(TokenStringMaps::KEYWORDS); + $this->argumentStartTokensSet = \array_flip(TokenStringMaps::KEYWORDS); + unset($this->argumentStartTokensSet[TokenKind::YieldFromKeyword]); + $this->argumentStartTokensSet[TokenKind::DotDotDotToken] = '...'; + $this->nameOrKeywordOrReservedWordTokens = \array_merge([TokenKind::Name], $this->keywordTokens, $this->reservedWordTokens); + $this->nameOrReservedWordTokens = \array_merge([TokenKind::Name], $this->reservedWordTokens); + $this->nameOrStaticOrReservedWordTokens = \array_merge([TokenKind::Name, TokenKind::StaticKeyword], $this->reservedWordTokens); + $this->parameterTypeDeclarationTokens = + [TokenKind::ArrayKeyword, TokenKind::CallableKeyword, TokenKind::BoolReservedWord, + TokenKind::FloatReservedWord, TokenKind::IntReservedWord, TokenKind::StringReservedWord, + TokenKind::ObjectReservedWord, TokenKind::NullReservedWord, TokenKind::FalseReservedWord]; // TODO update spec + $this->returnTypeDeclarationTokens = \array_merge([TokenKind::VoidReservedWord, TokenKind::NullReservedWord, TokenKind::FalseReservedWord, TokenKind::StaticKeyword], $this->parameterTypeDeclarationTokens); + } + + /** + * This method exists so that it can be overridden in subclasses. + * Any subclass must return a token stream that is equivalent to the contents in $fileContents for this to work properly. + * + * Possible reasons for applications to override the lexer: + * + * - Imitate token stream of a newer/older PHP version (e.g. T_FN is only available in php 7.4) + * - Reuse the result of token_get_all to create a Node again. + * - Reuse the result of token_get_all in a different library. + */ + protected function makeLexer(string $fileContents): TokenStreamProviderInterface + { + return TokenStreamProviderFactory::GetTokenStreamProvider($fileContents); + } + + /** + * Generates AST from source file contents. Returns an instance of SourceFileNode, which is always the top-most + * Node-type of the tree. + * + * @param string $fileContents + * @return SourceFileNode + */ + public function parseSourceFile(string $fileContents, string $uri = null) : SourceFileNode { + $this->lexer = $this->makeLexer($fileContents); + + $this->reset(); + + $sourceFile = new SourceFileNode(); + $this->sourceFile = $sourceFile; + $sourceFile->fileContents = $fileContents; + $sourceFile->uri = $uri; + $sourceFile->statementList = array(); + if ($this->getCurrentToken()->kind !== TokenKind::EndOfFileToken) { + $inlineHTML = $this->parseInlineHtml($sourceFile); + $sourceFile->statementList[] = $inlineHTML; + if ($inlineHTML->echoStatement) { + $sourceFile->statementList[] = $inlineHTML->echoStatement; + $inlineHTML->echoStatement->parent = $sourceFile; + $inlineHTML->echoStatement = null; + } + } + $sourceFile->statementList = + \array_merge($sourceFile->statementList, $this->parseList($sourceFile, ParseContext::SourceElements)); + + $this->sourceFile->endOfFileToken = $this->eat1(TokenKind::EndOfFileToken); + $this->advanceToken(); + + $sourceFile->parent = null; + + return $sourceFile; + } + + private function reset() { + $this->advanceToken(); + + // Stores the current parse context, which includes the current and enclosing lists. + $this->currentParseContext = 0; + } + + /** + * Parse a list of elements for a given ParseContext until a list terminator associated + * with that ParseContext is reached. Additionally abort parsing when an element is reached + * that is invalid in the current context, but valid in an enclosing context. If an element + * is invalid in both current and enclosing contexts, generate a SkippedToken, and continue. + * @param Node $parentNode + * @param int $listParseContext + * @return array + */ + private function parseList($parentNode, int $listParseContext) { + $savedParseContext = $this->currentParseContext; + $this->currentParseContext |= 1 << $listParseContext; + $parseListElementFn = $this->getParseListElementFn($listParseContext); + + $nodeArray = array(); + while (!$this->isListTerminator($listParseContext)) { + if ($this->isValidListElement($listParseContext, $this->getCurrentToken())) { + $element = $parseListElementFn($parentNode); + $nodeArray[] = $element; + if ($element instanceof Node) { + $element->parent = $parentNode; + if ($element instanceof InlineHtml && $element->echoStatement) { + $nodeArray[] = $element->echoStatement; + $element->echoStatement->parent = $parentNode; + $element->echoStatement = null; + } + } + continue; + } + + // Error handling logic: + // The current parse context does not know how to handle the current token, + // so check if the enclosing contexts know what to do. If so, we assume that + // the list has completed parsing, and return to the enclosing context. + // + // Example: + // class A { + // function foo() { + // return; + // // } <- MissingToken (generated when we try to "eat" the closing brace) + // + // public function bar() { + // } + // } + // + // In the case above, the Method ParseContext doesn't know how to handle "public", but + // the Class ParseContext will know what to do with it. So we abort the Method ParseContext, + // and return to the Class ParseContext. This enables us to generate a tree with a single + // class that contains two method nodes, even though there was an error present in the first method. + if ($this->isCurrentTokenValidInEnclosingContexts()) { + break; + } + + // None of the enclosing contexts know how to handle the token. Generate a + // SkippedToken, and continue parsing in the current context. + // Example: + // class A { + // function foo() { + // return; + // & // <- SkippedToken + // } + // } + $token = new SkippedToken($this->getCurrentToken()); + $nodeArray[] = $token; + $this->advanceToken(); + } + + $this->currentParseContext = $savedParseContext; + + return $nodeArray; + } + + private function isListTerminator(int $parseContext) { + $tokenKind = $this->getCurrentToken()->kind; + if ($tokenKind === TokenKind::EndOfFileToken) { + // Being at the end of the file ends all lists. + return true; + } + + switch ($parseContext) { + case ParseContext::SourceElements: + return false; + + case ParseContext::InterfaceMembers: + case ParseContext::ClassMembers: + case ParseContext::BlockStatements: + case ParseContext::TraitMembers: + return $tokenKind === TokenKind::CloseBraceToken; + case ParseContext::SwitchStatementElements: + return $tokenKind === TokenKind::CloseBraceToken || $tokenKind === TokenKind::EndSwitchKeyword; + case ParseContext::IfClause2Elements: + return + $tokenKind === TokenKind::ElseIfKeyword || + $tokenKind === TokenKind::ElseKeyword || + $tokenKind === TokenKind::EndIfKeyword; + + case ParseContext::WhileStatementElements: + return $tokenKind === TokenKind::EndWhileKeyword; + + case ParseContext::CaseStatementElements: + return + $tokenKind === TokenKind::CaseKeyword || + $tokenKind === TokenKind::DefaultKeyword; + + case ParseContext::ForStatementElements: + return + $tokenKind === TokenKind::EndForKeyword; + + case ParseContext::ForeachStatementElements: + return $tokenKind === TokenKind::EndForEachKeyword; + + case ParseContext::DeclareStatementElements: + return $tokenKind === TokenKind::EndDeclareKeyword; + } + // TODO warn about unhandled parse context + return false; + } + + private function isValidListElement($context, Token $token) { + + // TODO + switch ($context) { + case ParseContext::SourceElements: + case ParseContext::BlockStatements: + case ParseContext::IfClause2Elements: + case ParseContext::CaseStatementElements: + case ParseContext::WhileStatementElements: + case ParseContext::ForStatementElements: + case ParseContext::ForeachStatementElements: + case ParseContext::DeclareStatementElements: + return $this->isStatementStart($token); + + case ParseContext::ClassMembers: + return $this->isClassMemberDeclarationStart($token); + + case ParseContext::TraitMembers: + return $this->isTraitMemberDeclarationStart($token); + + case ParseContext::InterfaceMembers: + return $this->isInterfaceMemberDeclarationStart($token); + + case ParseContext::SwitchStatementElements: + return + $token->kind === TokenKind::CaseKeyword || + $token->kind === TokenKind::DefaultKeyword; + } + return false; + } + + private function getParseListElementFn($context) { + switch ($context) { + case ParseContext::SourceElements: + case ParseContext::BlockStatements: + case ParseContext::IfClause2Elements: + case ParseContext::CaseStatementElements: + case ParseContext::WhileStatementElements: + case ParseContext::ForStatementElements: + case ParseContext::ForeachStatementElements: + case ParseContext::DeclareStatementElements: + return $this->parseStatementFn(); + case ParseContext::ClassMembers: + return $this->parseClassElementFn(); + + case ParseContext::TraitMembers: + return $this->parseTraitElementFn(); + + case ParseContext::InterfaceMembers: + return $this->parseInterfaceElementFn(); + + case ParseContext::SwitchStatementElements: + return $this->parseCaseOrDefaultStatement(); + default: + throw new \Exception("Unrecognized parse context"); + } + } + + /** + * Aborts parsing list when one of the parent contexts understands something + * @return bool + */ + private function isCurrentTokenValidInEnclosingContexts() { + for ($contextKind = 0; $contextKind < ParseContext::Count; $contextKind++) { + if ($this->isInParseContext($contextKind)) { + if ($this->isValidListElement($contextKind, $this->getCurrentToken()) || $this->isListTerminator($contextKind)) { + return true; + } + } + } + return false; + } + + private function isInParseContext($contextToCheck) { + return ($this->currentParseContext & (1 << $contextToCheck)); + } + + /** + * Retrieve the current token, and check that it's of the expected TokenKind. + * If so, advance and return the token. Otherwise return a MissingToken for + * the expected token. + * @param int|int[] ...$kinds + * @return Token + */ + private function eat(...$kinds) { + $token = $this->token; + if (\is_array($kinds[0])) { + $kinds = $kinds[0]; + } + foreach ($kinds as $kind) { + if ($token->kind === $kind) { + $this->token = $this->lexer->scanNextToken(); + return $token; + } + } + // TODO include optional grouping for token kinds + return new MissingToken($kinds[0], $token->fullStart); + } + + /** + * Retrieve the current token, and check that it's of the kind $kind. + * If so, advance and return the token. Otherwise return a MissingToken for + * the expected token. + * + * This is faster than calling eat() if there is a single token. + * + * @param int $kind + * @return Token + */ + private function eat1($kind) { + $token = $this->token; + if ($token->kind === $kind) { + $this->token = $this->lexer->scanNextToken(); + return $token; + } + // TODO include optional grouping for token kinds + return new MissingToken($kind, $token->fullStart); + } + + /** + * @param int|int[] ...$kinds (Can provide a single value with a list of kinds, or multiple kinds) + * @return Token|null + */ + private function eatOptional(...$kinds) { + $token = $this->token; + if (\is_array($kinds[0])) { + $kinds = $kinds[0]; + } + if (\in_array($token->kind, $kinds)) { + $this->token = $this->lexer->scanNextToken(); + return $token; + } + return null; + } + + /** + * @param int $kind a single kind + * @return Token|null + */ + private function eatOptional1($kind) { + $token = $this->token; + if ($token->kind === $kind) { + $this->token = $this->lexer->scanNextToken(); + return $token; + } + return null; + } + + private $token; + + private function getCurrentToken() : Token { + return $this->token; + } + + private function advanceToken() { + $this->token = $this->lexer->scanNextToken(); + } + + private function parseStatement($parentNode) { + return ($this->parseStatementFn())($parentNode); + } + + private function parseStatementFn() { + return function ($parentNode) { + $token = $this->getCurrentToken(); + switch ($token->kind) { + // compound-statement + case TokenKind::OpenBraceToken: + return $this->parseCompoundStatement($parentNode); + + // labeled-statement + case TokenKind::Name: + if ($this->lookahead(TokenKind::ColonToken)) { + return $this->parseNamedLabelStatement($parentNode); + } + break; + + // selection-statement + case TokenKind::IfKeyword: + return $this->parseIfStatement($parentNode); + case TokenKind::SwitchKeyword: + return $this->parseSwitchStatement($parentNode); + + // iteration-statement + case TokenKind::WhileKeyword: // while-statement + return $this->parseWhileStatement($parentNode); + case TokenKind::DoKeyword: // do-statement + return $this->parseDoStatement($parentNode); + case TokenKind::ForKeyword: // for-statement + return $this->parseForStatement($parentNode); + case TokenKind::ForeachKeyword: // foreach-statement + return $this->parseForeachStatement($parentNode); + + // jump-statement + case TokenKind::GotoKeyword: // goto-statement + return $this->parseGotoStatement($parentNode); + case TokenKind::ContinueKeyword: // continue-statement + case TokenKind::BreakKeyword: // break-statement + return $this->parseBreakOrContinueStatement($parentNode); + case TokenKind::ReturnKeyword: // return-statement + return $this->parseReturnStatement($parentNode); + case TokenKind::ThrowKeyword: // throw-statement + return $this->parseThrowStatement($parentNode); + + // try-statement + case TokenKind::TryKeyword: + return $this->parseTryStatement($parentNode); + + // declare-statement + case TokenKind::DeclareKeyword: + return $this->parseDeclareStatement($parentNode); + + // attribute before statement or anonymous function + case TokenKind::AttributeToken: + return $this->parseAttributeStatement($parentNode); + + // function-declaration + case TokenKind::FunctionKeyword: + // Check that this is not an anonymous-function-creation-expression + if ($this->lookahead($this->nameOrKeywordOrReservedWordTokens) || $this->lookahead(TokenKind::AmpersandToken, $this->nameOrKeywordOrReservedWordTokens)) { + return $this->parseFunctionDeclaration($parentNode); + } + break; + + // class-declaration + case TokenKind::FinalKeyword: + case TokenKind::AbstractKeyword: + if (!$this->lookahead(TokenKind::ClassKeyword)) { + $this->advanceToken(); + return new SkippedToken($token); + } + case TokenKind::ClassKeyword: + return $this->parseClassDeclaration($parentNode); + + // interface-declaration + case TokenKind::InterfaceKeyword: + return $this->parseInterfaceDeclaration($parentNode); + + // namespace-definition + case TokenKind::NamespaceKeyword: + if (!$this->lookahead(TokenKind::BackslashToken)) { + // TODO add error handling for the case where a namespace definition does not occur in the outer-most scope + return $this->parseNamespaceDefinition($parentNode); + } + break; + + // namespace-use-declaration + case TokenKind::UseKeyword: + return $this->parseNamespaceUseDeclaration($parentNode); + + case TokenKind::SemicolonToken: + return $this->parseEmptyStatement($parentNode); + + case TokenKind::EchoKeyword: + return $this->parseEchoStatement($parentNode); + + // trait-declaration + case TokenKind::TraitKeyword: + return $this->parseTraitDeclaration($parentNode); + + // global-declaration + case TokenKind::GlobalKeyword: + return $this->parseGlobalDeclaration($parentNode); + + // const-declaration + case TokenKind::ConstKeyword: + return $this->parseConstDeclaration($parentNode); + + // function-static-declaration + case TokenKind::StaticKeyword: + // Check that this is not an anonymous-function-creation-expression + if (!$this->lookahead([TokenKind::FunctionKeyword, TokenKind::FnKeyword, TokenKind::OpenParenToken, TokenKind::ColonColonToken])) { + return $this->parseFunctionStaticDeclaration($parentNode); + } + break; + + case TokenKind::ScriptSectionEndTag: + return $this->parseInlineHtml($parentNode); + + case TokenKind::UnsetKeyword: + return $this->parseUnsetStatement($parentNode); + } + + $expressionStatement = new ExpressionStatement(); + $expressionStatement->parent = $parentNode; + $expressionStatement->expression = $this->parseExpression($expressionStatement, true); + $expressionStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + return $expressionStatement; + }; + } + + private function parseClassElementFn() { + return function ($parentNode) { + $modifiers = $this->parseModifiers(); + + $token = $this->getCurrentToken(); + switch ($token->kind) { + case TokenKind::ConstKeyword: + return $this->parseClassConstDeclaration($parentNode, $modifiers); + + case TokenKind::FunctionKeyword: + return $this->parseMethodDeclaration($parentNode, $modifiers); + + case TokenKind::QuestionToken: + return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration( + $parentNode, + $modifiers, + $this->eat1(TokenKind::QuestionToken) + ); + case TokenKind::VariableName: + return $this->parsePropertyDeclaration($parentNode, $modifiers); + + case TokenKind::UseKeyword: + return $this->parseTraitUseClause($parentNode); + + case TokenKind::AttributeToken: + return $this->parseAttributeStatement($parentNode); + + default: + return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration($parentNode, $modifiers); + } + }; + } + + private function parseClassDeclaration($parentNode) : Node { + $classNode = new ClassDeclaration(); // TODO verify not nested + $classNode->parent = $parentNode; + $classNode->abstractOrFinalModifier = $this->eatOptional(TokenKind::AbstractKeyword, TokenKind::FinalKeyword); + $classNode->classKeyword = $this->eat1(TokenKind::ClassKeyword); + $classNode->name = $this->eat($this->nameOrReservedWordTokens); // TODO should be any + $classNode->name->kind = TokenKind::Name; + $classNode->classBaseClause = $this->parseClassBaseClause($classNode); + $classNode->classInterfaceClause = $this->parseClassInterfaceClause($classNode); + $classNode->classMembers = $this->parseClassMembers($classNode); + return $classNode; + } + + private function parseClassMembers($parentNode) : Node { + $classMembers = new ClassMembersNode(); + $classMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken); + $classMembers->classMemberDeclarations = $this->parseList($classMembers, ParseContext::ClassMembers); + $classMembers->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + $classMembers->parent = $parentNode; + return $classMembers; + } + + private function parseFunctionDeclaration($parentNode) { + $functionNode = new FunctionDeclaration(); + $this->parseFunctionType($functionNode); + $functionNode->parent = $parentNode; + return $functionNode; + } + + /** + * @return Node + */ + private function parseAttributeExpression($parentNode) { + $attributeGroups = $this->parseAttributeGroups(null); + // Warn about invalid syntax for attributed declarations + // Lookahead for static, function, or fn for the only type of expressions that can have attributes (anonymous functions) + if (in_array($this->token->kind, [TokenKind::FunctionKeyword, TokenKind::FnKeyword], true) || + $this->token->kind === TokenKind::StaticKeyword && $this->lookahead([TokenKind::FunctionKeyword, TokenKind::FnKeyword])) { + $expression = $this->parsePrimaryExpression($parentNode); + } else { + // Create a MissingToken so that diagnostics indicate that the attributes did not match up with an expression/declaration. + $expression = new MissingDeclaration(); + $expression->parent = $parentNode; + $expression->declaration = new MissingToken(TokenKind::Expression, $this->token->fullStart); + } + if ($expression instanceof AnonymousFunctionCreationExpression || + $expression instanceof ArrowFunctionCreationExpression || + $expression instanceof MissingDeclaration) { + $expression->attributes = $attributeGroups; + foreach ($attributeGroups as $attributeGroup) { + $attributeGroup->parent = $expression; + } + } + return $expression; + } + + /** + * Precondition: The next token is an AttributeToken + * @return Node + */ + private function parseAttributeStatement($parentNode) { + $attributeGroups = $this->parseAttributeGroups(null); + if ($parentNode instanceof ClassMembersNode) { + // Create a class element or a MissingMemberDeclaration + $statement = $this->parseClassElementFn()($parentNode); + } elseif ($parentNode instanceof TraitMembers) { + // Create a trait element or a MissingMemberDeclaration + $statement = $this->parseTraitElementFn()($parentNode); + } elseif ($parentNode instanceof InterfaceMembers) { + // Create an interface element or a MissingMemberDeclaration + $statement = $this->parseInterfaceElementFn()($parentNode); + } else { + // Classlikes, anonymous functions, global functions, and arrow functions can have attributes. Global constants cannot. + if (in_array($this->token->kind, [TokenKind::ClassKeyword, TokenKind::TraitKeyword, TokenKind::InterfaceKeyword, TokenKind::AbstractKeyword, TokenKind::FinalKeyword, TokenKind::FunctionKeyword, TokenKind::FnKeyword], true) || + $this->token->kind === TokenKind::StaticKeyword && $this->lookahead([TokenKind::FunctionKeyword, TokenKind::FnKeyword])) { + $statement = $this->parseStatement($parentNode); + } else { + // Create a MissingToken so that diagnostics indicate that the attributes did not match up with an expression/declaration. + $statement = new MissingDeclaration(); + $statement->parent = $parentNode; + $statement->declaration = new MissingToken(TokenKind::Expression, $this->token->fullStart); + } + } + + if ($statement instanceof FunctionLike || + $statement instanceof ClassDeclaration || + $statement instanceof TraitDeclaration || + $statement instanceof InterfaceDeclaration || + $statement instanceof ClassConstDeclaration || + $statement instanceof PropertyDeclaration || + $statement instanceof MissingDeclaration || + $statement instanceof MissingMemberDeclaration) { + + $statement->attributes = $attributeGroups; + foreach ($attributeGroups as $attributeGroup) { + $attributeGroup->parent = $statement; + } + } + return $statement; + } + + /** + * @param Node|null $parentNode + * @return AttributeGroup[] + */ + private function parseAttributeGroups($parentNode): array + { + $attributeGroups = []; + while ($attributeToken = $this->eatOptional1(TokenKind::AttributeToken)) { + $attributeGroup = new AttributeGroup(); + $attributeGroup->startToken = $attributeToken; + $attributeGroup->attributes = $this->parseAttributeElementList($attributeGroup) + ?: (new MissingToken(TokenKind::Name, $this->token->fullStart)); + $attributeGroup->endToken = $this->eat1(TokenKind::CloseBracketToken); + $attributeGroup->parent = $parentNode; + $attributeGroups[] = $attributeGroup; + } + return $attributeGroups; + } + + /** + * @return DelimitedList\AttributeElementList + */ + private function parseAttributeElementList(AttributeGroup $parentNode) { + return $this->parseDelimitedList( + DelimitedList\AttributeElementList::class, + TokenKind::CommaToken, + $this->isQualifiedNameStartFn(), + $this->parseAttributeFn(), + $parentNode, + false); + } + + private function parseAttributeFn() + { + return function ($parentNode): Attribute { + $attribute = new Attribute(); + $attribute->parent = $parentNode; + $attribute->name = $this->parseQualifiedName($attribute); + $attribute->openParen = $this->eatOptional1(TokenKind::OpenParenToken); + if ($attribute->openParen) { + $attribute->argumentExpressionList = $this->parseArgumentExpressionList($attribute); + $attribute->closeParen = $this->eat1(TokenKind::CloseParenToken); + } + return $attribute; + }; + } + + private function parseMethodDeclaration($parentNode, $modifiers) { + $methodDeclaration = new MethodDeclaration(); + $methodDeclaration->modifiers = $modifiers; + $this->parseFunctionType($methodDeclaration, true); + $methodDeclaration->parent = $parentNode; + return $methodDeclaration; + } + + private function parseParameterFn() { + return function ($parentNode) { + $parameter = new Parameter(); + $parameter->parent = $parentNode; + if ($this->token->kind === TokenKind::AttributeToken) { + $parameter->attributes = $this->parseAttributeGroups($parameter); + } + $parameter->visibilityToken = $this->eatOptional([TokenKind::PublicKeyword, TokenKind::ProtectedKeyword, TokenKind::PrivateKeyword]); + $parameter->questionToken = $this->eatOptional1(TokenKind::QuestionToken); + $typeDeclarationList = $this->tryParseParameterTypeDeclarationList($parameter); + if ($typeDeclarationList) { + $parameter->typeDeclaration = array_shift($typeDeclarationList->children); + $parameter->typeDeclaration->parent = $parameter; + if ($typeDeclarationList->children) { + $parameter->otherTypeDeclarations = $typeDeclarationList; + } + } + $parameter->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken); + // TODO add post-parse rule that prevents assignment + // TODO add post-parse rule that requires only last parameter be variadic + $parameter->dotDotDotToken = $this->eatOptional1(TokenKind::DotDotDotToken); + $parameter->variableName = $this->eat1(TokenKind::VariableName); + $parameter->equalsToken = $this->eatOptional1(TokenKind::EqualsToken); + if ($parameter->equalsToken !== null) { + // TODO add post-parse rule that checks for invalid assignments + $parameter->default = $this->parseExpression($parameter); + } + return $parameter; + }; + } + + /** + * @param ArrowFunctionCreationExpression|AnonymousFunctionCreationExpression|FunctionDeclaration|MethodDeclaration $parentNode a node with FunctionReturnType trait + */ + private function parseAndSetReturnTypeDeclarationList($parentNode) { + $returnTypeList = $this->parseReturnTypeDeclarationList($parentNode); + if (!$returnTypeList) { + $parentNode->returnType = new MissingToken(TokenKind::ReturnType, $this->token->fullStart); + return; + } + $returnType = array_shift($returnTypeList->children); + $parentNode->returnType = $returnType; + $returnType->parent = $parentNode; + if ($returnTypeList->children) { + $parentNode->otherReturnTypes = $returnTypeList; + } + } + + /** + * Attempt to parse the return type after the `:` and optional `?` token. + * + * @return DelimitedList\QualifiedNameList|null + */ + private function parseReturnTypeDeclarationList($parentNode) { + $result = $this->parseDelimitedList( + DelimitedList\QualifiedNameList::class, + TokenKind::BarToken, + function ($token) { + return \in_array($token->kind, $this->returnTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token); + }, + function ($parentNode) { + return $this->parseReturnTypeDeclaration($parentNode); + }, + $parentNode, + false); + + // Add a MissingToken so that this will warn about `function () : T| {}` + // TODO: Make this a reusable abstraction? + if ($result && (end($result->children)->kind ?? null) === TokenKind::BarToken) { + $result->children[] = new MissingToken(TokenKind::ReturnType, $this->token->fullStart); + } + return $result; + } + + private function parseReturnTypeDeclaration($parentNode) { + return $this->eatOptional($this->returnTypeDeclarationTokens) + ?? $this->parseQualifiedName($parentNode); + } + + private function tryParseParameterTypeDeclaration($parentNode) { + $parameterTypeDeclaration = + $this->eatOptional($this->parameterTypeDeclarationTokens) ?? $this->parseQualifiedName($parentNode); + return $parameterTypeDeclaration; + } + + /** + * @param Node $parentNode + * @return DelimitedList\QualifiedNameList|null + */ + private function tryParseParameterTypeDeclarationList($parentNode) { + $result = $this->parseDelimitedList( + DelimitedList\QualifiedNameList::class, + TokenKind::BarToken, + function ($token) { + return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token); + }, + function ($parentNode) { + return $this->tryParseParameterTypeDeclaration($parentNode); + }, + $parentNode, + true); + + // Add a MissingToken so that this will Warn about `function (T| $x) {}` + // TODO: Make this a reusable abstraction? + if ($result && (end($result->children)->kind ?? null) === TokenKind::BarToken) { + $result->children[] = new MissingToken(TokenKind::Name, $this->token->fullStart); + } + return $result; + } + + private function parseCompoundStatement($parentNode) { + $compoundStatement = new CompoundStatementNode(); + $compoundStatement->openBrace = $this->eat1(TokenKind::OpenBraceToken); + $compoundStatement->statements = $this->parseList($compoundStatement, ParseContext::BlockStatements); + $compoundStatement->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + $compoundStatement->parent = $parentNode; + return $compoundStatement; + } + + private function array_push_list(& $array, $list) { + foreach ($list as $item) { + $array[] = $item; + } + } + + private function isClassMemberDeclarationStart(Token $token) { + switch ($token->kind) { + // const-modifier + case TokenKind::ConstKeyword: + + // visibility-modifier + case TokenKind::PublicKeyword: + case TokenKind::ProtectedKeyword: + case TokenKind::PrivateKeyword: + + // static-modifier + case TokenKind::StaticKeyword: + + // class-modifier + case TokenKind::AbstractKeyword: + case TokenKind::FinalKeyword: + + case TokenKind::VarKeyword: + + case TokenKind::FunctionKeyword: + + case TokenKind::UseKeyword: + + // attributes + case TokenKind::AttributeToken: + return true; + + } + + return false; + } + + private function isStatementStart(Token $token) { + // https://github.com/php/php-langspec/blob/master/spec/19-grammar.md#statements + switch ($token->kind) { + // Compound Statements + case TokenKind::OpenBraceToken: + + // Labeled Statements + case TokenKind::Name: +// case TokenKind::CaseKeyword: // TODO update spec +// case TokenKind::DefaultKeyword: + + // Expression Statements + case TokenKind::SemicolonToken: + case TokenKind::IfKeyword: + case TokenKind::SwitchKeyword: + + // Iteration Statements + case TokenKind::WhileKeyword: + case TokenKind::DoKeyword: + case TokenKind::ForKeyword: + case TokenKind::ForeachKeyword: + + // Jump Statements + case TokenKind::GotoKeyword: + case TokenKind::ContinueKeyword: + case TokenKind::BreakKeyword: + case TokenKind::ReturnKeyword: + case TokenKind::ThrowKeyword: + + // The try Statement + case TokenKind::TryKeyword: + + // The declare Statement + case TokenKind::DeclareKeyword: + + // const-declaration + case TokenKind::ConstKeyword: + + // function-definition + case TokenKind::FunctionKeyword: + + // class-declaration + case TokenKind::ClassKeyword: + case TokenKind::AbstractKeyword: + case TokenKind::FinalKeyword: + + // interface-declaration + case TokenKind::InterfaceKeyword: + + // trait-declaration + case TokenKind::TraitKeyword: + + // namespace-definition + case TokenKind::NamespaceKeyword: + + // namespace-use-declaration + case TokenKind::UseKeyword: + + // global-declaration + case TokenKind::GlobalKeyword: + + // function-static-declaration + case TokenKind::StaticKeyword: + + case TokenKind::ScriptSectionEndTag: + + // attributes + case TokenKind::AttributeToken: + return true; + + default: + return $this->isExpressionStart($token); + } + } + + private function isExpressionStart($token) { + return ($this->isExpressionStartFn())($token); + } + + private function isExpressionStartFn() { + return function ($token) { + switch ($token->kind) { + // Script Inclusion Expression + case TokenKind::RequireKeyword: + case TokenKind::RequireOnceKeyword: + case TokenKind::IncludeKeyword: + case TokenKind::IncludeOnceKeyword: + + // yield-expression + case TokenKind::YieldKeyword: + case TokenKind::YieldFromKeyword: + + // object-creation-expression + case TokenKind::NewKeyword: + case TokenKind::CloneKeyword: + return true; + + // unary-op-expression + case TokenKind::PlusToken: + case TokenKind::MinusToken: + case TokenKind::ExclamationToken: + case TokenKind::TildeToken: + + // error-control-expression + case TokenKind::AtSymbolToken: + + // prefix-increment-expression + case TokenKind::PlusPlusToken: + // prefix-decrement-expression + case TokenKind::MinusMinusToken: + return true; + + // variable-name + case TokenKind::VariableName: + case TokenKind::DollarToken: + return true; + + // qualified-name + case TokenKind::Name: + case TokenKind::BackslashToken: + return true; + case TokenKind::NamespaceKeyword: + // TODO currently only supports qualified-names, but eventually parse namespace declarations + return $this->isNamespaceKeywordStartOfExpression($token); + + // literal + case TokenKind::DecimalLiteralToken: // TODO merge dec, oct, hex, bin, float -> NumericLiteral + case TokenKind::OctalLiteralToken: + case TokenKind::HexadecimalLiteralToken: + case TokenKind::BinaryLiteralToken: + case TokenKind::FloatingLiteralToken: + case TokenKind::InvalidOctalLiteralToken: + case TokenKind::InvalidHexadecimalLiteral: + case TokenKind::InvalidBinaryLiteral: + case TokenKind::IntegerLiteralToken: + + case TokenKind::StringLiteralToken: + + case TokenKind::SingleQuoteToken: + case TokenKind::DoubleQuoteToken: + case TokenKind::HeredocStart: + case TokenKind::BacktickToken: + + // array-creation-expression + case TokenKind::ArrayKeyword: + case TokenKind::OpenBracketToken: + + // intrinsic-construct + case TokenKind::EchoKeyword: + case TokenKind::ListKeyword: + case TokenKind::UnsetKeyword: + + // intrinsic-operator + case TokenKind::EmptyKeyword: + case TokenKind::EvalKeyword: + case TokenKind::ExitKeyword: + case TokenKind::DieKeyword: + case TokenKind::IsSetKeyword: + case TokenKind::PrintKeyword: + + // ( expression ) + case TokenKind::OpenParenToken: + case TokenKind::ArrayCastToken: + case TokenKind::BoolCastToken: + case TokenKind::DoubleCastToken: + case TokenKind::IntCastToken: + case TokenKind::ObjectCastToken: + case TokenKind::StringCastToken: + case TokenKind::UnsetCastToken: + case TokenKind::MatchKeyword: + + // anonymous-function-creation-expression + case TokenKind::StaticKeyword: + case TokenKind::FunctionKeyword: + case TokenKind::FnKeyword: + case TokenKind::AttributeToken: + return true; + } + return \in_array($token->kind, $this->reservedWordTokens, true); + }; + } + + /** + * Handles the fact that $token may either be getCurrentToken or the token immediately before it in isExpressionStartFn(). + * An expression can be namespace\CONST, namespace\fn(), or namespace\ClassName + */ + private function isNamespaceKeywordStartOfExpression(Token $token) : bool { + $nextToken = $this->getCurrentToken(); + if ($nextToken->kind === TokenKind::BackslashToken) { + return true; + } + if ($nextToken !== $token) { + return false; + } + $oldPosition = $this->lexer->getCurrentPosition(); + $nextToken = $this->lexer->scanNextToken(); + $this->lexer->setCurrentPosition($oldPosition); + return $nextToken->kind === TokenKind::BackslashToken; + } + + /** + * @param Node $parentNode + * @return Token|MissingToken|Node + */ + private function parsePrimaryExpression($parentNode) { + $token = $this->getCurrentToken(); + switch ($token->kind) { + // variable-name + case TokenKind::VariableName: // TODO special case $this + case TokenKind::DollarToken: + return $this->parseSimpleVariable($parentNode); + + // qualified-name + case TokenKind::Name: // TODO Qualified name + case TokenKind::BackslashToken: + case TokenKind::NamespaceKeyword: + return $this->parseQualifiedName($parentNode); + + case TokenKind::DecimalLiteralToken: // TODO merge dec, oct, hex, bin, float -> NumericLiteral + case TokenKind::OctalLiteralToken: + case TokenKind::HexadecimalLiteralToken: + case TokenKind::BinaryLiteralToken: + case TokenKind::FloatingLiteralToken: + case TokenKind::InvalidOctalLiteralToken: + case TokenKind::InvalidHexadecimalLiteral: + case TokenKind::InvalidBinaryLiteral: + case TokenKind::IntegerLiteralToken: + return $this->parseNumericLiteralExpression($parentNode); + + case TokenKind::StringLiteralToken: + return $this->parseStringLiteralExpression($parentNode); + + case TokenKind::DoubleQuoteToken: + case TokenKind::SingleQuoteToken: + case TokenKind::HeredocStart: + case TokenKind::BacktickToken: + return $this->parseStringLiteralExpression2($parentNode); + + // TODO constant-expression + + // array-creation-expression + case TokenKind::ArrayKeyword: + case TokenKind::OpenBracketToken: + return $this->parseArrayCreationExpression($parentNode); + + // intrinsic-construct + case TokenKind::ListKeyword: + return $this->parseListIntrinsicExpression($parentNode); + + // intrinsic-operator + case TokenKind::EmptyKeyword: + return $this->parseEmptyIntrinsicExpression($parentNode); + case TokenKind::EvalKeyword: + return $this->parseEvalIntrinsicExpression($parentNode); + + case TokenKind::ExitKeyword: + case TokenKind::DieKeyword: + return $this->parseExitIntrinsicExpression($parentNode); + + case TokenKind::IsSetKeyword: + return $this->parseIssetIntrinsicExpression($parentNode); + + case TokenKind::PrintKeyword: + return $this->parsePrintIntrinsicExpression($parentNode); + + // ( expression ) + case TokenKind::OpenParenToken: + return $this->parseParenthesizedExpression($parentNode); + + // anonymous-function-creation-expression + case TokenKind::AttributeToken: + return $this->parseAttributeExpression($parentNode); + + case TokenKind::StaticKeyword: + // handle `static::`, `static(`, `new static;`, `instanceof static` + if (!$this->lookahead([TokenKind::FunctionKeyword, TokenKind::FnKeyword])) { + // TODO: Should this check the parent type to reject `$x = static;`, `$x = static();`, etc. + return $this->parseStaticQualifiedName($parentNode); + } + // Could be `static function` anonymous function creation expression, so flow through + case TokenKind::FunctionKeyword: + case TokenKind::FnKeyword: + return $this->parseAnonymousFunctionCreationExpression($parentNode); + + case TokenKind::TrueReservedWord: + case TokenKind::FalseReservedWord: + case TokenKind::NullReservedWord: + // handle `true::`, `true(`, `true\` + if ($this->lookahead([TokenKind::BackslashToken, TokenKind::ColonColonToken, TokenKind::OpenParenToken])) { + return $this->parseQualifiedName($parentNode); + } + return $this->parseReservedWordExpression($parentNode); + case TokenKind::MatchKeyword: + return $this->parseMatchExpression($parentNode); + } + if (\in_array($token->kind, TokenStringMaps::RESERVED_WORDS)) { + return $this->parseQualifiedName($parentNode); + } + + return new MissingToken(TokenKind::Expression, $token->fullStart); + } + + private function parseEmptyStatement($parentNode) { + $emptyStatement = new EmptyStatement(); + $emptyStatement->parent = $parentNode; + $emptyStatement->semicolon = $this->eat1(TokenKind::SemicolonToken); + return $emptyStatement; + } + + private function parseStringLiteralExpression($parentNode) { + // TODO validate input token + $expression = new StringLiteral(); + $expression->parent = $parentNode; + $expression->children = $this->getCurrentToken(); // TODO - merge string types + $this->advanceToken(); + return $expression; + } + + private function parseStringLiteralExpression2($parentNode) { + // TODO validate input token + $expression = new StringLiteral(); + $expression->parent = $parentNode; + $expression->startQuote = $this->eat(TokenKind::SingleQuoteToken, TokenKind::DoubleQuoteToken, TokenKind::HeredocStart, TokenKind::BacktickToken); + $expression->children = array(); + + while (true) { + switch ($this->getCurrentToken()->kind) { + case TokenKind::DollarOpenBraceToken: + case TokenKind::OpenBraceDollarToken: + $expression->children[] = $this->eat(TokenKind::DollarOpenBraceToken, TokenKind::OpenBraceDollarToken); + // TODO: Reject ${var->prop} and ${(var->prop)} without rejecting ${var+otherVar} + // Currently, this fails to reject ${var->prop} (because `var` has TokenKind::Name instead of StringVarname) + if ($this->getCurrentToken()->kind === TokenKind::StringVarname) { + $expression->children[] = $this->parseComplexDollarTemplateStringExpression($expression); + } else { + $expression->children[] = $this->parseExpression($expression); + } + $expression->children[] = $this->eat1(TokenKind::CloseBraceToken); + break; + case $startQuoteKind = $expression->startQuote->kind: + case TokenKind::EndOfFileToken: + case TokenKind::HeredocEnd: + $expression->endQuote = $this->eat($startQuoteKind, TokenKind::HeredocEnd); + return $expression; + case TokenKind::VariableName: + $expression->children[] = $this->parseTemplateStringExpression($expression); + break; + default: + $expression->children[] = $this->getCurrentToken(); + $this->advanceToken(); + break; + } + } + } + + /** + * This is used to parse the contents of `"${...}"` expressions. + * + * Supported: x, x[0], x[$y] + * Not supported: $x->p1, x[0][1], etc. + * @see parseTemplateStringExpression + * + * Precondition: getCurrentToken()->kind === TokenKind::StringVarname + */ + private function parseComplexDollarTemplateStringExpression($parentNode) { + $var = $this->parseSimpleVariable($parentNode); + $token = $this->getCurrentToken(); + if ($token->kind === TokenKind::OpenBracketToken) { + return $this->parseTemplateStringSubscriptExpression($var); + } + return $var; + } + + /** + * Double-quoted and heredoc strings support a basic set of expression types, described in http://php.net/manual/en/language.types.string.php#language.types.string.parsing + * Supported: $x, $x->p, $x[0], $x[$y] + * Not supported: $x->p1->p2, $x[0][1], etc. + * Since there is a relatively small finite set of allowed forms, I implement it here rather than trying to reuse the general expression parsing code. + */ + private function parseTemplateStringExpression($parentNode) { + $token = $this->getCurrentToken(); + if ($token->kind === TokenKind::VariableName) { + $var = $this->parseSimpleVariable($parentNode); + $token = $this->getCurrentToken(); + if ($token->kind === TokenKind::OpenBracketToken) { + return $this->parseTemplateStringSubscriptExpression($var); + } else if ($token->kind === TokenKind::ArrowToken || $token->kind === TokenKind::QuestionArrowToken) { + return $this->parseTemplateStringMemberAccessExpression($var); + } else { + return $var; + } + } + + return null; + } + + private function parseTemplateStringSubscriptExpression($postfixExpression) : SubscriptExpression { + $subscriptExpression = new SubscriptExpression(); + $subscriptExpression->parent = $postfixExpression->parent; + $postfixExpression->parent = $subscriptExpression; + + $subscriptExpression->postfixExpression = $postfixExpression; + $subscriptExpression->openBracketOrBrace = $this->eat1(TokenKind::OpenBracketToken); // Only [] syntax is supported, not {} + $token = $this->getCurrentToken(); + if ($token->kind === TokenKind::VariableName) { + $subscriptExpression->accessExpression = $this->parseSimpleVariable($subscriptExpression); + } elseif ($token->kind === TokenKind::IntegerLiteralToken) { + $subscriptExpression->accessExpression = $this->parseNumericLiteralExpression($subscriptExpression); + } elseif ($token->kind === TokenKind::StringLiteralToken) { + // TODO: investigate if this should add other uncommon types of tokens for strings/numbers mentioned in parsePrimaryExpression() + $subscriptExpression->accessExpression = $this->parseStringLiteralExpression($subscriptExpression); + } elseif ($token->kind === TokenKind::Name) { + $subscriptExpression->accessExpression = $this->parseTemplateStringSubscriptStringLiteral($subscriptExpression); + } else { + $subscriptExpression->accessExpression = new MissingToken(TokenKind::Expression, $token->fullStart); + } + + $subscriptExpression->closeBracketOrBrace = $this->eat1(TokenKind::CloseBracketToken); + + return $subscriptExpression; + } + + private function parseTemplateStringSubscriptStringLiteral($parentNode) : StringLiteral { + $expression = new StringLiteral(); + $expression->parent = $parentNode; + $expression->children = $this->eat1(TokenKind::Name); + return $expression; + } + + private function parseTemplateStringMemberAccessExpression($expression) : MemberAccessExpression { + $memberAccessExpression = new MemberAccessExpression(); + $memberAccessExpression->parent = $expression->parent; + $expression->parent = $memberAccessExpression; + + $memberAccessExpression->dereferencableExpression = $expression; + $memberAccessExpression->arrowToken = $this->eat(TokenKind::ArrowToken, TokenKind::QuestionArrowToken); + $memberAccessExpression->memberName = $this->eat1(TokenKind::Name); + + return $memberAccessExpression; + } + + private function parseNumericLiteralExpression($parentNode) { + $numericLiteral = new NumericLiteral(); + $numericLiteral->parent = $parentNode; + $numericLiteral->children = $this->getCurrentToken(); + $this->advanceToken(); + return $numericLiteral; + } + + private function parseReservedWordExpression($parentNode) { + $reservedWord = new ReservedWord(); + $reservedWord->parent = $parentNode; + $reservedWord->children = $this->getCurrentToken(); + $this->advanceToken(); + return $reservedWord; + } + + private function isModifier($token) { + switch ($token->kind) { + // class-modifier + case TokenKind::AbstractKeyword: + case TokenKind::FinalKeyword: + + // visibility-modifier + case TokenKind::PublicKeyword: + case TokenKind::ProtectedKeyword: + case TokenKind::PrivateKeyword: + + // static-modifier + case TokenKind::StaticKeyword: + + // var + case TokenKind::VarKeyword: + return true; + } + return false; + } + + private function parseModifiers() { + $modifiers = array(); + $token = $this->getCurrentToken(); + while ($this->isModifier($token)) { + $modifiers[] = $token; + $this->advanceToken(); + $token = $this->getCurrentToken(); + } + return $modifiers; + } + + private function isParameterStartFn() { + return function ($token) { + switch ($token->kind) { + case TokenKind::DotDotDotToken: + + // qualified-name + case TokenKind::Name: // http://php.net/manual/en/language.namespaces.rules.php + case TokenKind::BackslashToken: + case TokenKind::NamespaceKeyword: + + case TokenKind::AmpersandToken: + + case TokenKind::VariableName: + + // nullable-type + case TokenKind::QuestionToken: + + // parameter promotion + case TokenKind::PublicKeyword: + case TokenKind::ProtectedKeyword: + case TokenKind::PrivateKeyword: + case TokenKind::AttributeToken: + return true; + } + + // scalar-type + return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true); + }; + } + + /** + * @param string $className (name of subclass of DelimitedList) + * @param int $delimiter + * @param callable $isElementStartFn + * @param callable $parseElementFn + * @param Node $parentNode + * @param bool $allowEmptyElements + * @return DelimitedList|null instance of $className + */ + private function parseDelimitedList($className, $delimiter, $isElementStartFn, $parseElementFn, $parentNode, $allowEmptyElements = false) { + // TODO consider allowing empty delimiter to be more tolerant + $node = new $className(); + $token = $this->getCurrentToken(); + do { + if ($isElementStartFn($token)) { + $node->addElement($parseElementFn($node)); + } elseif (!$allowEmptyElements || ($allowEmptyElements && !$this->checkToken($delimiter))) { + break; + } + + $delimiterToken = $this->eatOptional($delimiter); + if ($delimiterToken !== null) { + $node->addElement($delimiterToken); + } + $token = $this->getCurrentToken(); + // TODO ERROR CASE - no delimiter, but a param follows + } while ($delimiterToken !== null); + + + $node->parent = $parentNode; + if ($node->children === null) { + return null; + } + return $node; + } + + /** + * @internal + */ + const QUALIFIED_NAME_START_TOKENS = [ + TokenKind::BackslashToken, + TokenKind::NamespaceKeyword, + TokenKind::Name, + ]; + + private function isQualifiedNameStart($token) { + return \in_array($token->kind, self::QUALIFIED_NAME_START_TOKENS, true); + } + + private function isQualifiedNameStartFn() { + return function ($token) { + return \in_array($token->kind, self::QUALIFIED_NAME_START_TOKENS, true); + }; + } + + private function isQualifiedNameStartForCatchFn() { + return function ($token) { + // Unfortunately, catch(int $x) is *syntactically valid* php which `php --syntax-check` would accept. + // (tolerant-php-parser is concerned with syntax, not semantics) + return \in_array($token->kind, self::QUALIFIED_NAME_START_TOKENS, true) || + \in_array($token->kind, $this->reservedWordTokens, true); + }; + } + + /** + * @return QualifiedName + */ + private function parseStaticQualifiedName($parentNode) { + $node = new QualifiedName(); + $token = $this->eat(TokenKind::StaticKeyword); + $token->kind = TokenKind::Name; + $node->parent = $parentNode; + $node->nameParts = [$token]; + return $node; + } + + /** + * @return QualifiedName|null - returns null for invalid qualified names such as `static\` (use parseStaticQualifiedName for that) + */ + private function parseQualifiedName($parentNode) { + return ($this->parseQualifiedNameFn())($parentNode); + } + + private function parseQualifiedNameFn() { + return function ($parentNode) { + $node = new QualifiedName(); + $node->parent = $parentNode; + $node->relativeSpecifier = $this->parseRelativeSpecifier($node); + if (!isset($node->relativeSpecifier)) { + $node->globalSpecifier = $this->eatOptional1(TokenKind::BackslashToken); + } + + $nameParts = + $this->parseDelimitedList( + DelimitedList\QualifiedNameParts::class, + TokenKind::BackslashToken, + function ($token) { + // a\static() <- INVALID (but not checked for right now) + // new a\static() <- INVALID + // new static() <- VALID + // a\static\b <- INVALID + // a\function <- INVALID + // a\true\b <-VALID + // a\b\true <-VALID + // a\static::b <-VALID + // TODO more tests + return $this->lookahead(TokenKind::BackslashToken) + ? in_array($token->kind, $this->nameOrReservedWordTokens) + : in_array($token->kind, $this->nameOrStaticOrReservedWordTokens); + }, + function ($parentNode) { + $name = $this->lookahead(TokenKind::BackslashToken) + ? $this->eat($this->nameOrReservedWordTokens) + : $this->eat($this->nameOrStaticOrReservedWordTokens); // TODO support keyword name + $name->kind = TokenKind::Name; // bool/true/null/static should not be treated as keywords in this case + return $name; + }, $node); + if ($nameParts === null && $node->globalSpecifier === null && $node->relativeSpecifier === null) { + return null; + } + + $node->nameParts = $nameParts ? $nameParts->children : []; + + return $node; + }; + } + + private function parseRelativeSpecifier($parentNode) { + $node = new RelativeSpecifier(); + $node->parent = $parentNode; + $node->namespaceKeyword = $this->eatOptional1(TokenKind::NamespaceKeyword); + if ($node->namespaceKeyword !== null) { + $node->backslash = $this->eat1(TokenKind::BackslashToken); + } + if (isset($node->backslash)) { + return $node; + } + return null; + } + + /** + * @param MethodDeclaration|FunctionDeclaration|AnonymousFunctionCreationExpression $functionDeclaration + */ + private function parseFunctionType(Node $functionDeclaration, $canBeAbstract = false, $isAnonymous = false) { + + $functionDeclaration->functionKeyword = $this->eat1(TokenKind::FunctionKeyword); + $functionDeclaration->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken); + $functionDeclaration->name = $isAnonymous + ? $this->eatOptional($this->nameOrKeywordOrReservedWordTokens) + : $this->eat($this->nameOrKeywordOrReservedWordTokens); + + if (isset($functionDeclaration->name)) { + $functionDeclaration->name->kind = TokenKind::Name; + } + + if ($isAnonymous && isset($functionDeclaration->name)) { + // Anonymous functions should not have names + $functionDeclaration->name = new SkippedToken($functionDeclaration->name); // TODO instead handle this during post-walk + } + + $functionDeclaration->openParen = $this->eat1(TokenKind::OpenParenToken); + $functionDeclaration->parameters = $this->parseDelimitedList( + DelimitedList\ParameterDeclarationList::class, + TokenKind::CommaToken, + $this->isParameterStartFn(), + $this->parseParameterFn(), + $functionDeclaration); + $functionDeclaration->closeParen = $this->eat1(TokenKind::CloseParenToken); + if ($isAnonymous) { + $functionDeclaration->anonymousFunctionUseClause = $this->parseAnonymousFunctionUseClause($functionDeclaration); + } + + if ($this->checkToken(TokenKind::ColonToken)) { + $functionDeclaration->colonToken = $this->eat1(TokenKind::ColonToken); + $functionDeclaration->questionToken = $this->eatOptional1(TokenKind::QuestionToken); + $this->parseAndSetReturnTypeDeclarationList($functionDeclaration); + } + + if ($canBeAbstract) { + $functionDeclaration->compoundStatementOrSemicolon = $this->eatOptional1(TokenKind::SemicolonToken); + } + + if (!isset($functionDeclaration->compoundStatementOrSemicolon)) { + $functionDeclaration->compoundStatementOrSemicolon = $this->parseCompoundStatement($functionDeclaration); + } + } + + private function parseNamedLabelStatement($parentNode) { + $namedLabelStatement = new NamedLabelStatement(); + $namedLabelStatement->parent = $parentNode; + $namedLabelStatement->name = $this->eat1(TokenKind::Name); + $namedLabelStatement->colon = $this->eat1(TokenKind::ColonToken); + // A named label is a statement on its own. E.g. `while (false) label: echo "test";` + // is parsed as `while (false) { label: } echo "test"; + return $namedLabelStatement; + } + + /** + * @param int|int[] ...$expectedKinds an array of one or more kinds/sets of allowed kinds in each position + */ + private function lookahead(...$expectedKinds) : bool { + $startPos = $this->lexer->getCurrentPosition(); + $startToken = $this->token; + $succeeded = true; + foreach ($expectedKinds as $kind) { + $token = $this->lexer->scanNextToken(); + $currentPosition = $this->lexer->getCurrentPosition(); + $endOfFilePosition = $this->lexer->getEndOfFilePosition(); + if (\is_array($kind)) { + $succeeded = false; + foreach ($kind as $kindOption) { + if ($currentPosition <= $endOfFilePosition && $token->kind === $kindOption) { + $succeeded = true; + break; + } + } + } else { + if ($currentPosition > $endOfFilePosition || $token->kind !== $kind) { + $succeeded = false; + break; + } + } + } + $this->lexer->setCurrentPosition($startPos); + $this->token = $startToken; + return $succeeded; + } + + private function checkToken($expectedKind) : bool { + return $this->getCurrentToken()->kind === $expectedKind; + } + + private function parseIfStatement($parentNode) { + $ifStatement = new IfStatementNode(); + $ifStatement->parent = $parentNode; + $ifStatement->ifKeyword = $this->eat1(TokenKind::IfKeyword); + $ifStatement->openParen = $this->eat1(TokenKind::OpenParenToken); + $ifStatement->expression = $this->parseExpression($ifStatement); + $ifStatement->closeParen = $this->eat1(TokenKind::CloseParenToken); + $curTokenKind = $this->getCurrentToken()->kind; + if ($curTokenKind === TokenKind::ColonToken) { + $ifStatement->colon = $this->eat1(TokenKind::ColonToken); + $ifStatement->statements = $this->parseList($ifStatement, ParseContext::IfClause2Elements); + } else if ($curTokenKind !== TokenKind::ScriptSectionEndTag) { + // Fix #246 : properly parse `if (false) ?\>echoed text\statements = $this->parseStatement($ifStatement); + } + $ifStatement->elseIfClauses = []; // TODO - should be some standard for empty arrays vs. null? + while ($this->checkToken(TokenKind::ElseIfKeyword)) { + $ifStatement->elseIfClauses[] = $this->parseElseIfClause($ifStatement); + } + + if ($this->checkToken(TokenKind::ElseKeyword)) { + $ifStatement->elseClause = $this->parseElseClause($ifStatement); + } + + $ifStatement->endifKeyword = $this->eatOptional1(TokenKind::EndIfKeyword); + if ($ifStatement->endifKeyword) { + $ifStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + } + + return $ifStatement; + } + + private function parseElseIfClause($parentNode) { + $elseIfClause = new ElseIfClauseNode(); + $elseIfClause->parent = $parentNode; + $elseIfClause->elseIfKeyword = $this->eat1(TokenKind::ElseIfKeyword); + $elseIfClause->openParen = $this->eat1(TokenKind::OpenParenToken); + $elseIfClause->expression = $this->parseExpression($elseIfClause); + $elseIfClause->closeParen = $this->eat1(TokenKind::CloseParenToken); + $curTokenKind = $this->getCurrentToken()->kind; + if ($curTokenKind === TokenKind::ColonToken) { + $elseIfClause->colon = $this->eat1(TokenKind::ColonToken); + $elseIfClause->statements = $this->parseList($elseIfClause, ParseContext::IfClause2Elements); + } elseif ($curTokenKind !== TokenKind::ScriptSectionEndTag) { + $elseIfClause->statements = $this->parseStatement($elseIfClause); + } + return $elseIfClause; + } + + private function parseElseClause($parentNode) { + $elseClause = new ElseClauseNode(); + $elseClause->parent = $parentNode; + $elseClause->elseKeyword = $this->eat1(TokenKind::ElseKeyword); + $curTokenKind = $this->getCurrentToken()->kind; + if ($curTokenKind === TokenKind::ColonToken) { + $elseClause->colon = $this->eat1(TokenKind::ColonToken); + $elseClause->statements = $this->parseList($elseClause, ParseContext::IfClause2Elements); + } elseif ($curTokenKind !== TokenKind::ScriptSectionEndTag) { + $elseClause->statements = $this->parseStatement($elseClause); + } + return $elseClause; + } + + private function parseSwitchStatement($parentNode) { + $switchStatement = new SwitchStatementNode(); + $switchStatement->parent = $parentNode; + $switchStatement->switchKeyword = $this->eat1(TokenKind::SwitchKeyword); + $switchStatement->openParen = $this->eat1(TokenKind::OpenParenToken); + $switchStatement->expression = $this->parseExpression($switchStatement); + $switchStatement->closeParen = $this->eat1(TokenKind::CloseParenToken); + $switchStatement->openBrace = $this->eatOptional1(TokenKind::OpenBraceToken); + $switchStatement->colon = $this->eatOptional1(TokenKind::ColonToken); + $switchStatement->caseStatements = $this->parseList($switchStatement, ParseContext::SwitchStatementElements); + if ($switchStatement->colon !== null) { + $switchStatement->endswitch = $this->eat1(TokenKind::EndSwitchKeyword); + $switchStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + } else { + $switchStatement->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + } + + return $switchStatement; + } + + private function parseCaseOrDefaultStatement() { + return function ($parentNode) { + $caseStatement = new CaseStatementNode(); + $caseStatement->parent = $parentNode; + // TODO add error checking + $caseStatement->caseKeyword = $this->eat(TokenKind::CaseKeyword, TokenKind::DefaultKeyword); + if ($caseStatement->caseKeyword->kind === TokenKind::CaseKeyword) { + $caseStatement->expression = $this->parseExpression($caseStatement); + } + $caseStatement->defaultLabelTerminator = $this->eat(TokenKind::ColonToken, TokenKind::SemicolonToken); + $caseStatement->statementList = $this->parseList($caseStatement, ParseContext::CaseStatementElements); + return $caseStatement; + }; + } + + private function parseWhileStatement($parentNode) { + $whileStatement = new WhileStatement(); + $whileStatement->parent = $parentNode; + $whileStatement->whileToken = $this->eat1(TokenKind::WhileKeyword); + $whileStatement->openParen = $this->eat1(TokenKind::OpenParenToken); + $whileStatement->expression = $this->parseExpression($whileStatement); + $whileStatement->closeParen = $this->eat1(TokenKind::CloseParenToken); + $whileStatement->colon = $this->eatOptional1(TokenKind::ColonToken); + if ($whileStatement->colon !== null) { + $whileStatement->statements = $this->parseList($whileStatement, ParseContext::WhileStatementElements); + $whileStatement->endWhile = $this->eat1(TokenKind::EndWhileKeyword); + $whileStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + } elseif (!$this->checkToken(TokenKind::ScriptSectionEndTag)) { + $whileStatement->statements = $this->parseStatement($whileStatement); + } + return $whileStatement; + } + + /** + * @param Node $parentNode + * @param bool $force + * @return Node|MissingToken|array - The expression, or a missing token, or (if $force) an array containing a missed and skipped token + */ + private function parseExpression($parentNode, $force = false) { + $token = $this->getCurrentToken(); + if ($token->kind === TokenKind::EndOfFileToken) { + return new MissingToken(TokenKind::Expression, $token->fullStart); + } + + // Equivalent to (parseExpressionFn())($parentNode) + $expression = $this->parseBinaryExpressionOrHigher(0, $parentNode); + if ($force && $expression instanceof MissingToken) { + $expression = [$expression, new SkippedToken($token)]; + $this->advanceToken(); + } + + return $expression; + } + + private function parseExpressionFn() { + return function ($parentNode) { + return $this->parseBinaryExpressionOrHigher(0, $parentNode); + }; + } + + /** + * @param Node $parentNode + * @return Expression + */ + private function parseUnaryExpressionOrHigher($parentNode) { + $token = $this->getCurrentToken(); + switch ($token->kind) { + // unary-op-expression + case TokenKind::PlusToken: + case TokenKind::MinusToken: + case TokenKind::ExclamationToken: + case TokenKind::TildeToken: + return $this->parseUnaryOpExpression($parentNode); + + // error-control-expression + case TokenKind::AtSymbolToken: + return $this->parseErrorControlExpression($parentNode); + + // prefix-increment-expression + case TokenKind::PlusPlusToken: + // prefix-decrement-expression + case TokenKind::MinusMinusToken: + return $this->parsePrefixUpdateExpression($parentNode); + + case TokenKind::ArrayCastToken: + case TokenKind::BoolCastToken: + case TokenKind::DoubleCastToken: + case TokenKind::IntCastToken: + case TokenKind::ObjectCastToken: + case TokenKind::StringCastToken: + case TokenKind::UnsetCastToken: + return $this->parseCastExpression($parentNode); + + case TokenKind::OpenParenToken: + // TODO remove duplication + if ($this->lookahead( + [TokenKind::ArrayKeyword, + TokenKind::BinaryReservedWord, + TokenKind::BoolReservedWord, + TokenKind::BooleanReservedWord, + TokenKind::DoubleReservedWord, + TokenKind::IntReservedWord, + TokenKind::IntegerReservedWord, + TokenKind::FloatReservedWord, + TokenKind::ObjectReservedWord, + TokenKind::RealReservedWord, + TokenKind::StringReservedWord, + TokenKind::UnsetKeyword], TokenKind::CloseParenToken)) { + return $this->parseCastExpressionGranular($parentNode); + } + break; + +/* + + case TokenKind::BacktickToken: + return $this->parseShellCommandExpression($parentNode); + + case TokenKind::OpenParenToken: + // TODO +// return $this->parseCastExpressionGranular($parentNode); + break;*/ + + // object-creation-expression (postfix-expression) + case TokenKind::NewKeyword: + return $this->parseObjectCreationExpression($parentNode); + + // clone-expression (postfix-expression) + case TokenKind::CloneKeyword: + return $this->parseCloneExpression($parentNode); + + case TokenKind::YieldKeyword: + case TokenKind::YieldFromKeyword: + return $this->parseYieldExpression($parentNode); + + // include-expression + // include-once-expression + // require-expression + // require-once-expression + case TokenKind::IncludeKeyword: + case TokenKind::IncludeOnceKeyword: + case TokenKind::RequireKeyword: + case TokenKind::RequireOnceKeyword: + return $this->parseScriptInclusionExpression($parentNode); + case TokenKind::ThrowKeyword: // throw-statement will become an expression in php 8.0 + return $this->parseThrowExpression($parentNode); + } + + $expression = $this->parsePrimaryExpression($parentNode); + return $this->parsePostfixExpressionRest($expression); + } + + /** + * @param int $precedence + * @param Node $parentNode + * @return Expression + */ + private function parseBinaryExpressionOrHigher($precedence, $parentNode) { + $leftOperand = $this->parseUnaryExpressionOrHigher($parentNode); + + list($prevNewPrecedence, $prevAssociativity) = self::UNKNOWN_PRECEDENCE_AND_ASSOCIATIVITY; + + while (true) { + $token = $this->getCurrentToken(); + + list($newPrecedence, $associativity) = $this->getBinaryOperatorPrecedenceAndAssociativity($token); + + // Expressions using operators w/o associativity (equality, relational, instanceof) + // cannot reference identical expression types within one of their operands. + // + // Example: + // $a < $b < $c // CASE 1: INVALID + // $a < $b === $c < $d // CASE 2: VALID + // + // In CASE 1, it is expected that we stop parsing the expression after the $b token. + if ($prevAssociativity === Associativity::None && $prevNewPrecedence === $newPrecedence) { + break; + } + + // Precedence and associativity properties determine whether we recurse, and continue + // building up the current operand, or whether we pop out. + // + // Example: + // $a + $b + $c // CASE 1: additive-expression (left-associative) + // $a = $b = $c // CASE 2: equality-expression (right-associative) + // + // CASE 1: + // The additive-expression is left-associative, which means we expect the grouping to be: + // ($a + $b) + $c + // + // Because both + operators have the same precedence, and the + operator is left associative, + // we expect the second + operator NOT to be consumed because $newPrecedence > $precedence => FALSE + // + // CASE 2: + // The equality-expression is right-associative, which means we expect the grouping to be: + // $a = ($b = $c) + // + // Because both = operators have the same precedence, and the = operator is right-associative, + // we expect the second = operator to be consumed because $newPrecedence >= $precedence => TRUE + $shouldConsumeCurrentOperator = + $associativity === Associativity::Right ? + $newPrecedence >= $precedence: + $newPrecedence > $precedence; + + if (!$shouldConsumeCurrentOperator) { + break; + } + + // Unlike every other binary expression, exponentiation operators take precedence over unary operators. + // + // Example: + // -3**2 => -9 + // + // In these cases, we strip the UnaryExpression operator, and reassign $leftOperand to + // $unaryExpression->operand. + // + // After we finish building the BinaryExpression, we rebuild the UnaryExpression so that it includes + // the original operator, and the newly constructed exponentiation-expression as the operand. + $shouldOperatorTakePrecedenceOverUnary = false; + switch ($token->kind) { + case TokenKind::AsteriskAsteriskToken: + $shouldOperatorTakePrecedenceOverUnary = $leftOperand instanceof UnaryExpression; + break; + case TokenKind::EqualsToken: + case TokenKind::AsteriskAsteriskEqualsToken: + case TokenKind::AsteriskEqualsToken: + case TokenKind::SlashEqualsToken: + case TokenKind::PercentEqualsToken: + case TokenKind::PlusEqualsToken: + case TokenKind::MinusEqualsToken: + case TokenKind::DotEqualsToken: + case TokenKind::LessThanLessThanEqualsToken: + case TokenKind::GreaterThanGreaterThanEqualsToken: + case TokenKind::AmpersandEqualsToken: + case TokenKind::CaretEqualsToken: + case TokenKind::BarEqualsToken: + case TokenKind::QuestionQuestionEqualsToken: + // Workarounds for https://github.com/Microsoft/tolerant-php-parser/issues/19#issue-201714377 + // Parse `!$a = $b` as `!($a = $b)` - PHP constrains the Left Hand Side of an assignment to a variable. A unary operator (`@`, `!`, etc.) is not a variable. + // Instanceof has similar constraints for the LHS. + // So does `!$a += $b` + // TODO: Any other operators? + if ($leftOperand instanceof UnaryOpExpression) { + $shouldOperatorTakePrecedenceOverUnary = true; + } + break; + case TokenKind::InstanceOfKeyword: + // Unlike assignment, the instanceof operator doesn't have restrictions on what can go in the left hand side. + // `!` is the only unary operator with lower precedence than instanceof. + if ($leftOperand instanceof UnaryOpExpression) { + if ($leftOperand->operator->kind === TokenKind::ExclamationToken) { + $shouldOperatorTakePrecedenceOverUnary = true; + } + } + break; + case TokenKind::QuestionToken: + if ($parentNode instanceof TernaryExpression) { + // Workaround to parse "a ? b : c ? d : e" as "(a ? b : c) ? d : e" + break 2; + } + break; + } + + if ($shouldOperatorTakePrecedenceOverUnary) { + $unaryExpression = $leftOperand; + $leftOperand = $unaryExpression->operand; + } + + $this->advanceToken(); + + if ($token->kind === TokenKind::EqualsToken) { + $byRefToken = $this->eatOptional1(TokenKind::AmpersandToken); + } + + $leftOperand = $token->kind === TokenKind::QuestionToken ? + $this->parseTernaryExpression($leftOperand, $token, $parentNode) : + $this->makeBinaryExpression( + $leftOperand, + $token, + $byRefToken ?? null, + $this->parseBinaryExpressionOrHigher($newPrecedence, null), + $parentNode); + + // Rebuild the unary expression if we deconstructed it earlier. + if ($shouldOperatorTakePrecedenceOverUnary) { + $leftOperand->parent = $unaryExpression; + $unaryExpression->operand = $leftOperand; + $leftOperand = $unaryExpression; + } + + // Hold onto these values, so we know whether we've hit duplicate non-associative operators, + // and need to terminate early. + $prevNewPrecedence = $newPrecedence; + $prevAssociativity = $associativity; + } + return $leftOperand; + } + + const OPERATOR_PRECEDENCE_AND_ASSOCIATIVITY = + [ + // logical-inc-OR-expression-2 (L) + TokenKind::OrKeyword => [6, Associativity::Left], + + // logical-exc-OR-expression-2 (L) + TokenKind::XorKeyword=> [7, Associativity::Left], + + // logical-AND-expression-2 (L) + TokenKind::AndKeyword=> [8, Associativity::Left], + + // simple-assignment-expression (R) + // TODO byref-assignment-expression + TokenKind::EqualsToken => [9, Associativity::Right], + + // compound-assignment-expression (R) + TokenKind::AsteriskAsteriskEqualsToken => [9, Associativity::Right], + TokenKind::AsteriskEqualsToken => [9, Associativity::Right], + TokenKind::SlashEqualsToken => [9, Associativity::Right], + TokenKind::PercentEqualsToken => [9, Associativity::Right], + TokenKind::PlusEqualsToken => [9, Associativity::Right], + TokenKind::MinusEqualsToken => [9, Associativity::Right], + TokenKind::DotEqualsToken => [9, Associativity::Right], + TokenKind::LessThanLessThanEqualsToken => [9, Associativity::Right], + TokenKind::GreaterThanGreaterThanEqualsToken => [9, Associativity::Right], + TokenKind::AmpersandEqualsToken => [9, Associativity::Right], + TokenKind::CaretEqualsToken => [9, Associativity::Right], + TokenKind::BarEqualsToken => [9, Associativity::Right], + TokenKind::QuestionQuestionEqualsToken => [9, Associativity::Right], + + // TODO conditional-expression (L) + TokenKind::QuestionToken => [10, Associativity::Left], +// TokenKind::ColonToken => [9, Associativity::Left], + + // TODO coalesce-expression (R) + TokenKind::QuestionQuestionToken => [9, Associativity::Right], + + //logical-inc-OR-expression-1 (L) + TokenKind::BarBarToken => [12, Associativity::Left], + + // logical-AND-expression-1 (L) + TokenKind::AmpersandAmpersandToken => [13, Associativity::Left], + + // bitwise-inc-OR-expression (L) + TokenKind::BarToken => [14, Associativity::Left], + + // bitwise-exc-OR-expression (L) + TokenKind::CaretToken => [15, Associativity::Left], + + // bitwise-AND-expression (L) + TokenKind::AmpersandToken => [16, Associativity::Left], + + // equality-expression (X) + TokenKind::EqualsEqualsToken => [17, Associativity::None], + TokenKind::ExclamationEqualsToken => [17, Associativity::None], + TokenKind::LessThanGreaterThanToken => [17, Associativity::None], + TokenKind::EqualsEqualsEqualsToken => [17, Associativity::None], + TokenKind::ExclamationEqualsEqualsToken => [17, Associativity::None], + TokenKind::LessThanEqualsGreaterThanToken => [17, Associativity::None], + + // relational-expression (X) + TokenKind::LessThanToken => [18, Associativity::None], + TokenKind::GreaterThanToken => [18, Associativity::None], + TokenKind::LessThanEqualsToken => [18, Associativity::None], + TokenKind::GreaterThanEqualsToken => [18, Associativity::None], + + // shift-expression (L) + TokenKind::LessThanLessThanToken => [19, Associativity::Left], + TokenKind::GreaterThanGreaterThanToken => [19, Associativity::Left], + + // additive-expression (L) + TokenKind::PlusToken => [20, Associativity::Left], + TokenKind::MinusToken => [20, Associativity::Left], + TokenKind::DotToken =>[20, Associativity::Left], + + // multiplicative-expression (L) + TokenKind::AsteriskToken => [21, Associativity::Left], + TokenKind::SlashToken => [21, Associativity::Left], + TokenKind::PercentToken => [21, Associativity::Left], + + // instanceof-expression (X) + TokenKind::InstanceOfKeyword => [22, Associativity::None], + + // exponentiation-expression (R) + TokenKind::AsteriskAsteriskToken => [23, Associativity::Right] + ]; + + const UNKNOWN_PRECEDENCE_AND_ASSOCIATIVITY = [-1, -1]; + + private function getBinaryOperatorPrecedenceAndAssociativity($token) { + return self::OPERATOR_PRECEDENCE_AND_ASSOCIATIVITY[$token->kind] ?? self::UNKNOWN_PRECEDENCE_AND_ASSOCIATIVITY; + } + + /** + * @internal Do not use outside this class, this may be changed or removed. + */ + const KNOWN_ASSIGNMENT_TOKEN_SET = [ + TokenKind::AsteriskAsteriskEqualsToken => true, + TokenKind::AsteriskEqualsToken => true, + TokenKind::SlashEqualsToken => true, + TokenKind::PercentEqualsToken => true, + TokenKind::PlusEqualsToken => true, + TokenKind::MinusEqualsToken => true, + TokenKind::DotEqualsToken => true, + TokenKind::LessThanLessThanEqualsToken => true, + TokenKind::GreaterThanGreaterThanEqualsToken => true, + TokenKind::AmpersandEqualsToken => true, + TokenKind::CaretEqualsToken => true, + TokenKind::BarEqualsToken => true, + TokenKind::QuestionQuestionEqualsToken => true, + // InstanceOf has other remaining issues, but this heuristic is an improvement for many common cases such as `$x && $y = $z` + ]; + + private function makeBinaryExpression($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) { + $assignmentExpression = $operatorToken->kind === TokenKind::EqualsToken; + if ($assignmentExpression || \array_key_exists($operatorToken->kind, self::KNOWN_ASSIGNMENT_TOKEN_SET)) { + if ($leftOperand instanceof BinaryExpression) { + if (!\array_key_exists($leftOperand->operator->kind, self::KNOWN_ASSIGNMENT_TOKEN_SET)) { + // Handle cases without parenthesis, such as $x ** $y === $z, as $x ** ($y === $z) + return $this->shiftBinaryOperands($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode); + } + } elseif ($leftOperand instanceof UnaryOpExpression || $leftOperand instanceof ErrorControlExpression) { + return $this->shiftUnaryOperands($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode); + } + } + $binaryExpression = $assignmentExpression ? new AssignmentExpression() : new BinaryExpression(); + $binaryExpression->parent = $parentNode; + $leftOperand->parent = $binaryExpression; + $rightOperand->parent = $binaryExpression; + $binaryExpression->leftOperand = $leftOperand; + $binaryExpression->operator = $operatorToken; + if ($binaryExpression instanceof AssignmentExpression && isset($byRefToken)) { + $binaryExpression->byRef = $byRefToken; + } + $binaryExpression->rightOperand = $rightOperand; + return $binaryExpression; + } + + /** + * @param ErrorControlExpression|UnaryOpExpression $leftOperand + */ + private function shiftUnaryOperands(UnaryExpression $leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) { + $outerUnaryOpExpression = clone($leftOperand); + $inner = $this->makeBinaryExpression( + $leftOperand->operand, + $operatorToken, + $byRefToken, + $rightOperand, + $outerUnaryOpExpression + ); + // Either ErrorControlExpression or a UnaryOpExpression + $outerUnaryOpExpression->parent = $parentNode; + // TODO should this binaryExpression be wrapped in a UnaryExpression? + $outerUnaryOpExpression->operand = $inner; + + return $outerUnaryOpExpression; + } + + private function shiftBinaryOperands(BinaryExpression $leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) { + $inner = $this->makeBinaryExpression( + $leftOperand->rightOperand, + $operatorToken, + $byRefToken, + $rightOperand, + $parentNode + ); + $outer = $this->makeBinaryExpression( + $leftOperand->leftOperand, + $leftOperand->operator, + null, + $inner, + $parentNode + ); + $inner->parent = $outer; + return $outer; + } + + private function parseDoStatement($parentNode) { + $doStatement = new DoStatement(); + $doStatement->parent = $parentNode; + $doStatement->do = $this->eat1(TokenKind::DoKeyword); + $doStatement->statement = $this->parseStatement($doStatement); + $doStatement->whileToken = $this->eat1(TokenKind::WhileKeyword); + $doStatement->openParen = $this->eat1(TokenKind::OpenParenToken); + $doStatement->expression = $this->parseExpression($doStatement); + $doStatement->closeParen = $this->eat1(TokenKind::CloseParenToken); + $doStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + return $doStatement; + } + + private function parseForStatement($parentNode) { + $forStatement = new ForStatement(); + $forStatement->parent = $parentNode; + $forStatement->for = $this->eat1(TokenKind::ForKeyword); + $forStatement->openParen = $this->eat1(TokenKind::OpenParenToken); + $forStatement->forInitializer = $this->parseExpressionList($forStatement); // TODO spec is redundant + $forStatement->exprGroupSemicolon1 = $this->eat1(TokenKind::SemicolonToken); + $forStatement->forControl = $this->parseExpressionList($forStatement); + $forStatement->exprGroupSemicolon2 = $this->eat1(TokenKind::SemicolonToken); + $forStatement->forEndOfLoop = $this->parseExpressionList($forStatement); + $forStatement->closeParen = $this->eat1(TokenKind::CloseParenToken); + $forStatement->colon = $this->eatOptional1(TokenKind::ColonToken); + if ($forStatement->colon !== null) { + $forStatement->statements = $this->parseList($forStatement, ParseContext::ForStatementElements); + $forStatement->endFor = $this->eat1(TokenKind::EndForKeyword); + $forStatement->endForSemicolon = $this->eatSemicolonOrAbortStatement(); + } elseif (!$this->checkToken(TokenKind::ScriptSectionEndTag)) { + $forStatement->statements = $this->parseStatement($forStatement); + } + return $forStatement; + } + + private function parseForeachStatement($parentNode) { + $foreachStatement = new ForeachStatement(); + $foreachStatement->parent = $parentNode; + $foreachStatement->foreach = $this->eat1(TokenKind::ForeachKeyword); + $foreachStatement->openParen = $this->eat1(TokenKind::OpenParenToken); + $foreachStatement->forEachCollectionName = $this->parseExpression($foreachStatement); + $foreachStatement->asKeyword = $this->eat1(TokenKind::AsKeyword); + $foreachStatement->foreachKey = $this->tryParseForeachKey($foreachStatement); + $foreachStatement->foreachValue = $this->parseForeachValue($foreachStatement); + $foreachStatement->closeParen = $this->eat1(TokenKind::CloseParenToken); + $foreachStatement->colon = $this->eatOptional1(TokenKind::ColonToken); + if ($foreachStatement->colon !== null) { + $foreachStatement->statements = $this->parseList($foreachStatement, ParseContext::ForeachStatementElements); + $foreachStatement->endForeach = $this->eat1(TokenKind::EndForEachKeyword); + $foreachStatement->endForeachSemicolon = $this->eatSemicolonOrAbortStatement(); + } elseif (!$this->checkToken(TokenKind::ScriptSectionEndTag)) { + $foreachStatement->statements = $this->parseStatement($foreachStatement); + } + return $foreachStatement; + } + + private function tryParseForeachKey($parentNode) { + if (!$this->isExpressionStart($this->getCurrentToken())) { + return null; + } + + $startPos = $this->lexer->getCurrentPosition(); + $startToken = $this->getCurrentToken(); + $foreachKey = new ForeachKey(); + $foreachKey->parent = $parentNode; + $foreachKey->expression = $this->parseExpression($foreachKey); + + if (!$this->checkToken(TokenKind::DoubleArrowToken)) { + $this->lexer->setCurrentPosition($startPos); + $this->token = $startToken; + return null; + } + + $foreachKey->arrow = $this->eat1(TokenKind::DoubleArrowToken); + return $foreachKey; + } + + private function parseForeachValue($parentNode) { + $foreachValue = new ForeachValue(); + $foreachValue->parent = $parentNode; + $foreachValue->ampersand = $this->eatOptional1(TokenKind::AmpersandToken); + $foreachValue->expression = $this->parseExpression($foreachValue); + return $foreachValue; + } + + private function parseGotoStatement($parentNode) { + $gotoStatement = new GotoStatement(); + $gotoStatement->parent = $parentNode; + $gotoStatement->goto = $this->eat1(TokenKind::GotoKeyword); + $gotoStatement->name = $this->eat1(TokenKind::Name); + $gotoStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + return $gotoStatement; + } + + private function parseBreakOrContinueStatement($parentNode) { + // TODO should be error checking if on top level + $continueStatement = new BreakOrContinueStatement(); + $continueStatement->parent = $parentNode; + $continueStatement->breakOrContinueKeyword = $this->eat(TokenKind::ContinueKeyword, TokenKind::BreakKeyword); + + if ($this->isExpressionStart($this->getCurrentToken())) { + $continueStatement->breakoutLevel = $this->parseExpression($continueStatement); + } + + $continueStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + + return $continueStatement; + } + + private function parseReturnStatement($parentNode) { + $returnStatement = new ReturnStatement(); + $returnStatement->parent = $parentNode; + $returnStatement->returnKeyword = $this->eat1(TokenKind::ReturnKeyword); + if ($this->isExpressionStart($this->getCurrentToken())) { + $returnStatement->expression = $this->parseExpression($returnStatement); + } + $returnStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + + return $returnStatement; + } + + private function parseThrowStatement($parentNode) { + $throwStatement = new ThrowStatement(); + $throwStatement->parent = $parentNode; + $throwStatement->throwKeyword = $this->eat1(TokenKind::ThrowKeyword); + // TODO error for failures to parse expressions when not optional + $throwStatement->expression = $this->parseExpression($throwStatement); + $throwStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + + return $throwStatement; + } + + private function parseThrowExpression($parentNode) { + $throwExpression = new ThrowExpression(); + $throwExpression->parent = $parentNode; + $throwExpression->throwKeyword = $this->eat1(TokenKind::ThrowKeyword); + // TODO error for failures to parse expressions when not optional + $throwExpression->expression = $this->parseExpression($throwExpression); + + return $throwExpression; + } + + private function parseTryStatement($parentNode) { + $tryStatement = new TryStatement(); + $tryStatement->parent = $parentNode; + $tryStatement->tryKeyword = $this->eat1(TokenKind::TryKeyword); + $tryStatement->compoundStatement = $this->parseCompoundStatement($tryStatement); // TODO verifiy this is only compound + + $tryStatement->catchClauses = array(); // TODO - should be some standard for empty arrays vs. null? + while ($this->checkToken(TokenKind::CatchKeyword)) { + $tryStatement->catchClauses[] = $this->parseCatchClause($tryStatement); + } + + if ($this->checkToken(TokenKind::FinallyKeyword)) { + $tryStatement->finallyClause = $this->parseFinallyClause($tryStatement); + } + + return $tryStatement; + } + + private function parseCatchClause($parentNode) { + $catchClause = new CatchClause(); + $catchClause->parent = $parentNode; + $catchClause->catch = $this->eat1(TokenKind::CatchKeyword); + $catchClause->openParen = $this->eat1(TokenKind::OpenParenToken); + $qualifiedNameList = $this->parseQualifiedNameCatchList($catchClause)->children ?? []; + $catchClause->qualifiedName = $qualifiedNameList[0] ?? null; // TODO generate missing token or error if null + $catchClause->otherQualifiedNameList = array_slice($qualifiedNameList, 1); // TODO: Generate error if the name list has missing tokens + $catchClause->variableName = $this->eatOptional1(TokenKind::VariableName); + $catchClause->closeParen = $this->eat1(TokenKind::CloseParenToken); + $catchClause->compoundStatement = $this->parseCompoundStatement($catchClause); + + return $catchClause; + } + + private function parseFinallyClause($parentNode) { + $finallyClause = new FinallyClause(); + $finallyClause->parent = $parentNode; + $finallyClause->finallyToken = $this->eat1(TokenKind::FinallyKeyword); + $finallyClause->compoundStatement = $this->parseCompoundStatement($finallyClause); + + return $finallyClause; + } + + private function parseDeclareStatement($parentNode) { + $declareStatement = new DeclareStatement(); + $declareStatement->parent = $parentNode; + $declareStatement->declareKeyword = $this->eat1(TokenKind::DeclareKeyword); + $declareStatement->openParen = $this->eat1(TokenKind::OpenParenToken); + $this->parseAndSetDeclareDirectiveList($declareStatement); + $declareStatement->closeParen = $this->eat1(TokenKind::CloseParenToken); + + if ($this->checkToken(TokenKind::SemicolonToken)) { + $declareStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + } elseif ($this->checkToken(TokenKind::ColonToken)) { + $declareStatement->colon = $this->eat1(TokenKind::ColonToken); + $declareStatement->statements = $this->parseList($declareStatement, ParseContext::DeclareStatementElements); + $declareStatement->enddeclareKeyword = $this->eat1(TokenKind::EndDeclareKeyword); + $declareStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + } else { + $declareStatement->statements = $this->parseStatement($declareStatement); + } + + return $declareStatement; + } + + /** + * @param DeclareStatement $parentNode + */ + private function parseAndSetDeclareDirectiveList($parentNode) { + $declareDirectiveList = $this->parseDeclareDirectiveList($parentNode); + + if (!$declareDirectiveList) { + $declareDirective = new DeclareDirective(); + $declareDirective->parent = $parentNode; + + $declareDirective->name = new MissingToken(TokenKind::Name, $this->token->fullStart); + $declareDirective->equals = new MissingToken(TokenKind::EqualsToken, $this->token->fullStart); + // TODO: This is matching the first token in $this::parseDeclareDirectiveFn. + // Probably best to emit a more general "literal error". + $declareDirective->literal = new MissingToken(TokenKind::FloatingLiteralToken, $this->token->fullStart); + + $parentNode->declareDirective = $declareDirective; + return; + } + + $declareDirective = array_shift($declareDirectiveList->children); + $parentNode->declareDirective = $declareDirective; + $declareDirective->parent = $parentNode; + + if ($declareDirectiveList->children) { + $parentNode->otherDeclareDirectives = $declareDirectiveList; + } + } + + /** + * @param DeclareStatement $parentNode + * @return DelimitedList\DeclareDirectiveList|null + */ + private function parseDeclareDirectiveList($parentNode) { + $declareDirectiveList = $this->parseDelimitedList( + DelimitedList\DeclareDirectiveList::class, + TokenKind::CommaToken, + function ($token) { + return $token->kind === TokenKind::Name; + }, + $this->parseDeclareDirectiveFn(), + $parentNode, + false + ); + + return $declareDirectiveList; + } + + private function parseDeclareDirectiveFn() { + return function ($parentNode) { + $declareDirective = new DeclareDirective(); + $declareDirective->parent = $parentNode; + $declareDirective->name = $this->eat1(TokenKind::Name); + $declareDirective->equals = $this->eat1(TokenKind::EqualsToken); + $declareDirective->literal = + $this->eat( + TokenKind::FloatingLiteralToken, + TokenKind::IntegerLiteralToken, + TokenKind::DecimalLiteralToken, + TokenKind::OctalLiteralToken, + TokenKind::HexadecimalLiteralToken, + TokenKind::BinaryLiteralToken, + TokenKind::InvalidOctalLiteralToken, + TokenKind::InvalidHexadecimalLiteral, + TokenKind::InvalidBinaryLiteral, + TokenKind::StringLiteralToken + ); // TODO simplify + + return $declareDirective; + }; + } + + private function parseSimpleVariable($parentNode) { + return ($this->parseSimpleVariableFn())($parentNode); + } + + private function parseSimpleVariableFn() { + return function ($parentNode) { + $token = $this->getCurrentToken(); + $variable = new Variable(); + $variable->parent = $parentNode; + + if ($token->kind === TokenKind::DollarToken) { + $variable->dollar = $this->eat1(TokenKind::DollarToken); + $token = $this->getCurrentToken(); + + switch ($token->kind) { + case TokenKind::OpenBraceToken: + $variable->name = $this->parseBracedExpression($variable); + break; + case TokenKind::VariableName: + case TokenKind::StringVarname: + case TokenKind::DollarToken: + $variable->name = $this->parseSimpleVariable($variable); + break; + default: + $variable->name = new MissingToken(TokenKind::VariableName, $token->fullStart); + break; + } + } elseif ($token->kind === TokenKind::VariableName || $token->kind === TokenKind::StringVarname) { + // TODO consider splitting into dollar and name. + // StringVarname is the variable name without $, used in a template string e.g. `"${foo}"` + $variable->name = $this->eat(TokenKind::VariableName, TokenKind::StringVarname); + } else { + $variable->name = new MissingToken(TokenKind::VariableName, $token->fullStart); + } + + return $variable; + }; + } + + private function parseYieldExpression($parentNode) { + $yieldExpression = new YieldExpression(); + $yieldExpression->parent = $parentNode; + $yieldExpression->yieldOrYieldFromKeyword = $this->eat( + TokenKind::YieldFromKeyword, + TokenKind::YieldKeyword + ); + if ($yieldExpression->yieldOrYieldFromKeyword->kind === TokenKind::YieldFromKeyword) { + // Don't use parseArrayElement. E.g. `yield from &$varName` or `yield from $key => $varName` are both syntax errors + $arrayElement = new ArrayElement(); + $arrayElement->parent = $yieldExpression; + $arrayElement->elementValue = $this->parseExpression($arrayElement); + $yieldExpression->arrayElement = $arrayElement; + } else { + // This is always an ArrayElement for backwards compatibilitiy. + // TODO: Can this be changed to a non-ArrayElement in a future release? + if ($this->isExpressionStart($this->getCurrentToken())) { + // Both `yield expr;` and `yield;` are possible. + $yieldExpression->arrayElement = $this->parseArrayElement($yieldExpression); + } else { + $yieldExpression->arrayElement = null; + } + } + + return $yieldExpression; + } + + private function parseScriptInclusionExpression($parentNode) { + $scriptInclusionExpression = new ScriptInclusionExpression(); + $scriptInclusionExpression->parent = $parentNode; + $scriptInclusionExpression->requireOrIncludeKeyword = + $this->eat( + TokenKind::RequireKeyword, TokenKind::RequireOnceKeyword, + TokenKind::IncludeKeyword, TokenKind::IncludeOnceKeyword + ); + $scriptInclusionExpression->expression = $this->parseExpression($scriptInclusionExpression); + return $scriptInclusionExpression; + } + + private function parseEchoStatement($parentNode) { + $expressionStatement = new ExpressionStatement(); + + // TODO: Could flatten into EchoStatement instead? + $echoExpression = new EchoExpression(); + $echoExpression->parent = $expressionStatement; + $echoExpression->echoKeyword = $this->eat1(TokenKind::EchoKeyword); + $echoExpression->expressions = + $this->parseExpressionList($echoExpression); + + $expressionStatement->parent = $parentNode; + $expressionStatement->expression = $echoExpression; + $expressionStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + + return $expressionStatement; + } + + private function parseUnsetStatement($parentNode) { + $expressionStatement = new ExpressionStatement(); + + // TODO: Could flatten into UnsetStatement instead? + $unsetExpression = $this->parseUnsetIntrinsicExpression($expressionStatement); + + $expressionStatement->parent = $parentNode; + $expressionStatement->expression = $unsetExpression; + $expressionStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + + return $expressionStatement; + } + + private function parseListIntrinsicExpression($parentNode) { + $listExpression = new ListIntrinsicExpression(); + $listExpression->parent = $parentNode; + $listExpression->listKeyword = $this->eat1(TokenKind::ListKeyword); + $listExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + // TODO - parse loosely as ArrayElementList, and validate parse tree later + $listExpression->listElements = + $this->parseArrayElementList($listExpression, DelimitedList\ListExpressionList::class); + $listExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + + return $listExpression; + } + + private function isArrayElementStart($token) { + return ($this->isArrayElementStartFn())($token); + } + + private function isArrayElementStartFn() { + return function ($token) { + return $token->kind === TokenKind::AmpersandToken || $token->kind === TokenKind::DotDotDotToken || $this->isExpressionStart($token); + }; + } + + private function parseArrayElement($parentNode) { + return ($this->parseArrayElementFn())($parentNode); + } + + private function parseArrayElementFn() { + return function ($parentNode) { + $arrayElement = new ArrayElement(); + $arrayElement->parent = $parentNode; + + if ($this->checkToken(TokenKind::AmpersandToken)) { + $arrayElement->byRef = $this->eat1(TokenKind::AmpersandToken); + $arrayElement->elementValue = $this->parseExpression($arrayElement); + } elseif ($this->checkToken(TokenKind::DotDotDotToken)) { + $arrayElement->dotDotDot = $this->eat1(TokenKind::DotDotDotToken); + $arrayElement->elementValue = $this->parseExpression($arrayElement); + } else { + $expression = $this->parseExpression($arrayElement); + if ($this->checkToken(TokenKind::DoubleArrowToken)) { + $arrayElement->elementKey = $expression; + $arrayElement->arrowToken = $this->eat1(TokenKind::DoubleArrowToken); + $arrayElement->byRef = $this->eatOptional1(TokenKind::AmpersandToken); // TODO not okay for list expressions + $arrayElement->elementValue = $this->parseExpression($arrayElement); + } else { + $arrayElement->elementValue = $expression; + } + } + + return $arrayElement; + }; + } + + private function parseExpressionList($parentExpression) { + return $this->parseDelimitedList( + DelimitedList\ExpressionList::class, + TokenKind::CommaToken, + $this->isExpressionStartFn(), + $this->parseExpressionFn(), + $parentExpression + ); + } + + private function parseUnsetIntrinsicExpression($parentNode) { + $unsetExpression = new UnsetIntrinsicExpression(); + $unsetExpression->parent = $parentNode; + + $unsetExpression->unsetKeyword = $this->eat1(TokenKind::UnsetKeyword); + $unsetExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + $unsetExpression->expressions = $this->parseExpressionList($unsetExpression); + $unsetExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + + return $unsetExpression; + } + + private function parseArrayCreationExpression($parentNode) { + $arrayExpression = new ArrayCreationExpression(); + $arrayExpression->parent = $parentNode; + + $arrayExpression->arrayKeyword = $this->eatOptional1(TokenKind::ArrayKeyword); + + $arrayExpression->openParenOrBracket = $arrayExpression->arrayKeyword !== null + ? $this->eat1(TokenKind::OpenParenToken) + : $this->eat1(TokenKind::OpenBracketToken); + + $arrayExpression->arrayElements = $this->parseArrayElementList($arrayExpression, DelimitedList\ArrayElementList::class); + + $arrayExpression->closeParenOrBracket = $arrayExpression->arrayKeyword !== null + ? $this->eat1(TokenKind::CloseParenToken) + : $this->eat1(TokenKind::CloseBracketToken); + + return $arrayExpression; + } + + private function parseArrayElementList($listExpression, $className) { + return $this->parseDelimitedList( + $className, + TokenKind::CommaToken, + $this->isArrayElementStartFn(), + $this->parseArrayElementFn(), + $listExpression, + true + ); + } + + private function parseEmptyIntrinsicExpression($parentNode) { + $emptyExpression = new EmptyIntrinsicExpression(); + $emptyExpression->parent = $parentNode; + + $emptyExpression->emptyKeyword = $this->eat1(TokenKind::EmptyKeyword); + $emptyExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + $emptyExpression->expression = $this->parseExpression($emptyExpression); + $emptyExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + + return $emptyExpression; + } + + private function parseEvalIntrinsicExpression($parentNode) { + $evalExpression = new EvalIntrinsicExpression(); + $evalExpression->parent = $parentNode; + + $evalExpression->evalKeyword = $this->eat1(TokenKind::EvalKeyword); + $evalExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + $evalExpression->expression = $this->parseExpression($evalExpression); + $evalExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + + return $evalExpression; + } + + private function parseParenthesizedExpression($parentNode) { + $parenthesizedExpression = new ParenthesizedExpression(); + $parenthesizedExpression->parent = $parentNode; + + $parenthesizedExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + $parenthesizedExpression->expression = $this->parseExpression($parenthesizedExpression); + $parenthesizedExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + + return $parenthesizedExpression; + } + + private function parseExitIntrinsicExpression($parentNode) { + $exitExpression = new ExitIntrinsicExpression(); + $exitExpression->parent = $parentNode; + + $exitExpression->exitOrDieKeyword = $this->eat(TokenKind::ExitKeyword, TokenKind::DieKeyword); + $exitExpression->openParen = $this->eatOptional1(TokenKind::OpenParenToken); + if ($exitExpression->openParen !== null) { + if ($this->isExpressionStart($this->getCurrentToken())) { + $exitExpression->expression = $this->parseExpression($exitExpression); + } + $exitExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + } + + return $exitExpression; + } + + private function parsePrintIntrinsicExpression($parentNode) { + $printExpression = new PrintIntrinsicExpression(); + $printExpression->parent = $parentNode; + + $printExpression->printKeyword = $this->eat1(TokenKind::PrintKeyword); + $printExpression->expression = $this->parseExpression($printExpression); + + return $printExpression; + } + + private function parseIssetIntrinsicExpression($parentNode) { + $issetExpression = new IssetIntrinsicExpression(); + $issetExpression->parent = $parentNode; + + $issetExpression->issetKeyword = $this->eat1(TokenKind::IsSetKeyword); + $issetExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + $issetExpression->expressions = $this->parseExpressionList($issetExpression); + $issetExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + + return $issetExpression; + } + + private function parseUnaryOpExpression($parentNode) { + $unaryOpExpression = new UnaryOpExpression(); + $unaryOpExpression->parent = $parentNode; + $unaryOpExpression->operator = + $this->eat(TokenKind::PlusToken, TokenKind::MinusToken, TokenKind::ExclamationToken, TokenKind::TildeToken); + $unaryOpExpression->operand = $this->parseUnaryExpressionOrHigher($unaryOpExpression); + + return $unaryOpExpression; + } + + private function parseErrorControlExpression($parentNode) { + $errorControlExpression = new ErrorControlExpression(); + $errorControlExpression->parent = $parentNode; + + $errorControlExpression->operator = $this->eat1(TokenKind::AtSymbolToken); + $errorControlExpression->operand = $this->parseUnaryExpressionOrHigher($errorControlExpression); + + return $errorControlExpression; + } + + private function parsePrefixUpdateExpression($parentNode) { + $prefixUpdateExpression = new PrefixUpdateExpression(); + $prefixUpdateExpression->parent = $parentNode; + + $prefixUpdateExpression->incrementOrDecrementOperator = $this->eat(TokenKind::PlusPlusToken, TokenKind::MinusMinusToken); + + $prefixUpdateExpression->operand = $this->parsePrimaryExpression($prefixUpdateExpression); + + if (!($prefixUpdateExpression->operand instanceof MissingToken)) { + $prefixUpdateExpression->operand = $this->parsePostfixExpressionRest($prefixUpdateExpression->operand, false); + } + + // TODO also check operand expression validity + return $prefixUpdateExpression; + } + + private function parsePostfixExpressionRest($expression, $allowUpdateExpression = true) { + $tokenKind = $this->getCurrentToken()->kind; + + // `--$a++` is invalid + if ($allowUpdateExpression && + ($tokenKind === TokenKind::PlusPlusToken || + $tokenKind === TokenKind::MinusMinusToken)) { + return $this->parseParsePostfixUpdateExpression($expression); + } + + // TODO write tons of tests + if (!($expression instanceof Variable || + $expression instanceof ParenthesizedExpression || + $expression instanceof QualifiedName || + $expression instanceof CallExpression || + $expression instanceof MemberAccessExpression || + $expression instanceof SubscriptExpression || + $expression instanceof ScopedPropertyAccessExpression || + $expression instanceof StringLiteral || + $expression instanceof ArrayCreationExpression + )) { + return $expression; + } + if ($tokenKind === TokenKind::ColonColonToken) { + $expression = $this->parseScopedPropertyAccessExpression($expression, null); + return $this->parsePostfixExpressionRest($expression); + } + + $tokenKind = $this->getCurrentToken()->kind; + + if ($tokenKind === TokenKind::OpenBraceToken || + $tokenKind === TokenKind::OpenBracketToken) { + $expression = $this->parseSubscriptExpression($expression); + return $this->parsePostfixExpressionRest($expression); + } + + if ($expression instanceof ArrayCreationExpression) { + // Remaining postfix expressions are invalid, so abort + return $expression; + } + + if ($tokenKind === TokenKind::ArrowToken || $tokenKind === TokenKind::QuestionArrowToken) { + $expression = $this->parseMemberAccessExpression($expression); + return $this->parsePostfixExpressionRest($expression); + } + + if ($tokenKind === TokenKind::OpenParenToken && !$this->isParsingUnparenthesizedObjectCreationExpression($expression)) { + $expression = $this->parseCallExpressionRest($expression); + + if (!$this->checkToken(TokenKind::OpenParenToken)) { + return $this->parsePostfixExpressionRest($expression); + } + if ( + $expression instanceof ParenthesizedExpression || + $expression instanceof CallExpression || + $expression instanceof SubscriptExpression) { + // Continue parsing the remaining brackets for expressions + // such as `(new Foo())()`, `foo()()`, `foo()['index']()` + return $this->parsePostfixExpressionRest($expression); + } + return $expression; + } + + // Reached the end of the postfix-expression, so return + return $expression; + } + + private function parseMemberName($parentNode) { + $token = $this->getCurrentToken(); + switch ($token->kind) { + case TokenKind::Name: + $this->advanceToken(); // TODO all names should be Nodes + return $token; + case TokenKind::VariableName: + case TokenKind::DollarToken: + return $this->parseSimpleVariable($parentNode); // TODO should be simple-variable + case TokenKind::OpenBraceToken: + return $this->parseBracedExpression($parentNode); + + default: + if (\in_array($token->kind, $this->nameOrKeywordOrReservedWordTokens)) { + $this->advanceToken(); + $token->kind = TokenKind::Name; + return $token; + } + } + return new MissingToken(TokenKind::MemberName, $token->fullStart); + } + + private function isArgumentExpressionStartFn() { + return function ($token) { + return + isset($this->argumentStartTokensSet[$token->kind]) || $this->isExpressionStart($token); + }; + } + + private function parseArgumentExpressionFn() { + return function ($parentNode) { + $argumentExpression = new ArgumentExpression(); + $argumentExpression->parent = $parentNode; + + $nextToken = $this->lexer->getTokensArray()[$this->lexer->getCurrentPosition()] ?? null; + if ($nextToken && $nextToken->kind === TokenKind::ColonToken) { + $name = $this->token; + $this->advanceToken(); + if ($name->kind === TokenKind::YieldFromKeyword || !\in_array($name->kind, $this->nameOrKeywordOrReservedWordTokens)) { + $name = new SkippedToken($name); + } else { + $name->kind = TokenKind::Name; + } + $argumentExpression->name = $name; + $argumentExpression->colonToken = $this->eat1(TokenKind::ColonToken); + } else { + $argumentExpression->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken); + $argumentExpression->dotDotDotToken = $this->eatOptional1(TokenKind::DotDotDotToken); + } + $argumentExpression->expression = $this->parseExpression($argumentExpression); + return $argumentExpression; + }; + } + + private function parseCallExpressionRest($expression) { + $callExpression = new CallExpression(); + $callExpression->parent = $expression->parent; + $expression->parent = $callExpression; + $callExpression->callableExpression = $expression; + $callExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + $callExpression->argumentExpressionList = + $this->parseArgumentExpressionList($callExpression); + $callExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + return $callExpression; + } + + private function parseParsePostfixUpdateExpression($prefixExpression) { + $postfixUpdateExpression = new PostfixUpdateExpression(); + $postfixUpdateExpression->operand = $prefixExpression; + $postfixUpdateExpression->parent = $prefixExpression->parent; + $prefixExpression->parent = $postfixUpdateExpression; + $postfixUpdateExpression->incrementOrDecrementOperator = + $this->eat(TokenKind::PlusPlusToken, TokenKind::MinusMinusToken); + return $postfixUpdateExpression; + } + + private function parseBracedExpression($parentNode) { + $bracedExpression = new BracedExpression(); + $bracedExpression->parent = $parentNode; + + $bracedExpression->openBrace = $this->eat1(TokenKind::OpenBraceToken); + $bracedExpression->expression = $this->parseExpression($bracedExpression); + $bracedExpression->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + + return $bracedExpression; + } + + private function parseSubscriptExpression($expression) : SubscriptExpression { + $subscriptExpression = new SubscriptExpression(); + $subscriptExpression->parent = $expression->parent; + $expression->parent = $subscriptExpression; + + $subscriptExpression->postfixExpression = $expression; + $subscriptExpression->openBracketOrBrace = $this->eat(TokenKind::OpenBracketToken, TokenKind::OpenBraceToken); + $subscriptExpression->accessExpression = $this->isExpressionStart($this->getCurrentToken()) + ? $this->parseExpression($subscriptExpression) + : null; // TODO error if used in a getter + + if ($subscriptExpression->openBracketOrBrace->kind === TokenKind::OpenBraceToken) { + $subscriptExpression->closeBracketOrBrace = $this->eat1(TokenKind::CloseBraceToken); + } else { + $subscriptExpression->closeBracketOrBrace = $this->eat1(TokenKind::CloseBracketToken); + } + + return $subscriptExpression; + } + + private function parseMemberAccessExpression($expression):MemberAccessExpression { + $memberAccessExpression = new MemberAccessExpression(); + $memberAccessExpression->parent = $expression->parent; + $expression->parent = $memberAccessExpression; + + $memberAccessExpression->dereferencableExpression = $expression; + $memberAccessExpression->arrowToken = $this->eat(TokenKind::ArrowToken, TokenKind::QuestionArrowToken); + $memberAccessExpression->memberName = $this->parseMemberName($memberAccessExpression); + + return $memberAccessExpression; + } + + /** + * @param Node|null $expression + * @param Node|null $fallbackParentNode (Workaround for the invalid AST `use TraitName::foo as ::x`) + */ + private function parseScopedPropertyAccessExpression($expression, $fallbackParentNode): ScopedPropertyAccessExpression { + $scopedPropertyAccessExpression = new ScopedPropertyAccessExpression(); + $scopedPropertyAccessExpression->parent = $expression->parent ?? $fallbackParentNode; + if ($expression instanceof Node) { + $expression->parent = $scopedPropertyAccessExpression; + $scopedPropertyAccessExpression->scopeResolutionQualifier = $expression; // TODO ensure always a Node + } + + $scopedPropertyAccessExpression->doubleColon = $this->eat1(TokenKind::ColonColonToken); + $scopedPropertyAccessExpression->memberName = $this->parseMemberName($scopedPropertyAccessExpression); + + return $scopedPropertyAccessExpression; + } + + public function isParsingUnparenthesizedObjectCreationExpression($expression) { + if (!$this->isParsingObjectCreationExpression) { + return false; + } + if ($expression instanceof Token) { + return true; + } + while ($expression->parent) { + $expression = $expression->parent; + if ($expression instanceof ObjectCreationExpression) { + return true; + } elseif ($expression instanceof ParenthesizedExpression) { + return false; + } + } + } + + private $isParsingObjectCreationExpression = false; + + private function parseObjectCreationExpression($parentNode) { + $objectCreationExpression = new ObjectCreationExpression(); + $objectCreationExpression->parent = $parentNode; + $objectCreationExpression->newKeword = $this->eat1(TokenKind::NewKeyword); + // TODO - add tests for this scenario + $oldIsParsingObjectCreationExpression = $this->isParsingObjectCreationExpression; + $this->isParsingObjectCreationExpression = true; + + if ($this->getCurrentToken()->kind === TokenKind::AttributeToken) { + // Attributes such as `new #[MyAttr] class` can only be used with anonymous class declarations. + // But handle this like $objectCreationExpression->classMembers and leave it up to the applications to detect the invalid combination. + $objectCreationExpression->attributes = $this->parseAttributeGroups($objectCreationExpression); + } + $objectCreationExpression->classTypeDesignator = + $this->eatOptional1(TokenKind::ClassKeyword) ?? + $this->parseExpression($objectCreationExpression); + + $this->isParsingObjectCreationExpression = $oldIsParsingObjectCreationExpression; + + $objectCreationExpression->openParen = $this->eatOptional1(TokenKind::OpenParenToken); + if ($objectCreationExpression->openParen !== null) { + $objectCreationExpression->argumentExpressionList = $this->parseArgumentExpressionList($objectCreationExpression); + $objectCreationExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + } + + $objectCreationExpression->classBaseClause = $this->parseClassBaseClause($objectCreationExpression); + $objectCreationExpression->classInterfaceClause = $this->parseClassInterfaceClause($objectCreationExpression); + + if ($this->getCurrentToken()->kind === TokenKind::OpenBraceToken) { + $objectCreationExpression->classMembers = $this->parseClassMembers($objectCreationExpression); + } + + return $objectCreationExpression; + } + + private function parseArgumentExpressionList($parentNode) { + return $this->parseDelimitedList( + DelimitedList\ArgumentExpressionList::class, + TokenKind::CommaToken, + $this->isArgumentExpressionStartFn(), + $this->parseArgumentExpressionFn(), + $parentNode + ); + } + + /** + * @param Node|Token $leftOperand (should only be a token for invalid ASTs) + * @param Token $questionToken + * @param Node $fallbackParentNode + */ + private function parseTernaryExpression($leftOperand, $questionToken, $fallbackParentNode):TernaryExpression { + $ternaryExpression = new TernaryExpression(); + if ($leftOperand instanceof Node) { + $ternaryExpression->parent = $leftOperand->parent; + $leftOperand->parent = $ternaryExpression; + } else { + $ternaryExpression->parent = $fallbackParentNode; + } + $ternaryExpression->condition = $leftOperand; + $ternaryExpression->questionToken = $questionToken; + $ternaryExpression->ifExpression = $this->isExpressionStart($this->getCurrentToken()) ? $this->parseExpression($ternaryExpression) : null; + $ternaryExpression->colonToken = $this->eat1(TokenKind::ColonToken); + $ternaryExpression->elseExpression = $this->parseBinaryExpressionOrHigher(9, $ternaryExpression); + $leftOperand = $ternaryExpression; + return $leftOperand; + } + + private function parseClassInterfaceClause($parentNode) { + $classInterfaceClause = new ClassInterfaceClause(); + $classInterfaceClause->parent = $parentNode; + $classInterfaceClause->implementsKeyword = $this->eatOptional1(TokenKind::ImplementsKeyword); + + if ($classInterfaceClause->implementsKeyword === null) { + return null; + } + + $classInterfaceClause->interfaceNameList = + $this->parseQualifiedNameList($classInterfaceClause); + return $classInterfaceClause; + } + + private function parseClassBaseClause($parentNode) { + $classBaseClause = new ClassBaseClause(); + $classBaseClause->parent = $parentNode; + + $classBaseClause->extendsKeyword = $this->eatOptional1(TokenKind::ExtendsKeyword); + if ($classBaseClause->extendsKeyword === null) { + return null; + } + $classBaseClause->baseClass = $this->parseQualifiedName($classBaseClause); + + return $classBaseClause; + } + + private function parseClassConstDeclaration($parentNode, $modifiers) { + $classConstDeclaration = new ClassConstDeclaration(); + $classConstDeclaration->parent = $parentNode; + + $classConstDeclaration->modifiers = $modifiers; + $classConstDeclaration->constKeyword = $this->eat1(TokenKind::ConstKeyword); + $classConstDeclaration->constElements = $this->parseConstElements($classConstDeclaration); + $classConstDeclaration->semicolon = $this->eat1(TokenKind::SemicolonToken); + + return $classConstDeclaration; + } + + /** + * @param Node $parentNode + * @param Token[] $modifiers + * @param Token|null $questionToken + */ + private function parseRemainingPropertyDeclarationOrMissingMemberDeclaration($parentNode, $modifiers, $questionToken = null) + { + $typeDeclarationList = $this->tryParseParameterTypeDeclarationList(null); + if ($this->getCurrentToken()->kind !== TokenKind::VariableName) { + return $this->makeMissingMemberDeclaration($parentNode, $modifiers, $questionToken, $typeDeclarationList); + } + return $this->parsePropertyDeclaration($parentNode, $modifiers, $questionToken, $typeDeclarationList); + } + + /** + * @param Node $parentNode + * @param Token[] $modifiers + * @param Token|null $questionToken + * @param DelimitedList\QualifiedNameList|null $typeDeclarationList + */ + private function parsePropertyDeclaration($parentNode, $modifiers, $questionToken = null, $typeDeclarationList = null) { + $propertyDeclaration = new PropertyDeclaration(); + $propertyDeclaration->parent = $parentNode; + + $propertyDeclaration->modifiers = $modifiers; + $propertyDeclaration->questionToken = $questionToken; + if ($typeDeclarationList) { + /** $typeDeclarationList is a Node or a Token (e.g. IntKeyword) */ + $typeDeclaration = \array_shift($typeDeclarationList->children); + $propertyDeclaration->typeDeclaration = $typeDeclaration; + if ($typeDeclaration instanceof Node) { + $typeDeclaration->parent = $propertyDeclaration; + } + if ($typeDeclarationList->children) { + $propertyDeclaration->otherTypeDeclarations = $typeDeclarationList; + $typeDeclarationList->parent = $propertyDeclaration; + } + } elseif ($questionToken) { + $propertyDeclaration->typeDeclaration = new MissingToken(TokenKind::PropertyType, $this->token->fullStart); + } + $propertyDeclaration->propertyElements = $this->parseExpressionList($propertyDeclaration); + $propertyDeclaration->semicolon = $this->eat1(TokenKind::SemicolonToken); + + return $propertyDeclaration; + } + + /** + * @param Node $parentNode + * @return DelimitedList\QualifiedNameList + */ + private function parseQualifiedNameList($parentNode) { + return $this->parseDelimitedList( + DelimitedList\QualifiedNameList::class, + TokenKind::CommaToken, + $this->isQualifiedNameStartFn(), + $this->parseQualifiedNameFn(), + $parentNode); + } + + private function parseQualifiedNameCatchList($parentNode) { + $result = $this->parseDelimitedList( + DelimitedList\QualifiedNameList::class, + TokenKind::BarToken, + $this->isQualifiedNameStartForCatchFn(), + $this->parseQualifiedNameFn(), + $parentNode); + + // Add a MissingToken so that this will Warn about `catch (T| $x) {}` + // TODO: Make this a reusable abstraction? + if ($result && (end($result->children)->kind ?? null) === TokenKind::BarToken) { + $result->children[] = new MissingToken(TokenKind::Name, $this->token->fullStart); + } + return $result; + } + + private function parseInterfaceDeclaration($parentNode) { + $interfaceDeclaration = new InterfaceDeclaration(); // TODO verify not nested + $interfaceDeclaration->parent = $parentNode; + $interfaceDeclaration->interfaceKeyword = $this->eat1(TokenKind::InterfaceKeyword); + $interfaceDeclaration->name = $this->eat1(TokenKind::Name); + $interfaceDeclaration->interfaceBaseClause = $this->parseInterfaceBaseClause($interfaceDeclaration); + $interfaceDeclaration->interfaceMembers = $this->parseInterfaceMembers($interfaceDeclaration); + return $interfaceDeclaration; + } + + private function parseInterfaceMembers($parentNode) : Node { + $interfaceMembers = new InterfaceMembers(); + $interfaceMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken); + $interfaceMembers->interfaceMemberDeclarations = $this->parseList($interfaceMembers, ParseContext::InterfaceMembers); + $interfaceMembers->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + $interfaceMembers->parent = $parentNode; + return $interfaceMembers; + } + + private function isInterfaceMemberDeclarationStart(Token $token) { + switch ($token->kind) { + // visibility-modifier + case TokenKind::PublicKeyword: + case TokenKind::ProtectedKeyword: + case TokenKind::PrivateKeyword: + + // static-modifier + case TokenKind::StaticKeyword: + + // class-modifier + case TokenKind::AbstractKeyword: + case TokenKind::FinalKeyword: + + case TokenKind::ConstKeyword: + + case TokenKind::FunctionKeyword: + + case TokenKind::AttributeToken: + return true; + } + return false; + } + + private function parseInterfaceElementFn() { + return function ($parentNode) { + $modifiers = $this->parseModifiers(); + + $token = $this->getCurrentToken(); + switch ($token->kind) { + case TokenKind::ConstKeyword: + return $this->parseClassConstDeclaration($parentNode, $modifiers); + + case TokenKind::FunctionKeyword: + return $this->parseMethodDeclaration($parentNode, $modifiers); + + case TokenKind::AttributeToken: + return $this->parseAttributeStatement($parentNode); + + default: + $missingInterfaceMemberDeclaration = new MissingMemberDeclaration(); + $missingInterfaceMemberDeclaration->parent = $parentNode; + $missingInterfaceMemberDeclaration->modifiers = $modifiers; + return $missingInterfaceMemberDeclaration; + } + }; + } + + private function parseInterfaceBaseClause($parentNode) { + $interfaceBaseClause = new InterfaceBaseClause(); + $interfaceBaseClause->parent = $parentNode; + + $interfaceBaseClause->extendsKeyword = $this->eatOptional1(TokenKind::ExtendsKeyword); + if (isset($interfaceBaseClause->extendsKeyword)) { + $interfaceBaseClause->interfaceNameList = $this->parseQualifiedNameList($interfaceBaseClause); + } else { + return null; + } + + return $interfaceBaseClause; + } + + private function parseNamespaceDefinition($parentNode) { + $namespaceDefinition = new NamespaceDefinition(); + $namespaceDefinition->parent = $parentNode; + + $namespaceDefinition->namespaceKeyword = $this->eat1(TokenKind::NamespaceKeyword); + + if (!$this->checkToken(TokenKind::NamespaceKeyword)) { + $namespaceDefinition->name = $this->parseQualifiedName($namespaceDefinition); // TODO only optional with compound statement block + } + + $namespaceDefinition->compoundStatementOrSemicolon = + $this->checkToken(TokenKind::OpenBraceToken) ? + $this->parseCompoundStatement($namespaceDefinition) : $this->eatSemicolonOrAbortStatement(); + + return $namespaceDefinition; + } + + private function parseNamespaceUseDeclaration($parentNode) { + $namespaceUseDeclaration = new NamespaceUseDeclaration(); + $namespaceUseDeclaration->parent = $parentNode; + $namespaceUseDeclaration->useKeyword = $this->eat1(TokenKind::UseKeyword); + $namespaceUseDeclaration->functionOrConst = $this->eatOptional(TokenKind::FunctionKeyword, TokenKind::ConstKeyword); + $namespaceUseDeclaration->useClauses = $this->parseNamespaceUseClauseList($namespaceUseDeclaration); + $namespaceUseDeclaration->semicolon = $this->eatSemicolonOrAbortStatement(); + return $namespaceUseDeclaration; + } + + private function parseNamespaceUseClauseList($parentNode) { + return $this->parseDelimitedList( + DelimitedList\NamespaceUseClauseList::class, + TokenKind::CommaToken, + function ($token) { + return $this->isQualifiedNameStart($token) || $token->kind === TokenKind::FunctionKeyword || $token->kind === TokenKind::ConstKeyword; + }, + function ($parentNode) { + $namespaceUseClause = new NamespaceUseClause(); + $namespaceUseClause->parent = $parentNode; + $namespaceUseClause->namespaceName = $this->parseQualifiedName($namespaceUseClause); + if ($this->checkToken(TokenKind::AsKeyword)) { + $namespaceUseClause->namespaceAliasingClause = $this->parseNamespaceAliasingClause($namespaceUseClause); + } + elseif ($this->checkToken(TokenKind::OpenBraceToken)) { + $namespaceUseClause->openBrace = $this->eat1(TokenKind::OpenBraceToken); + $namespaceUseClause->groupClauses = $this->parseNamespaceUseGroupClauseList($namespaceUseClause); + $namespaceUseClause->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + } + + return $namespaceUseClause; + }, + $parentNode + ); + } + + private function parseNamespaceUseGroupClauseList($parentNode) { + return $this->parseDelimitedList( + DelimitedList\NamespaceUseGroupClauseList::class, + TokenKind::CommaToken, + function ($token) { + return $this->isQualifiedNameStart($token) || $token->kind === TokenKind::FunctionKeyword || $token->kind === TokenKind::ConstKeyword; + }, + function ($parentNode) { + $namespaceUseGroupClause = new NamespaceUseGroupClause(); + $namespaceUseGroupClause->parent = $parentNode; + + $namespaceUseGroupClause->functionOrConst = $this->eatOptional(TokenKind::FunctionKeyword, TokenKind::ConstKeyword); + $namespaceUseGroupClause->namespaceName = $this->parseQualifiedName($namespaceUseGroupClause); + if ($this->checkToken(TokenKind::AsKeyword)) { + $namespaceUseGroupClause->namespaceAliasingClause = $this->parseNamespaceAliasingClause($namespaceUseGroupClause); + } + + return $namespaceUseGroupClause; + }, + $parentNode + ); + } + + private function parseNamespaceAliasingClause($parentNode) { + $namespaceAliasingClause = new NamespaceAliasingClause(); + $namespaceAliasingClause->parent = $parentNode; + $namespaceAliasingClause->asKeyword = $this->eat1(TokenKind::AsKeyword); + $namespaceAliasingClause->name = $this->eat1(TokenKind::Name); + return $namespaceAliasingClause; + } + + private function parseTraitDeclaration($parentNode) { + $traitDeclaration = new TraitDeclaration(); + $traitDeclaration->parent = $parentNode; + + $traitDeclaration->traitKeyword = $this->eat1(TokenKind::TraitKeyword); + $traitDeclaration->name = $this->eat1(TokenKind::Name); + + $traitDeclaration->traitMembers = $this->parseTraitMembers($traitDeclaration); + + return $traitDeclaration; + } + + private function parseTraitMembers($parentNode) { + $traitMembers = new TraitMembers(); + $traitMembers->parent = $parentNode; + + $traitMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken); + + $traitMembers->traitMemberDeclarations = $this->parseList($traitMembers, ParseContext::TraitMembers); + + $traitMembers->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + + return $traitMembers; + } + + private function isTraitMemberDeclarationStart($token) { + switch ($token->kind) { + // property-declaration + case TokenKind::VariableName: + + // modifiers + case TokenKind::PublicKeyword: + case TokenKind::ProtectedKeyword: + case TokenKind::PrivateKeyword: + case TokenKind::VarKeyword: + case TokenKind::StaticKeyword: + case TokenKind::AbstractKeyword: + case TokenKind::FinalKeyword: + + // method-declaration + case TokenKind::FunctionKeyword: + + // trait-use-clauses + case TokenKind::UseKeyword: + + // attributes + case TokenKind::AttributeToken: + return true; + } + return false; + } + + private function parseTraitElementFn() { + return function ($parentNode) { + $modifiers = $this->parseModifiers(); + + $token = $this->getCurrentToken(); + switch ($token->kind) { + case TokenKind::FunctionKeyword: + return $this->parseMethodDeclaration($parentNode, $modifiers); + + case TokenKind::QuestionToken: + return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration( + $parentNode, + $modifiers, + $this->eat1(TokenKind::QuestionToken) + ); + case TokenKind::VariableName: + return $this->parsePropertyDeclaration($parentNode, $modifiers); + + case TokenKind::UseKeyword: + return $this->parseTraitUseClause($parentNode); + + case TokenKind::AttributeToken: + return $this->parseAttributeStatement($parentNode); + + default: + return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration($parentNode, $modifiers); + } + }; + } + + /** + * @param Node $parentNode + * @param Token[] $modifiers + * @param Token $questionToken + * @param DelimitedList\QualifiedNameList|null $typeDeclarationList + */ + private function makeMissingMemberDeclaration($parentNode, $modifiers, $questionToken = null, $typeDeclarationList = null) { + $missingTraitMemberDeclaration = new MissingMemberDeclaration(); + $missingTraitMemberDeclaration->parent = $parentNode; + $missingTraitMemberDeclaration->modifiers = $modifiers; + $missingTraitMemberDeclaration->questionToken = $questionToken; + if ($typeDeclarationList) { + $missingTraitMemberDeclaration->typeDeclaration = \array_shift($typeDeclarationList->children); + $missingTraitMemberDeclaration->typeDeclaration->parent = $missingTraitMemberDeclaration; + if ($typeDeclarationList->children) { + $missingTraitMemberDeclaration->otherTypeDeclarations = $typeDeclarationList; + $typeDeclarationList->parent = $missingTraitMemberDeclaration; + } + } elseif ($questionToken) { + $missingTraitMemberDeclaration->typeDeclaration = new MissingToken(TokenKind::PropertyType, $this->token->fullStart); + } + return $missingTraitMemberDeclaration; + } + + private function parseTraitUseClause($parentNode) { + $traitUseClause = new TraitUseClause(); + $traitUseClause->parent = $parentNode; + + $traitUseClause->useKeyword = $this->eat1(TokenKind::UseKeyword); + $traitUseClause->traitNameList = $this->parseQualifiedNameList($traitUseClause); + + $traitUseClause->semicolonOrOpenBrace = $this->eat(TokenKind::OpenBraceToken, TokenKind::SemicolonToken); + if ($traitUseClause->semicolonOrOpenBrace->kind === TokenKind::OpenBraceToken) { + $traitUseClause->traitSelectAndAliasClauses = $this->parseTraitSelectAndAliasClauseList($traitUseClause); + $traitUseClause->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + } + + return $traitUseClause; + } + + private function parseTraitSelectAndAliasClauseList($parentNode) { + return $this->parseDelimitedList( + DelimitedList\TraitSelectOrAliasClauseList::class, + TokenKind::SemicolonToken, + $this->isQualifiedNameStartFn(), + $this->parseTraitSelectOrAliasClauseFn(), + $parentNode + ); + } + + private function parseTraitSelectOrAliasClauseFn() { + return function ($parentNode) { + $traitSelectAndAliasClause = new TraitSelectOrAliasClause(); + $traitSelectAndAliasClause->parent = $parentNode; + $traitSelectAndAliasClause->name = // TODO update spec + $this->parseQualifiedNameOrScopedPropertyAccessExpression($traitSelectAndAliasClause); + + $traitSelectAndAliasClause->asOrInsteadOfKeyword = $this->eat(TokenKind::AsKeyword, TokenKind::InsteadOfKeyword); + $traitSelectAndAliasClause->modifiers = $this->parseModifiers(); // TODO accept all modifiers, verify later + + if ($traitSelectAndAliasClause->asOrInsteadOfKeyword->kind === TokenKind::InsteadOfKeyword) { + // https://github.com/Microsoft/tolerant-php-parser/issues/190 + // TODO: In the next backwards incompatible release, convert targetName to a list? + $interfaceNameList = $this->parseQualifiedNameList($traitSelectAndAliasClause)->children ?? []; + $traitSelectAndAliasClause->targetName = $interfaceNameList[0] ?? new MissingToken(TokenKind::BarToken, $this->token->fullStart); + $traitSelectAndAliasClause->remainingTargetNames = array_slice($interfaceNameList, 1); + } else { + $traitSelectAndAliasClause->targetName = + $this->parseQualifiedNameOrScopedPropertyAccessExpression($traitSelectAndAliasClause); + $traitSelectAndAliasClause->remainingTargetNames = []; + } + + // TODO errors for insteadof/as + return $traitSelectAndAliasClause; + }; + } + + private function parseQualifiedNameOrScopedPropertyAccessExpression($parentNode) { + $qualifiedNameOrScopedProperty = $this->parseQualifiedName($parentNode); + if ($this->getCurrentToken()->kind === TokenKind::ColonColonToken) { + $qualifiedNameOrScopedProperty = $this->parseScopedPropertyAccessExpression($qualifiedNameOrScopedProperty, $parentNode); + } + return $qualifiedNameOrScopedProperty; + } + + private function parseGlobalDeclaration($parentNode) { + $globalDeclaration = new GlobalDeclaration(); + $globalDeclaration->parent = $parentNode; + + $globalDeclaration->globalKeyword = $this->eat1(TokenKind::GlobalKeyword); + $globalDeclaration->variableNameList = $this->parseDelimitedList( + DelimitedList\VariableNameList::class, + TokenKind::CommaToken, + $this->isVariableNameStartFn(), + $this->parseSimpleVariableFn(), + $globalDeclaration + ); + + $globalDeclaration->semicolon = $this->eatSemicolonOrAbortStatement(); + + return $globalDeclaration; + } + + private function parseFunctionStaticDeclaration($parentNode) { + $functionStaticDeclaration = new FunctionStaticDeclaration(); + $functionStaticDeclaration->parent = $parentNode; + + $functionStaticDeclaration->staticKeyword = $this->eat1(TokenKind::StaticKeyword); + $functionStaticDeclaration->staticVariableNameList = $this->parseDelimitedList( + DelimitedList\StaticVariableNameList::class, + TokenKind::CommaToken, + function ($token) { + return $token->kind === TokenKind::VariableName; + }, + $this->parseStaticVariableDeclarationFn(), + $functionStaticDeclaration + ); + $functionStaticDeclaration->semicolon = $this->eatSemicolonOrAbortStatement(); + + return $functionStaticDeclaration; + } + + private function isVariableNameStartFn() { + return function ($token) { + return $token->kind === TokenKind::VariableName || $token->kind === TokenKind::DollarToken; + }; + } + + private function parseStaticVariableDeclarationFn() { + return function ($parentNode) { + $staticVariableDeclaration = new StaticVariableDeclaration(); + $staticVariableDeclaration->parent = $parentNode; + $staticVariableDeclaration->variableName = $this->eat1(TokenKind::VariableName); + $staticVariableDeclaration->equalsToken = $this->eatOptional1(TokenKind::EqualsToken); + if ($staticVariableDeclaration->equalsToken !== null) { + // TODO add post-parse rule that checks for invalid assignments + $staticVariableDeclaration->assignment = $this->parseExpression($staticVariableDeclaration); + } + return $staticVariableDeclaration; + }; + } + + private function parseConstDeclaration($parentNode) { + $constDeclaration = new ConstDeclaration(); + $constDeclaration->parent = $parentNode; + + $constDeclaration->constKeyword = $this->eat1(TokenKind::ConstKeyword); + $constDeclaration->constElements = $this->parseConstElements($constDeclaration); + $constDeclaration->semicolon = $this->eatSemicolonOrAbortStatement(); + + return $constDeclaration; + } + + private function parseConstElements($parentNode) { + return $this->parseDelimitedList( + DelimitedList\ConstElementList::class, + TokenKind::CommaToken, + function ($token) { + return \in_array($token->kind, $this->nameOrKeywordOrReservedWordTokens); + }, + $this->parseConstElementFn(), + $parentNode + ); + } + + private function parseConstElementFn() { + return function ($parentNode) { + $constElement = new ConstElement(); + $constElement->parent = $parentNode; + $constElement->name = $this->getCurrentToken(); + $this->advanceToken(); + $constElement->name->kind = TokenKind::Name; // to support keyword names + $constElement->equalsToken = $this->eat1(TokenKind::EqualsToken); + // TODO add post-parse rule that checks for invalid assignments + $constElement->assignment = $this->parseExpression($constElement); + return $constElement; + }; + } + + private function parseCastExpression($parentNode) { + $castExpression = new CastExpression(); + $castExpression->parent = $parentNode; + $castExpression->castType = $this->eat( + TokenKind::ArrayCastToken, + TokenKind::BoolCastToken, + TokenKind::DoubleCastToken, + TokenKind::IntCastToken, + TokenKind::ObjectCastToken, + TokenKind::StringCastToken, + TokenKind::UnsetCastToken + ); + + $castExpression->operand = $this->parseUnaryExpressionOrHigher($castExpression); + + return $castExpression; + } + + private function parseCastExpressionGranular($parentNode) { + $castExpression = new CastExpression(); + $castExpression->parent = $parentNode; + + $castExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + $castExpression->castType = $this->eat( + TokenKind::ArrayKeyword, + TokenKind::BinaryReservedWord, + TokenKind::BoolReservedWord, + TokenKind::BooleanReservedWord, + TokenKind::DoubleReservedWord, + TokenKind::IntReservedWord, + TokenKind::IntegerReservedWord, + TokenKind::FloatReservedWord, + TokenKind::ObjectReservedWord, + TokenKind::RealReservedWord, + TokenKind::StringReservedWord, + TokenKind::UnsetKeyword + ); + $castExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + $castExpression->operand = $this->parseUnaryExpressionOrHigher($castExpression); + + return $castExpression; + } + + private function parseAnonymousFunctionCreationExpression($parentNode) { + $staticModifier = $this->eatOptional1(TokenKind::StaticKeyword); + if ($this->getCurrentToken()->kind === TokenKind::FnKeyword) { + return $this->parseArrowFunctionCreationExpression($parentNode, $staticModifier); + } + $anonymousFunctionCreationExpression = new AnonymousFunctionCreationExpression(); + $anonymousFunctionCreationExpression->parent = $parentNode; + + $anonymousFunctionCreationExpression->staticModifier = $staticModifier; + $this->parseFunctionType($anonymousFunctionCreationExpression, false, true); + + return $anonymousFunctionCreationExpression; + } + + private function parseArrowFunctionCreationExpression($parentNode, $staticModifier) : ArrowFunctionCreationExpression { + $arrowFunction = new ArrowFunctionCreationExpression(); + $arrowFunction->parent = $parentNode; + $arrowFunction->staticModifier = $staticModifier; + + $arrowFunction->functionKeyword = $this->eat1(TokenKind::FnKeyword); + $arrowFunction->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken); + $arrowFunction->name = $this->eatOptional($this->nameOrKeywordOrReservedWordTokens); + + if (isset($arrowFunction->name)) { + // Anonymous functions should not have names. + // This is based on the code for AnonymousFunctionCreationExpression. + $arrowFunction->name->kind = TokenKind::Name; + $arrowFunction->name = new SkippedToken($arrowFunction->name); // TODO instead handle this during post-walk + } + + $arrowFunction->openParen = $this->eat1(TokenKind::OpenParenToken); + $arrowFunction->parameters = $this->parseDelimitedList( + DelimitedList\ParameterDeclarationList::class, + TokenKind::CommaToken, + $this->isParameterStartFn(), + $this->parseParameterFn(), + $arrowFunction); + $arrowFunction->closeParen = $this->eat1(TokenKind::CloseParenToken); + + if ($this->checkToken(TokenKind::ColonToken)) { + $arrowFunction->colonToken = $this->eat1(TokenKind::ColonToken); + $arrowFunction->questionToken = $this->eatOptional1(TokenKind::QuestionToken); + $this->parseAndSetReturnTypeDeclarationList($arrowFunction); + } + + $arrowFunction->arrowToken = $this->eat1(TokenKind::DoubleArrowToken); + $arrowFunction->resultExpression = $this->parseExpression($arrowFunction); + + return $arrowFunction; + } + + private function parseAnonymousFunctionUseClause($parentNode) { + $anonymousFunctionUseClause = new AnonymousFunctionUseClause(); + $anonymousFunctionUseClause->parent = $parentNode; + + $anonymousFunctionUseClause->useKeyword = $this->eatOptional1(TokenKind::UseKeyword); + if ($anonymousFunctionUseClause->useKeyword === null) { + return null; + } + $anonymousFunctionUseClause->openParen = $this->eat1(TokenKind::OpenParenToken); + $anonymousFunctionUseClause->useVariableNameList = $this->parseDelimitedList( + DelimitedList\UseVariableNameList::class, + TokenKind::CommaToken, + function ($token) { + return $token->kind === TokenKind::AmpersandToken || $token->kind === TokenKind::VariableName; + }, + function ($parentNode) { + $useVariableName = new UseVariableName(); + $useVariableName->parent = $parentNode; + $useVariableName->byRef = $this->eatOptional1(TokenKind::AmpersandToken); + $useVariableName->variableName = $this->eat1(TokenKind::VariableName); + return $useVariableName; + }, + $anonymousFunctionUseClause + ) ?: (new MissingToken(TokenKind::VariableName, $this->token->fullStart)); + $anonymousFunctionUseClause->closeParen = $this->eat1(TokenKind::CloseParenToken); + + return $anonymousFunctionUseClause; + } + + private function parseMatchExpression($parentNode) { + $matchExpression = new MatchExpression(); + $matchExpression->parent = $parentNode; + $matchExpression->matchToken = $this->eat1(TokenKind::MatchKeyword); + $matchExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + $matchExpression->expression = $this->parseExpression($matchExpression); + $matchExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + $matchExpression->openBrace = $this->eat1(TokenKind::OpenBraceToken); + $matchExpression->arms = $this->parseDelimitedList( + DelimitedList\MatchExpressionArmList::class, + TokenKind::CommaToken, + $this->isMatchConditionStartFn(), + $this->parseMatchArmFn(), + $matchExpression); + $matchExpression->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + return $matchExpression; + } + + private function isMatchConditionStartFn() { + return function ($token) { + return $token->kind === TokenKind::DefaultKeyword || + $this->isExpressionStart($token); + }; + } + + private function parseMatchArmFn() { + return function ($parentNode) { + $matchArm = new MatchArm(); + $matchArm->parent = $parentNode; + $matchArmConditionList = $this->parseDelimitedList( + DelimitedList\MatchArmConditionList::class, + TokenKind::CommaToken, + $this->isMatchConditionStartFn(), + $this->parseMatchConditionFn(), + $matchArm + ); + $matchArmConditionList->parent = $matchArm; + $matchArm->conditionList = $matchArmConditionList; + $matchArm->arrowToken = $this->eat1(TokenKind::DoubleArrowToken); + $matchArm->body = $this->parseExpression($matchArm); + return $matchArm; + }; + } + + private function parseMatchConditionFn() { + return function ($parentNode) { + if ($this->token->kind === TokenKind::DefaultKeyword) { + return $this->eat1(TokenKind::DefaultKeyword); + } + return $this->parseExpression($parentNode); + }; + } + + private function parseCloneExpression($parentNode) { + $cloneExpression = new CloneExpression(); + $cloneExpression->parent = $parentNode; + + $cloneExpression->cloneKeyword = $this->eat1(TokenKind::CloneKeyword); + $cloneExpression->expression = $this->parseUnaryExpressionOrHigher($cloneExpression); + + return $cloneExpression; + } + + private function eatSemicolonOrAbortStatement() { + if ($this->getCurrentToken()->kind !== TokenKind::ScriptSectionEndTag) { + return $this->eat1(TokenKind::SemicolonToken); + } + return null; + } + + private function parseInlineHtml($parentNode) { + $inlineHtml = new InlineHtml(); + $inlineHtml->parent = $parentNode; + $inlineHtml->scriptSectionEndTag = $this->eatOptional1(TokenKind::ScriptSectionEndTag); + $inlineHtml->text = $this->eatOptional1(TokenKind::InlineHtml); + $inlineHtml->scriptSectionStartTag = $this->eatOptional(TokenKind::ScriptSectionStartTag, TokenKind::ScriptSectionStartWithEchoTag); + + // This is the easiest way to represent `scriptSectionStartTag->kind ?? null) === TokenKind::ScriptSectionStartWithEchoTag) { + $echoStatement = new ExpressionStatement(); + + $echoExpression = new EchoExpression(); + $expressionList = $this->parseExpressionList($echoExpression) ?? (new MissingToken(TokenKind::Expression, $this->token->fullStart)); + $echoExpression->expressions = $expressionList; + $echoExpression->parent = $echoStatement; + + $echoStatement->expression = $echoExpression; + $echoStatement->semicolon = $this->eatSemicolonOrAbortStatement(); + $echoStatement->parent = $inlineHtml; + // Deliberately leave echoKeyword as null instead of MissingToken + + $inlineHtml->echoStatement = $echoStatement; + } + + return $inlineHtml; + } +} + +class Associativity { + const None = 0; + const Left = 1; + const Right = 2; +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/PhpTokenizer.php b/bundled-libs/microsoft/tolerant-php-parser/src/PhpTokenizer.php new file mode 100644 index 000000000..5c2b19ac6 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/PhpTokenizer.php @@ -0,0 +1,404 @@ +tokensArray = $this->getTokensArrayFromContent($content); + $this->endOfFilePos = \count($this->tokensArray) - 1; + $this->pos = 0; + } + + public function scanNextToken() : Token { + return $this->pos >= $this->endOfFilePos + ? $this->tokensArray[$this->endOfFilePos] + : $this->tokensArray[$this->pos++]; + } + + public function getCurrentPosition() : int { + return $this->pos; + } + + public function setCurrentPosition(int $pos) { + $this->pos = $pos; + } + + public function getEndOfFilePosition() : int { + return $this->endOfFilePos; + } + + public function getTokensArray() : array { + return $this->tokensArray; + } + + /** + * Return an array of Token object instances created from $content. + * + * This method is optimized heavily - this processes every single token being created. + * + * @param string $content the raw php code + * @param ?int $parseContext can be SourceElements when extracting doc comments + * @param int $initialPos + * @param bool $treatCommentsAsTrivia + * @return Token[] + */ + public static function getTokensArrayFromContent( + $content, $parseContext = null, $initialPos = 0, $treatCommentsAsTrivia = true + ) : array { + if ($parseContext !== null) { + // If needed, add a prefix so that token_get_all will tokenize the remaining $contents + $prefix = self::PARSE_CONTEXT_TO_PREFIX[$parseContext]; + $content = $prefix . $content; + } + + $tokens = static::tokenGetAll($content, $parseContext); + + $arr = array(); + $fullStart = $start = $pos = $initialPos; + if ($parseContext !== null) { + // If needed, skip over the prefix we added for token_get_all and remove those tokens. + // This was moved out of the below main loop as an optimization. + // (the common case of parsing an entire file uses a null parseContext) + foreach ($tokens as $i => $token) { + unset($tokens[$i]); + if (\is_array($token)) { + $pos += \strlen($token[1]); + } else { + $pos += \strlen($token); + } + if (\strlen($prefix) < $pos) { + $fullStart = $start = $pos = $initialPos; + break; + } + } + } + + // Convert tokens from token_get_all to Token instances, + // skipping whitespace and (usually, when parseContext is null) comments. + foreach ($tokens as $token) { + if (\is_array($token)) { + $tokenKind = $token[0]; + $strlen = \strlen($token[1]); + } else { + $pos += \strlen($token); + $newTokenKind = self::TOKEN_MAP[$token] ?? TokenKind::Unknown; + $arr[] = new Token($newTokenKind, $fullStart, $start, $pos - $fullStart); + $start = $fullStart = $pos; + continue; + } + + $pos += $strlen; + + // Optimization note: In PHP < 7.2, the switch statement would check case by case, + // so putting the most common cases first is slightly faster + switch ($tokenKind) { + case \T_WHITESPACE: + $start += $strlen; + break; + case \T_STRING: + $name = \strtolower($token[1]); + $newTokenKind = TokenStringMaps::RESERVED_WORDS[$name] ?? TokenKind::Name; + $arr[] = new Token($newTokenKind, $fullStart, $start, $pos - $fullStart); + $start = $fullStart = $pos; + break; + case \T_OPEN_TAG: + $arr[] = new Token(TokenKind::ScriptSectionStartTag, $fullStart, $start, $pos-$fullStart); + $start = $fullStart = $pos; + break; + case \PHP_VERSION_ID >= 80000 ? \T_NAME_QUALIFIED : -1000: + case \PHP_VERSION_ID >= 80000 ? \T_NAME_FULLY_QUALIFIED : -1001: + // NOTE: This switch is called on every token of every file being parsed, so this traded performance for readability. + // + // PHP's Opcache is able to optimize switches that are exclusively known longs, + // but not switches that mix strings and longs or have unknown longs. + // Longs are only known if they're declared within the same *class* or an internal constant (tokenizer). + // + // For some reason, the SWITCH_LONG opcode was not generated when the expression was part of a class constant. + // (seen with php -d opcache.opt_debug_level=0x20000) + // + // Use negative values because that's not expected to overlap with token kinds that token_get_all() will return. + // + // T_NAME_* was added in php 8.0 to forbid whitespace between parts of names. + // Here, emulate the tokenization of php 7 by splitting it up into 1 or more tokens. + foreach (\explode('\\', $token[1]) as $i => $name) { + if ($i) { + $arr[] = new Token(TokenKind::BackslashToken, $fullStart, $start, 1 + $start - $fullStart); + $start++; + $fullStart = $start; + } + if ($name === '') { + continue; + } + // TODO: TokenStringMaps::RESERVED_WORDS[$name] ?? TokenKind::Name for compatibility? + $len = \strlen($name); + $arr[] = new Token(TokenKind::Name, $fullStart, $start, $len + $start - $fullStart); + $start += $len; + $fullStart = $start; + } + break; + case \PHP_VERSION_ID >= 80000 ? \T_NAME_RELATIVE : -1002: + // This is a namespace-relative name: namespace\... + foreach (\explode('\\', $token[1]) as $i => $name) { + $len = \strlen($name); + if (!$i) { + $arr[] = new Token(TokenKind::NamespaceKeyword, $fullStart, $start, $len + $start - $fullStart); + $start += $len; + $fullStart = $start; + continue; + } + $arr[] = new Token(TokenKind::BackslashToken, $fullStart, $start, 1); + $start++; + + // TODO: TokenStringMaps::RESERVED_WORDS[$name] ?? TokenKind::Name for compatibility? + $arr[] = new Token(TokenKind::Name, $start, $start, $len); + + $start += $len; + $fullStart = $start; + } + break; + case \T_COMMENT: + case \T_DOC_COMMENT: + if ($treatCommentsAsTrivia) { + $start += $strlen; + break; + } + // fall through + default: + $newTokenKind = self::TOKEN_MAP[$tokenKind] ?? TokenKind::Unknown; + $arr[] = new Token($newTokenKind, $fullStart, $start, $pos - $fullStart); + $start = $fullStart = $pos; + break; + } + } + + $arr[] = new Token(TokenKind::EndOfFileToken, $fullStart, $start, $pos - $fullStart); + return $arr; + } + + /** + * @param string $content the raw php code + * @param ?int $parseContext can be SourceElements when extracting doc comments. + * Having this available may be useful for subclasses to decide whether or not to post-process results, cache results, etc. + * @return array[]|string[] an array of tokens. When concatenated, these tokens must equal $content. + * + * This exists so that it can be overridden in subclasses, e.g. to cache the result of tokenizing entire files. + * Applications using tolerant-php-parser may often end up needing to use the token stream for other reasons that are hard to do in the resulting AST, + * such as iterating over T_COMMENTS, checking for inline html, + * looking up all tokens (including skipped tokens) on a given line, etc. + */ + protected static function tokenGetAll(string $content, $parseContext): array + { + return @\token_get_all($content); + } + + const TOKEN_MAP = [ + T_CLASS_C => TokenKind::Name, + T_DIR => TokenKind::Name, + T_FILE => TokenKind::Name, + T_FUNC_C => TokenKind::Name, + T_HALT_COMPILER => TokenKind::Name, + T_METHOD_C => TokenKind::Name, + T_NS_C => TokenKind::Name, + T_TRAIT_C => TokenKind::Name, + T_LINE => TokenKind::Name, + + T_STRING => TokenKind::Name, + T_VARIABLE => TokenKind::VariableName, + + T_ABSTRACT => TokenKind::AbstractKeyword, + T_LOGICAL_AND => TokenKind::AndKeyword, + T_ARRAY => TokenKind::ArrayKeyword, + T_AS => TokenKind::AsKeyword, + T_BREAK => TokenKind::BreakKeyword, + T_CALLABLE => TokenKind::CallableKeyword, + T_CASE => TokenKind::CaseKeyword, + T_CATCH => TokenKind::CatchKeyword, + T_CLASS => TokenKind::ClassKeyword, + T_CLONE => TokenKind::CloneKeyword, + T_CONST => TokenKind::ConstKeyword, + T_CONTINUE => TokenKind::ContinueKeyword, + T_DECLARE => TokenKind::DeclareKeyword, + T_DEFAULT => TokenKind::DefaultKeyword, + T_DO => TokenKind::DoKeyword, + T_ECHO => TokenKind::EchoKeyword, + T_ELSE => TokenKind::ElseKeyword, + T_ELSEIF => TokenKind::ElseIfKeyword, + T_EMPTY => TokenKind::EmptyKeyword, + T_ENDDECLARE => TokenKind::EndDeclareKeyword, + T_ENDFOR => TokenKind::EndForKeyword, + T_ENDFOREACH => TokenKind::EndForEachKeyword, + T_ENDIF => TokenKind::EndIfKeyword, + T_ENDSWITCH => TokenKind::EndSwitchKeyword, + T_ENDWHILE => TokenKind::EndWhileKeyword, + T_EVAL => TokenKind::EvalKeyword, + T_EXIT => TokenKind::ExitKeyword, + T_EXTENDS => TokenKind::ExtendsKeyword, + T_FINAL => TokenKind::FinalKeyword, + T_FINALLY => TokenKind::FinallyKeyword, + T_FOR => TokenKind::ForKeyword, + T_FOREACH => TokenKind::ForeachKeyword, + T_FN => TokenKind::FnKeyword, + T_FUNCTION => TokenKind::FunctionKeyword, + T_GLOBAL => TokenKind::GlobalKeyword, + T_GOTO => TokenKind::GotoKeyword, + T_IF => TokenKind::IfKeyword, + T_IMPLEMENTS => TokenKind::ImplementsKeyword, + T_INCLUDE => TokenKind::IncludeKeyword, + T_INCLUDE_ONCE => TokenKind::IncludeOnceKeyword, + T_INSTANCEOF => TokenKind::InstanceOfKeyword, + T_INSTEADOF => TokenKind::InsteadOfKeyword, + T_INTERFACE => TokenKind::InterfaceKeyword, + T_ISSET => TokenKind::IsSetKeyword, + T_LIST => TokenKind::ListKeyword, + T_MATCH => TokenKind::MatchKeyword, + T_NAMESPACE => TokenKind::NamespaceKeyword, + T_NEW => TokenKind::NewKeyword, + T_LOGICAL_OR => TokenKind::OrKeyword, + T_PRINT => TokenKind::PrintKeyword, + T_PRIVATE => TokenKind::PrivateKeyword, + T_PROTECTED => TokenKind::ProtectedKeyword, + T_PUBLIC => TokenKind::PublicKeyword, + T_REQUIRE => TokenKind::RequireKeyword, + T_REQUIRE_ONCE => TokenKind::RequireOnceKeyword, + T_RETURN => TokenKind::ReturnKeyword, + T_STATIC => TokenKind::StaticKeyword, + T_SWITCH => TokenKind::SwitchKeyword, + T_THROW => TokenKind::ThrowKeyword, + T_TRAIT => TokenKind::TraitKeyword, + T_TRY => TokenKind::TryKeyword, + T_UNSET => TokenKind::UnsetKeyword, + T_USE => TokenKind::UseKeyword, + T_VAR => TokenKind::VarKeyword, + T_WHILE => TokenKind::WhileKeyword, + T_LOGICAL_XOR => TokenKind::XorKeyword, + T_YIELD => TokenKind::YieldKeyword, + T_YIELD_FROM => TokenKind::YieldFromKeyword, + + "[" => TokenKind::OpenBracketToken, + "]" => TokenKind::CloseBracketToken, + "(" => TokenKind::OpenParenToken, + ")" => TokenKind::CloseParenToken, + "{" => TokenKind::OpenBraceToken, + "}" => TokenKind::CloseBraceToken, + "." => TokenKind::DotToken, + T_OBJECT_OPERATOR => TokenKind::ArrowToken, + T_NULLSAFE_OBJECT_OPERATOR => TokenKind::QuestionArrowToken, + T_ATTRIBUTE => TokenKind::AttributeToken, + T_INC => TokenKind::PlusPlusToken, + T_DEC => TokenKind::MinusMinusToken, + T_POW => TokenKind::AsteriskAsteriskToken, + "*" => TokenKind::AsteriskToken, + "+" => TokenKind::PlusToken, + "-" => TokenKind::MinusToken, + "~" => TokenKind::TildeToken, + "!" => TokenKind::ExclamationToken, + "$" => TokenKind::DollarToken, + "/" => TokenKind::SlashToken, + "%" => TokenKind::PercentToken, + T_SL => TokenKind::LessThanLessThanToken, + T_SR => TokenKind::GreaterThanGreaterThanToken, + "<" => TokenKind::LessThanToken, + ">" => TokenKind::GreaterThanToken, + T_IS_SMALLER_OR_EQUAL => TokenKind::LessThanEqualsToken, + T_IS_GREATER_OR_EQUAL => TokenKind::GreaterThanEqualsToken, + T_IS_EQUAL => TokenKind::EqualsEqualsToken, + T_IS_IDENTICAL => TokenKind::EqualsEqualsEqualsToken, + T_IS_NOT_EQUAL => TokenKind::ExclamationEqualsToken, + T_IS_NOT_IDENTICAL => TokenKind::ExclamationEqualsEqualsToken, + "^" => TokenKind::CaretToken, + "|" => TokenKind::BarToken, + "&" => TokenKind::AmpersandToken, + T_BOOLEAN_AND => TokenKind::AmpersandAmpersandToken, + T_BOOLEAN_OR => TokenKind::BarBarToken, + ":" => TokenKind::ColonToken, + ";" => TokenKind::SemicolonToken, + "=" => TokenKind::EqualsToken, + T_POW_EQUAL => TokenKind::AsteriskAsteriskEqualsToken, + T_MUL_EQUAL => TokenKind::AsteriskEqualsToken, + T_DIV_EQUAL => TokenKind::SlashEqualsToken, + T_MOD_EQUAL => TokenKind::PercentEqualsToken, + T_PLUS_EQUAL => TokenKind::PlusEqualsToken, + T_MINUS_EQUAL => TokenKind::MinusEqualsToken, + T_CONCAT_EQUAL => TokenKind::DotEqualsToken, + T_SL_EQUAL => TokenKind::LessThanLessThanEqualsToken, + T_SR_EQUAL => TokenKind::GreaterThanGreaterThanEqualsToken, + T_AND_EQUAL => TokenKind::AmpersandEqualsToken, + T_XOR_EQUAL => TokenKind::CaretEqualsToken, + T_OR_EQUAL => TokenKind::BarEqualsToken, + "," => TokenKind::CommaToken, + namespace\T_COALESCE_EQUAL => TokenKind::QuestionQuestionEqualsToken, + T_COALESCE => TokenKind::QuestionQuestionToken, + T_SPACESHIP => TokenKind::LessThanEqualsGreaterThanToken, + T_ELLIPSIS => TokenKind::DotDotDotToken, + T_NS_SEPARATOR => TokenKind::BackslashToken, + T_PAAMAYIM_NEKUDOTAYIM => TokenKind::ColonColonToken, + T_DOUBLE_ARROW => TokenKind::DoubleArrowToken, // TODO missing from spec + + "@" => TokenKind::AtSymbolToken, + "`" => TokenKind::BacktickToken, + "?" => TokenKind::QuestionToken, + + T_LNUMBER => TokenKind::IntegerLiteralToken, + + T_DNUMBER => TokenKind::FloatingLiteralToken, + + T_OPEN_TAG => TokenKind::ScriptSectionStartTag, + T_OPEN_TAG_WITH_ECHO => TokenKind::ScriptSectionStartWithEchoTag, + T_CLOSE_TAG => TokenKind::ScriptSectionEndTag, + + T_INLINE_HTML => TokenKind::InlineHtml, + + "\"" => TokenKind::DoubleQuoteToken, + "'" => TokenKind::SingleQuoteToken, + T_ENCAPSED_AND_WHITESPACE => TokenKind::EncapsedAndWhitespace, + T_DOLLAR_OPEN_CURLY_BRACES => TokenKind::DollarOpenBraceToken, + T_CURLY_OPEN => TokenKind::OpenBraceDollarToken, + T_CONSTANT_ENCAPSED_STRING => TokenKind::StringLiteralToken, + + T_ARRAY_CAST => TokenKind::ArrayCastToken, + T_BOOL_CAST => TokenKind::BoolCastToken, + T_DOUBLE_CAST => TokenKind::DoubleCastToken, + T_INT_CAST => TokenKind::IntCastToken, + T_OBJECT_CAST => TokenKind::ObjectCastToken, + T_STRING_CAST => TokenKind::StringCastToken, + T_UNSET_CAST => TokenKind::UnsetCastToken, + + T_START_HEREDOC => TokenKind::HeredocStart, + T_END_HEREDOC => TokenKind::HeredocEnd, + T_STRING_VARNAME => TokenKind::StringVarname, + T_COMMENT => TokenKind::CommentToken, + T_DOC_COMMENT => TokenKind::DocCommentToken, + T_NUM_STRING => TokenKind::IntegerLiteralToken + ]; + + const PARSE_CONTEXT_TO_PREFIX = [ + ParseContext::SourceElements => "= $textLength) { + $pos = $textLength; + } elseif ($pos < 0) { + $pos = 0; + } + + // Start strrpos check from the character before the current character, + // in case the current character is a newline + $startAt = max(-($textLength - $pos) - 1, -$textLength); + $lastNewlinePos = \strrpos($text, "\n", $startAt); + $char = $pos - ($lastNewlinePos === false ? 0 : $lastNewlinePos + 1); + $line = $pos > 0 ? \substr_count($text, "\n", 0, $pos) : 0; + return new LineCharacterPosition($line, $char); + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Range.php b/bundled-libs/microsoft/tolerant-php-parser/src/Range.php new file mode 100644 index 000000000..36acf40c8 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Range.php @@ -0,0 +1,17 @@ +start = $start; + $this->end = $end; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/ResolvedName.php b/bundled-libs/microsoft/tolerant-php-parser/src/ResolvedName.php new file mode 100644 index 000000000..34acf4fff --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/ResolvedName.php @@ -0,0 +1,44 @@ +kind === TokenKind::Name) { + $name->parts[] = $token->getText($content); + } + } + return $name; + } + + public function addNameParts(array $parts, $content) { + foreach ($parts as $part) { + if ($part->kind === TokenKind::Name && !($part instanceof MissingToken)) { + $this->parts[] = $part->getText($content); + } + } + } + + public function getNameParts() { + return $this->parts; + } + + public function getFullyQualifiedNameText() : string { + return join("\\", $this->parts); + } + + public function __toString() { + return $this->getFullyQualifiedNameText(); + } +} \ No newline at end of file diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/SkippedToken.php b/bundled-libs/microsoft/tolerant-php-parser/src/SkippedToken.php new file mode 100644 index 000000000..4c3174d20 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/SkippedToken.php @@ -0,0 +1,20 @@ +kind, $token->fullStart, $token->start, $token->length); + } + + public function jsonSerialize() { + return array_merge( + ["error" => $this->getTokenKindNameFromValue(TokenKind::SkippedToken)], + parent::jsonSerialize() + ); + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/TextEdit.php b/bundled-libs/microsoft/tolerant-php-parser/src/TextEdit.php new file mode 100644 index 000000000..a967b3812 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/TextEdit.php @@ -0,0 +1,53 @@ +start = $start; + $this->length = $length; + $this->content = $content; + } + + /** + * Applies array of edits to the document, and returns the resulting text. + * Supplied $edits must not overlap, and be ordered by increasing start position. + * + * Note that after applying edits, the original AST should be invalidated. + * + * @param TextEdit[] $edits + * @param string $text + * @return string + */ + public static function applyEdits(array $edits, string $text) : string { + $prevEditStart = PHP_INT_MAX; + for ($i = \count($edits) - 1; $i >= 0; $i--) { + $edit = $edits[$i]; + \assert( + $prevEditStart > $edit->start && $prevEditStart > $edit->start + $edit->length, + "Supplied TextEdit[] must not overlap, and be in increasing start position order." + ); + if ($edit->start < 0 || $edit->length < 0 || $edit->start + $edit->length > \strlen($text)) { + throw new \OutOfBoundsException("Applied TextEdit range out of bounds."); + } + $prevEditStart = $edit->start; + $head = \substr($text, 0, $edit->start); + $tail = \substr($text, $edit->start + $edit->length); + $text = $head . $edit->content . $tail; + } + return $text; + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/Token.php b/bundled-libs/microsoft/tolerant-php-parser/src/Token.php new file mode 100644 index 000000000..f746e8a22 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/Token.php @@ -0,0 +1,134 @@ +kind = $kind; + $this->fullStart = $fullStart; + $this->start = $start; + $this->length = $length; + } + + public function getLeadingCommentsAndWhitespaceText(string $document) : string { + return substr($document, $this->fullStart, $this->start - $this->fullStart); + } + + /** + * @param string|null $document + * @return bool|null|string + */ + public function getText(string $document = null) { + if ($document === null) { + return null; + } + return substr($document, $this->start, $this->length - ($this->start - $this->fullStart)); + } + + public function getFullText(string $document) : string { + return substr($document, $this->fullStart, $this->length); + } + + /** + * @return int + */ + public function getStartPosition() { + return $this->start; + } + + /** + * @return int + */ + public function getFullStart() { + return $this->fullStart; + } + + /** + * @return int + */ + public function getWidth() { + return $this->length + $this->fullStart - $this->start; + } + + /** + * @return int + */ + public function getFullWidth() { + return $this->length; + } + + /** + * @return int + */ + public function getEndPosition() { + return $this->fullStart + $this->length; + } + + /** + * @return string[] - A hash map of the format [int $tokenKind => string $tokenName] + */ + private static function getTokenKindNameFromValueMap() { + static $mapToKindName; + if ($mapToKindName === null) { + $constants = (new \ReflectionClass("Microsoft\\PhpParser\\TokenKind"))->getConstants(); + $mapToKindName = \array_flip($constants); + } + return $mapToKindName; + } + + /** + * Returns the token kind name as a string, or the token number if the name + * was not found. + * + * @param int $kind + * @return int|string + */ + public static function getTokenKindNameFromValue($kind) { + $mapToKindName = self::getTokenKindNameFromValueMap(); + return $mapToKindName[$kind] ?? $kind; + } + + public function jsonSerialize() { + $kindName = $this->getTokenKindNameFromValue($this->kind); + + if (!isset($GLOBALS["SHORT_TOKEN_SERIALIZE"])) { + $GLOBALS["SHORT_TOKEN_SERIALIZE"] = false; + } + + if ($GLOBALS["SHORT_TOKEN_SERIALIZE"]) { + return [ + "kind" => $kindName, + "textLength" => $this->length - ($this->start - $this->fullStart) + ]; + } else { + return [ + "kind" => $kindName, + "fullStart" => $this->fullStart, + "start" => $this->start, + "length" => $this->length + ]; + } + } +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/TokenKind.php b/bundled-libs/microsoft/tolerant-php-parser/src/TokenKind.php new file mode 100644 index 000000000..bd97a0ee3 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/TokenKind.php @@ -0,0 +1,217 @@ + TokenKind::AbstractKeyword, + "and" => TokenKind::AndKeyword, + "array" => TokenKind::ArrayKeyword, + "as" => TokenKind::AsKeyword, + "break" => TokenKind::BreakKeyword, + "callable" => TokenKind::CallableKeyword, + "case" => TokenKind::CaseKeyword, + "catch" => TokenKind::CatchKeyword, + "class" => TokenKind::ClassKeyword, + "clone" => TokenKind::CloneKeyword, + "const" => TokenKind::ConstKeyword, + "continue" => TokenKind::ContinueKeyword, + "declare" => TokenKind::DeclareKeyword, + "default" => TokenKind::DefaultKeyword, + "die" => TokenKind::DieKeyword, + "do" => TokenKind::DoKeyword, + "echo" => TokenKind::EchoKeyword, + "else" => TokenKind::ElseKeyword, + "elseif" => TokenKind::ElseIfKeyword, + "empty" => TokenKind::EmptyKeyword, + "enddeclare" => TokenKind::EndDeclareKeyword, + "endfor" => TokenKind::EndForKeyword, + "endforeach" => TokenKind::EndForEachKeyword, + "endif" => TokenKind::EndIfKeyword, + "endswitch" => TokenKind::EndSwitchKeyword, + "endwhile" => TokenKind::EndWhileKeyword, + "eval" => TokenKind::EvalKeyword, + "exit" => TokenKind::ExitKeyword, + "extends" => TokenKind::ExtendsKeyword, + "final" => TokenKind::FinalKeyword, + "finally" => TokenKind::FinallyKeyword, + "for" => TokenKind::ForKeyword, + "foreach" => TokenKind::ForeachKeyword, + "fn" => TokenKind::FnKeyword, + "function" => TokenKind::FunctionKeyword, + "global" => TokenKind::GlobalKeyword, + "goto" => TokenKind::GotoKeyword, + "if" => TokenKind::IfKeyword, + "implements" => TokenKind::ImplementsKeyword, + "include" => TokenKind::IncludeKeyword, + "include_once" => TokenKind::IncludeOnceKeyword, + "instanceof" => TokenKind::InstanceOfKeyword, + "insteadof" => TokenKind::InsteadOfKeyword, + "interface" => TokenKind::InterfaceKeyword, + "isset" => TokenKind::IsSetKeyword, + "list" => TokenKind::ListKeyword, + "namespace" => TokenKind::NamespaceKeyword, + "new" => TokenKind::NewKeyword, + "or" => TokenKind::OrKeyword, + "print" => TokenKind::PrintKeyword, + "private" => TokenKind::PrivateKeyword, + "protected" => TokenKind::ProtectedKeyword, + "public" => TokenKind::PublicKeyword, + "require" => TokenKind::RequireKeyword, + "require_once" => TokenKind::RequireOnceKeyword, + "return" => TokenKind::ReturnKeyword, + "static" => TokenKind::StaticKeyword, + "switch" => TokenKind::SwitchKeyword, + "throw" => TokenKind::ThrowKeyword, + "trait" => TokenKind::TraitKeyword, + "try" => TokenKind::TryKeyword, + "unset" => TokenKind::UnsetKeyword, + "use" => TokenKind::UseKeyword, + "var" => TokenKind::VarKeyword, + "while" => TokenKind::WhileKeyword, + "xor" => TokenKind::XorKeyword, + "yield" => TokenKind::YieldKeyword, + "yield from" => TokenKind::YieldFromKeyword, + + + // TODO soft reserved words? + ); + + const RESERVED_WORDS = [ + // http://php.net/manual/en/reserved.constants.php + // TRUE, FALSE, NULL are special predefined constants + // TODO - also consider adding other constants + "true" => TokenKind::TrueReservedWord, + "false" => TokenKind::FalseReservedWord, + "null" => TokenKind::NullReservedWord, + + // RESERVED WORDS: + // http://php.net/manual/en/reserved.other-reserved-words.php + "int" => TokenKind::IntReservedWord, + "float" => TokenKind::FloatReservedWord, + "bool" => TokenKind::BoolReservedWord, + "string" => TokenKind::StringReservedWord, + "binary" => TokenKind::BinaryReservedWord, + "boolean" => TokenKind::BooleanReservedWord, + "double" => TokenKind::DoubleReservedWord, + "integer" => TokenKind::IntegerReservedWord, + "object" => TokenKind::ObjectReservedWord, + "real" => TokenKind::RealReservedWord, + "void" => TokenKind::VoidReservedWord + ]; + + const OPERATORS_AND_PUNCTUATORS = array( + "[" => TokenKind::OpenBracketToken, + "]" => TokenKind::CloseBracketToken, + "(" => TokenKind::OpenParenToken, + ")" => TokenKind::CloseParenToken, + "{" => TokenKind::OpenBraceToken, + "}" => TokenKind::CloseBraceToken, + "." => TokenKind::DotToken, + "->" => TokenKind::ArrowToken, + "=>" => TokenKind::DoubleArrowToken, + "++" => TokenKind::PlusPlusToken, + "--" => TokenKind::MinusMinusToken, + "**" => TokenKind::AsteriskAsteriskToken, + "*" => TokenKind::AsteriskToken, + "+" => TokenKind::PlusToken, + "-" => TokenKind::MinusToken, + "~" => TokenKind::TildeToken, + "!" => TokenKind::ExclamationToken, + "$" => TokenKind::DollarToken, + "/" => TokenKind::SlashToken, + "%" => TokenKind::PercentToken, + "<<" => TokenKind::LessThanLessThanToken, + ">>" => TokenKind::GreaterThanGreaterThanToken, + "<" => TokenKind::LessThanToken, + ">" => TokenKind::GreaterThanToken, + "<=" => TokenKind::LessThanEqualsToken, + ">=" => TokenKind::GreaterThanEqualsToken, + "==" => TokenKind::EqualsEqualsToken, + "===" => TokenKind::EqualsEqualsEqualsToken, + "!=" => TokenKind::ExclamationEqualsToken, + "!==" => TokenKind::ExclamationEqualsEqualsToken, + "^" => TokenKind::CaretToken, + "|" => TokenKind::BarToken, + "&" => TokenKind::AmpersandToken, + "&&" => TokenKind::AmpersandAmpersandToken, + "||" => TokenKind::BarBarToken, + "?" => TokenKind::QuestionToken, + ":" => TokenKind::ColonToken, + "::" => TokenKind::ColonColonToken, + ";" => TokenKind::SemicolonToken, + "=" => TokenKind::EqualsToken, + "**=" => TokenKind::AsteriskAsteriskEqualsToken, + "*=" => TokenKind::AsteriskEqualsToken, + "/=" => TokenKind::SlashEqualsToken, + "%=" => TokenKind::PercentEqualsToken, + "+=" => TokenKind::PlusEqualsToken, + "-=" => TokenKind::MinusEqualsToken, + ".=" => TokenKind::DotEqualsToken, + "<<=" => TokenKind::LessThanLessThanEqualsToken, + ">>=" => TokenKind::GreaterThanGreaterThanEqualsToken, + "&=" => TokenKind::AmpersandEqualsToken, + "^=" => TokenKind::CaretEqualsToken, + "|=" => TokenKind::BarEqualsToken, + "," => TokenKind::CommaToken, + "?->" => TokenKind::QuestionArrowToken, + "??" => TokenKind::QuestionQuestionToken, + "??=" => TokenKind::QuestionQuestionEqualsToken, + "<=>" => TokenKind::LessThanEqualsGreaterThanToken, + "<>" => TokenKind::LessThanGreaterThanToken, + "..." => TokenKind::DotDotDotToken, + "\\" => TokenKind::BackslashToken, + " TokenKind::ScriptSectionStartWithEchoTag, // TODO, technically not an operator + " TokenKind::ScriptSectionStartTag, // TODO, technically not an operator + " TokenKind::ScriptSectionStartTag, // TODO add tests + " TokenKind::ScriptSectionStartTag, + " TokenKind::ScriptSectionStartTag, + " TokenKind::ScriptSectionStartTag, + "?>" => TokenKind::ScriptSectionEndTag, // TODO, technically not an operator + "?>\n" => TokenKind::ScriptSectionEndTag, // TODO, technically not an operator + "?>\r\n" => TokenKind::ScriptSectionEndTag, // TODO, technically not an operator + "?>\r" => TokenKind::ScriptSectionEndTag, // TODO, technically not an operator + "@" => TokenKind::AtSymbolToken, // TODO not in spec + "`" => TokenKind::BacktickToken + ); + +// TODO add new tokens +} diff --git a/bundled-libs/microsoft/tolerant-php-parser/src/bootstrap.php b/bundled-libs/microsoft/tolerant-php-parser/src/bootstrap.php new file mode 100644 index 000000000..821c54444 --- /dev/null +++ b/bundled-libs/microsoft/tolerant-php-parser/src/bootstrap.php @@ -0,0 +1,12 @@ +" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/bundled-libs/netresearch/jsonmapper/composer.json b/bundled-libs/netresearch/jsonmapper/composer.json new file mode 100644 index 000000000..b6d277702 --- /dev/null +++ b/bundled-libs/netresearch/jsonmapper/composer.json @@ -0,0 +1,31 @@ +{ + "name": "netresearch/jsonmapper", + "description": "Map nested JSON structures onto PHP classes", + "license": "OSL-3.0", + "autoload": { + "psr-0": {"JsonMapper": "src/"} + }, + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues" + }, + "require":{ + "php": ">=5.6", + "ext-spl": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0", + "squizlabs/php_codesniffer": "~3.5" + } +} diff --git a/bundled-libs/netresearch/jsonmapper/contributing.rst b/bundled-libs/netresearch/jsonmapper/contributing.rst new file mode 100644 index 000000000..1e40f12c1 --- /dev/null +++ b/bundled-libs/netresearch/jsonmapper/contributing.rst @@ -0,0 +1,13 @@ +************************************** +How to add your features to JsonMapper +************************************** + +If you want to add a new feature or a fix to this library, please consider these aspects: + +- Respect the original code style and continue using it - it uses `PEAR Coding Standards`__. +- Pull requests fixing a bug should include a test case that illustrates the wrong behaviour. +- Pull requests adding a new feature should also include a test for the new feature. + + __ http://pear.php.net/manual/en/standards.php + +Having test cases included in your pull request greatly helps reviewing it and will increase the chance of it being merged. diff --git a/bundled-libs/netresearch/jsonmapper/src/JsonMapper.php b/bundled-libs/netresearch/jsonmapper/src/JsonMapper.php new file mode 100644 index 000000000..6977b0fe9 --- /dev/null +++ b/bundled-libs/netresearch/jsonmapper/src/JsonMapper.php @@ -0,0 +1,829 @@ + + * @license OSL-3.0 http://opensource.org/licenses/osl-3.0 + * @link http://cweiske.de/ + */ + +/** + * Automatically map JSON structures into objects. + * + * @category Netresearch + * @package JsonMapper + * @author Christian Weiske + * @license OSL-3.0 http://opensource.org/licenses/osl-3.0 + * @link http://cweiske.de/ + */ +class JsonMapper +{ + /** + * PSR-3 compatible logger object + * + * @link http://www.php-fig.org/psr/psr-3/ + * @var object + * @see setLogger() + */ + protected $logger; + + /** + * Throw an exception when JSON data contain a property + * that is not defined in the PHP class + * + * @var boolean + */ + public $bExceptionOnUndefinedProperty = false; + + /** + * Throw an exception if the JSON data miss a property + * that is marked with @required in the PHP class + * + * @var boolean + */ + public $bExceptionOnMissingData = false; + + /** + * If the types of map() parameters shall be checked. + * + * You have to disable it if you're using the json_decode "assoc" parameter. + * + * json_decode($str, false) + * + * @var boolean + */ + public $bEnforceMapType = true; + + /** + * Throw an exception when an object is expected but the JSON contains + * a non-object type. + * + * @var boolean + */ + public $bStrictObjectTypeChecking = false; + + /** + * Throw an exception, if null value is found + * but the type of attribute does not allow nulls. + * + * @var bool + */ + public $bStrictNullTypes = true; + + /** + * Allow mapping of private and proteted properties. + * + * @var boolean + */ + public $bIgnoreVisibility = false; + + /** + * Remove attributes that were not passed in JSON, + * to avoid confusion between them and NULL values. + * + * @var boolean + */ + public $bRemoveUndefinedAttributes = false; + + /** + * Override class names that JsonMapper uses to create objects. + * Useful when your setter methods accept abstract classes or interfaces. + * + * @var array + */ + public $classMap = array(); + + /** + * Callback used when an undefined property is found. + * + * Works only when $bExceptionOnUndefinedProperty is disabled. + * + * Parameters to this function are: + * 1. Object that is being filled + * 2. Name of the unknown JSON property + * 3. JSON value of the property + * + * @var callable + */ + public $undefinedPropertyHandler = null; + + /** + * Runtime cache for inspected classes. This is particularly effective if + * mapArray() is called with a large number of objects + * + * @var array property inspection result cache + */ + protected $arInspectedClasses = array(); + + /** + * Method to call on each object after deserialization is done. + * + * Is only called if it exists on the object. + * + * @var string|null + */ + public $postMappingMethod = null; + + /** + * Map data all data in $json into the given $object instance. + * + * @param object $json JSON object structure from json_decode() + * @param object $object Object to map $json data into + * + * @return mixed Mapped object is returned. + * @see mapArray() + */ + public function map($json, $object) + { + if ($this->bEnforceMapType && !is_object($json)) { + throw new InvalidArgumentException( + 'JsonMapper::map() requires first argument to be an object' + . ', ' . gettype($json) . ' given.' + ); + } + if (!is_object($object)) { + throw new InvalidArgumentException( + 'JsonMapper::map() requires second argument to be an object' + . ', ' . gettype($object) . ' given.' + ); + } + + $strClassName = get_class($object); + $rc = new ReflectionClass($object); + $strNs = $rc->getNamespaceName(); + $providedProperties = array(); + foreach ($json as $key => $jvalue) { + $key = $this->getSafeName($key); + $providedProperties[$key] = true; + + // Store the property inspection results so we don't have to do it + // again for subsequent objects of the same type + if (!isset($this->arInspectedClasses[$strClassName][$key])) { + $this->arInspectedClasses[$strClassName][$key] + = $this->inspectProperty($rc, $key); + } + + list($hasProperty, $accessor, $type) + = $this->arInspectedClasses[$strClassName][$key]; + + if (!$hasProperty) { + if ($this->bExceptionOnUndefinedProperty) { + throw new JsonMapper_Exception( + 'JSON property "' . $key . '" does not exist' + . ' in object of type ' . $strClassName + ); + } else if ($this->undefinedPropertyHandler !== null) { + call_user_func( + $this->undefinedPropertyHandler, + $object, $key, $jvalue + ); + } else { + $this->log( + 'info', + 'Property {property} does not exist in {class}', + array('property' => $key, 'class' => $strClassName) + ); + } + continue; + } + + if ($accessor === null) { + if ($this->bExceptionOnUndefinedProperty) { + throw new JsonMapper_Exception( + 'JSON property "' . $key . '" has no public setter method' + . ' in object of type ' . $strClassName + ); + } + $this->log( + 'info', + 'Property {property} has no public setter method in {class}', + array('property' => $key, 'class' => $strClassName) + ); + continue; + } + + if ($this->isNullable($type) || !$this->bStrictNullTypes) { + if ($jvalue === null) { + $this->setProperty($object, $accessor, null); + continue; + } + $type = $this->removeNullable($type); + } else if ($jvalue === null) { + throw new JsonMapper_Exception( + 'JSON property "' . $key . '" in class "' + . $strClassName . '" must not be NULL' + ); + } + + $type = $this->getFullNamespace($type, $strNs); + $type = $this->getMappedType($type, $jvalue); + + if ($type === null || $type === 'mixed') { + //no given type - simply set the json data + $this->setProperty($object, $accessor, $jvalue); + continue; + } else if ($this->isObjectOfSameType($type, $jvalue)) { + $this->setProperty($object, $accessor, $jvalue); + continue; + } else if ($this->isSimpleType($type)) { + if ($type === 'string' && is_object($jvalue)) { + throw new JsonMapper_Exception( + 'JSON property "' . $key . '" in class "' + . $strClassName . '" is an object and' + . ' cannot be converted to a string' + ); + } + settype($jvalue, $type); + $this->setProperty($object, $accessor, $jvalue); + continue; + } + + //FIXME: check if type exists, give detailed error message if not + if ($type === '') { + throw new JsonMapper_Exception( + 'Empty type at property "' + . $strClassName . '::$' . $key . '"' + ); + } + + $array = null; + $subtype = null; + if ($this->isArrayOfType($type)) { + //array + $array = array(); + $subtype = substr($type, 0, -2); + } else if (substr($type, -1) == ']') { + list($proptype, $subtype) = explode('[', substr($type, 0, -1)); + if ($proptype == 'array') { + $array = array(); + } else { + $array = $this->createInstance($proptype, false, $jvalue); + } + } else { + if (is_a($type, 'ArrayObject', true)) { + $array = $this->createInstance($type, false, $jvalue); + } + } + + if ($array !== null) { + if (!is_array($jvalue) && $this->isFlatType(gettype($jvalue))) { + throw new JsonMapper_Exception( + 'JSON property "' . $key . '" must be an array, ' + . gettype($jvalue) . ' given' + ); + } + + $cleanSubtype = $this->removeNullable($subtype); + $subtype = $this->getFullNamespace($cleanSubtype, $strNs); + $child = $this->mapArray($jvalue, $array, $subtype, $key); + } else if ($this->isFlatType(gettype($jvalue))) { + //use constructor parameter if we have a class + // but only a flat type (i.e. string, int) + if ($this->bStrictObjectTypeChecking) { + throw new JsonMapper_Exception( + 'JSON property "' . $key . '" must be an object, ' + . gettype($jvalue) . ' given' + ); + } + $child = $this->createInstance($type, true, $jvalue); + } else { + $child = $this->createInstance($type, false, $jvalue); + $this->map($jvalue, $child); + } + $this->setProperty($object, $accessor, $child); + } + + if ($this->bExceptionOnMissingData) { + $this->checkMissingData($providedProperties, $rc); + } + + if ($this->bRemoveUndefinedAttributes) { + $this->removeUndefinedAttributes($object, $providedProperties); + } + + if ($this->postMappingMethod !== null + && $rc->hasMethod($this->postMappingMethod) + ) { + $refDeserializePostMethod = $rc->getMethod( + $this->postMappingMethod + ); + $refDeserializePostMethod->setAccessible(true); + $refDeserializePostMethod->invoke($object); + } + + return $object; + } + + /** + * Convert a type name to a fully namespaced type name. + * + * @param string $type Type name (simple type or class name) + * @param string $strNs Base namespace that gets prepended to the type name + * + * @return string Fully-qualified type name with namespace + */ + protected function getFullNamespace($type, $strNs) + { + if ($type === null || $type === '' || $type[0] == '\\' + || $strNs == '' + ) { + return $type; + } + list($first) = explode('[', $type, 2); + if ($this->isSimpleType($first) || $first === 'mixed') { + return $type; + } + + //create a full qualified namespace + return '\\' . $strNs . '\\' . $type; + } + + /** + * Check required properties exist in json + * + * @param array $providedProperties array with json properties + * @param object $rc Reflection class to check + * + * @throws JsonMapper_Exception + * + * @return void + */ + protected function checkMissingData($providedProperties, ReflectionClass $rc) + { + foreach ($rc->getProperties() as $property) { + $rprop = $rc->getProperty($property->name); + $docblock = $rprop->getDocComment(); + $annotations = $this->parseAnnotations($docblock); + if (isset($annotations['required']) + && !isset($providedProperties[$property->name]) + ) { + throw new JsonMapper_Exception( + 'Required property "' . $property->name . '" of class ' + . $rc->getName() + . ' is missing in JSON data' + ); + } + } + } + + /** + * Remove attributes from object that were not passed in JSON data. + * + * This is to avoid confusion between those that were actually passed + * as NULL, and those that weren't provided at all. + * + * @param object $object Object to remove properties from + * @param array $providedProperties Array with JSON properties + * + * @return void + */ + protected function removeUndefinedAttributes($object, $providedProperties) + { + foreach (get_object_vars($object) as $propertyName => $dummy) { + if (!isset($providedProperties[$propertyName])) { + unset($object->{$propertyName}); + } + } + } + + /** + * Map an array + * + * @param array $json JSON array structure from json_decode() + * @param mixed $array Array or ArrayObject that gets filled with + * data from $json + * @param string $class Class name for children objects. + * All children will get mapped onto this type. + * Supports class names and simple types + * like "string" and nullability "string|null". + * Pass "null" to not convert any values + * @param string $parent_key Defines the key this array belongs to + * in order to aid debugging. + * + * @return mixed Mapped $array is returned + */ + public function mapArray($json, $array, $class = null, $parent_key = '') + { + $originalClass = $class; + foreach ($json as $key => $jvalue) { + $class = $this->getMappedType($originalClass, $jvalue); + if ($class === null) { + $array[$key] = $jvalue; + } else if ($this->isArrayOfType($class)) { + $array[$key] = $this->mapArray( + $jvalue, + array(), + substr($class, 0, -2) + ); + } else if ($this->isFlatType(gettype($jvalue))) { + //use constructor parameter if we have a class + // but only a flat type (i.e. string, int) + if ($jvalue === null) { + $array[$key] = null; + } else { + if ($this->isSimpleType($class)) { + settype($jvalue, $class); + $array[$key] = $jvalue; + } else { + $array[$key] = $this->createInstance( + $class, true, $jvalue + ); + } + } + } else if ($this->isFlatType($class)) { + throw new JsonMapper_Exception( + 'JSON property "' . ($parent_key ? $parent_key : '?') . '"' + . ' is an array of type "' . $class . '"' + . ' but contained a value of type' + . ' "' . gettype($jvalue) . '"' + ); + } else if (is_a($class, 'ArrayObject', true)) { + $array[$key] = $this->mapArray( + $jvalue, + $this->createInstance($class) + ); + } else { + $array[$key] = $this->map( + $jvalue, $this->createInstance($class, false, $jvalue) + ); + } + } + return $array; + } + + /** + * Try to find out if a property exists in a given class. + * Checks property first, falls back to setter method. + * + * @param object $rc Reflection class to check + * @param string $name Property name + * + * @return array First value: if the property exists + * Second value: the accessor to use ( + * ReflectionMethod or ReflectionProperty, or null) + * Third value: type of the property + */ + protected function inspectProperty(ReflectionClass $rc, $name) + { + //try setter method first + $setter = 'set' . $this->getCamelCaseName($name); + + if ($rc->hasMethod($setter)) { + $rmeth = $rc->getMethod($setter); + if ($rmeth->isPublic() || $this->bIgnoreVisibility) { + $rparams = $rmeth->getParameters(); + if (count($rparams) > 0) { + $pclass = $rparams[0]->getClass(); + $nullability = ''; + if ($rparams[0]->allowsNull()) { + $nullability = '|null'; + } + if ($pclass !== null) { + return array( + true, $rmeth, + '\\' . $pclass->getName() . $nullability + ); + } + } + + $docblock = $rmeth->getDocComment(); + $annotations = $this->parseAnnotations($docblock); + + if (!isset($annotations['param'][0])) { + // If there is no annotations (higher priority) inspect + // if there's a scalar type being defined + if (PHP_MAJOR_VERSION >= 7) { + $ptype = $rparams[0]->getType(); + if ($ptype !== null) { + // ReflectionType::__toString() is deprecated + if (PHP_VERSION >= 7.1 + && $ptype instanceof ReflectionNamedType + ) { + $ptype = $ptype->getName(); + } + return array(true, $rmeth, $ptype . $nullability); + } + } + return array(true, $rmeth, null); + } + list($type) = explode(' ', trim($annotations['param'][0])); + return array(true, $rmeth, $type); + } + } + + //now try to set the property directly + //we have to look it up in the class hierarchy + $class = $rc; + $rprop = null; + do { + if ($class->hasProperty($name)) { + $rprop = $class->getProperty($name); + } + } while ($rprop === null && $class = $class->getParentClass()); + + if ($rprop === null) { + //case-insensitive property matching + foreach ($rc->getProperties() as $p) { + if ((strcasecmp($p->name, $name) === 0)) { + $rprop = $p; + break; + } + } + } + if ($rprop !== null) { + if ($rprop->isPublic() || $this->bIgnoreVisibility) { + $docblock = $rprop->getDocComment(); + $annotations = $this->parseAnnotations($docblock); + + if (!isset($annotations['var'][0])) { + return array(true, $rprop, null); + } + + //support "@var type description" + list($type) = explode(' ', $annotations['var'][0]); + + return array(true, $rprop, $type); + } else { + //no setter, private property + return array(true, null, null); + } + } + + //no setter, no property + return array(false, null, null); + } + + /** + * Removes - and _ and makes the next letter uppercase + * + * @param string $name Property name + * + * @return string CamelCasedVariableName + */ + protected function getCamelCaseName($name) + { + return str_replace( + ' ', '', ucwords(str_replace(array('_', '-'), ' ', $name)) + ); + } + + /** + * Since hyphens cannot be used in variables we have to uppercase them. + * + * Technically you may use them, but they are awkward to access. + * + * @param string $name Property name + * + * @return string Name without hyphen + */ + protected function getSafeName($name) + { + if (strpos($name, '-') !== false) { + $name = $this->getCamelCaseName($name); + } + + return $name; + } + + /** + * Set a property on a given object to a given value. + * + * Checks if the setter or the property are public are made before + * calling this method. + * + * @param object $object Object to set property on + * @param object $accessor ReflectionMethod or ReflectionProperty + * @param mixed $value Value of property + * + * @return void + */ + protected function setProperty( + $object, $accessor, $value + ) { + if (!$accessor->isPublic() && $this->bIgnoreVisibility) { + $accessor->setAccessible(true); + } + if ($accessor instanceof ReflectionProperty) { + $accessor->setValue($object, $value); + } else { + //setter method + $accessor->invoke($object, $value); + } + } + + /** + * Create a new object of the given type. + * + * This method exists to be overwritten in child classes, + * so you can do dependency injection or so. + * + * @param string $class Class name to instantiate + * @param boolean $useParameter Pass $parameter to the constructor or not + * @param mixed $jvalue Constructor parameter (the json value) + * + * @return object Freshly created object + */ + protected function createInstance( + $class, $useParameter = false, $jvalue = null + ) { + if ($useParameter) { + return new $class($jvalue); + } else { + $reflectClass = new ReflectionClass($class); + $constructor = $reflectClass->getConstructor(); + if (null === $constructor + || $constructor->getNumberOfRequiredParameters() > 0 + ) { + return $reflectClass->newInstanceWithoutConstructor(); + } + return $reflectClass->newInstance(); + } + } + + /** + * Get the mapped class/type name for this class. + * Returns the incoming classname if not mapped. + * + * @param string $type Type name to map + * @param mixed $jvalue Constructor parameter (the json value) + * + * @return string The mapped type/class name + */ + protected function getMappedType($type, $jvalue = null) + { + if (isset($this->classMap[$type])) { + $target = $this->classMap[$type]; + } else if (is_string($type) && $type !== '' && $type[0] == '\\' + && isset($this->classMap[substr($type, 1)]) + ) { + $target = $this->classMap[substr($type, 1)]; + } else { + $target = null; + } + + if ($target) { + if (is_callable($target)) { + $type = $target($type, $jvalue); + } else { + $type = $target; + } + } + return $type; + } + + /** + * Checks if the given type is a "simple type" + * + * @param string $type type name from gettype() + * + * @return boolean True if it is a simple PHP type + * + * @see isFlatType() + */ + protected function isSimpleType($type) + { + return $type == 'string' + || $type == 'boolean' || $type == 'bool' + || $type == 'integer' || $type == 'int' + || $type == 'double' || $type == 'float' + || $type == 'array' || $type == 'object'; + } + + /** + * Checks if the object is of this type or has this type as one of its parents + * + * @param string $type class name of type being required + * @param mixed $value Some PHP value to be tested + * + * @return boolean True if $object has type of $type + */ + protected function isObjectOfSameType($type, $value) + { + if (false === is_object($value)) { + return false; + } + + return is_a($value, $type); + } + + /** + * Checks if the given type is a type that is not nested + * (simple type except array and object) + * + * @param string $type type name from gettype() + * + * @return boolean True if it is a non-nested PHP type + * + * @see isSimpleType() + */ + protected function isFlatType($type) + { + return $type == 'NULL' + || $type == 'string' + || $type == 'boolean' || $type == 'bool' + || $type == 'integer' || $type == 'int' + || $type == 'double' || $type == 'float'; + } + + /** + * Returns true if type is an array of elements + * (bracket notation) + * + * @param string $strType type to be matched + * + * @return bool + */ + protected function isArrayOfType($strType) + { + return substr($strType, -2) === '[]'; + } + + /** + * Checks if the given type is nullable + * + * @param string $type type name from the phpdoc param + * + * @return boolean True if it is nullable + */ + protected function isNullable($type) + { + return stripos('|' . $type . '|', '|null|') !== false; + } + + /** + * Remove the 'null' section of a type + * + * @param string $type type name from the phpdoc param + * + * @return string The new type value + */ + protected function removeNullable($type) + { + if ($type === null) { + return null; + } + return substr( + str_ireplace('|null|', '|', '|' . $type . '|'), + 1, -1 + ); + } + + /** + * Copied from PHPUnit 3.7.29, Util/Test.php + * + * @param string $docblock Full method docblock + * + * @return array + */ + protected static function parseAnnotations($docblock) + { + $annotations = array(); + // Strip away the docblock header and footer + // to ease parsing of one line annotations + $docblock = substr($docblock, 3, -2); + + $re = '/@(?P[A-Za-z_-]+)(?:[ \t]+(?P.*?))?[ \t]*\r?$/m'; + if (preg_match_all($re, $docblock, $matches)) { + $numMatches = count($matches[0]); + + for ($i = 0; $i < $numMatches; ++$i) { + $annotations[$matches['name'][$i]][] = $matches['value'][$i]; + } + } + + return $annotations; + } + + /** + * Log a message to the $logger object + * + * @param string $level Logging level + * @param string $message Text to log + * @param array $context Additional information + * + * @return null + */ + protected function log($level, $message, array $context = array()) + { + if ($this->logger) { + $this->logger->log($level, $message, $context); + } + } + + /** + * Sets a logger instance on the object + * + * @param LoggerInterface $logger PSR-3 compatible logger object + * + * @return null + */ + public function setLogger($logger) + { + $this->logger = $logger; + } +} +?> diff --git a/bundled-libs/netresearch/jsonmapper/src/JsonMapper/Exception.php b/bundled-libs/netresearch/jsonmapper/src/JsonMapper/Exception.php new file mode 100644 index 000000000..bb8040c60 --- /dev/null +++ b/bundled-libs/netresearch/jsonmapper/src/JsonMapper/Exception.php @@ -0,0 +1,26 @@ + + * @license OSL-3.0 http://opensource.org/licenses/osl-3.0 + * @link http://cweiske.de/ + */ + +/** + * Simple exception + * + * @category Netresearch + * @package JsonMapper + * @author Christian Weiske + * @license OSL-3.0 http://opensource.org/licenses/osl-3.0 + * @link http://cweiske.de/ + */ +class JsonMapper_Exception extends Exception +{ +} +?> diff --git a/bundled-libs/phan/phan/.azure/job.yml b/bundled-libs/phan/phan/.azure/job.yml new file mode 100644 index 000000000..e1a1b7eda --- /dev/null +++ b/bundled-libs/phan/phan/.azure/job.yml @@ -0,0 +1,52 @@ +parameters: + configurationName: '' + phpVersion: '' + +# A reference for testing PECL extensions using azure can be seen at https://github.com/microsoft/msphpsql/blob/master/azure-pipelines.yml +# (that extension also runs unit tests on Windows/Macs) +jobs: + - job: ${{ parameters.configurationName }} + # NOTE: This currently does not use containers. Doing so may be useful for testing on php zts/32-bit. + # Containers need to provide sudo apt-get in order to work, the php:x-cli images don't. + # Containers are slower to start up than the default vm images + pool: + vmImage: ${{ parameters.vmImage }} + steps: + - script: | + VER=${{ parameters.phpVersion }} + # Refresh the cache for the PPA repository, it can be out of date if it's updated recently + if [[ ! -f /usr/bin/phpize$VER ]]; then + sudo add-apt-repository -u ppa:ondrej/php + fi + # Silently try to install the php version if it's available. + # ondrej/php is a minimal install for php 8.0. + sudo apt-get install -y php$VER php$VER-dev php$VER-xml php$VER-mbstring + sudo update-alternatives --set php /usr/bin/php$VER + # Fail the build early if the php version isn't installed on this image + sudo update-alternatives --set phpize /usr/bin/phpize$VER || exit 1 + sudo update-alternatives --set pecl /usr/bin/pecl$VER + sudo update-alternatives --set phar /usr/bin/phar$VER + sudo update-alternatives --set phpdbg /usr/bin/phpdbg$VER + sudo update-alternatives --set php-cgi /usr/bin/php-cgi$VER + sudo update-alternatives --set phar.phar /usr/bin/phar.phar$VER + sudo update-alternatives --set php-config /usr/bin/php-config$VER + displayName: Use PHP version ${{ parameters.phpVersion }} + - script: | + VER=${{ parameters.phpVersion }} + CONF_DIR=/etc/php/$VER/cli/conf.d + + sudo pecl install ast-1.0.10 + php --version + sudo rm -f $CONF_DIR/*xdebug.ini + echo 'extension=ast.so' | sudo tee $CONF_DIR/20-ast.ini + php -m + php --ini + + composer validate + composer --prefer-dist --classmap-authoritative install + pushd internal/paratest; composer --prefer-dist --classmap-authoritative install; popd + displayName: 'Install dependencies' + - script: | + tests/run_all_tests || exit 1 + php -d phar.readonly=0 internal/package.php || exit 1 + displayName: 'Test phan' diff --git a/bundled-libs/phan/phan/.phan/baseline.php.example b/bundled-libs/phan/phan/.phan/baseline.php.example new file mode 100644 index 000000000..02dae0312 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/baseline.php.example @@ -0,0 +1,21 @@ + [ + '.phan/plugins/DuplicateExpressionPlugin.php' => ['PhanTypeMismatchArgumentInternal'], + ], + // 'directory_suppressions' => ['src/directory_name' => ['PhanIssueNames']] can be manually added if needed. + // (directory_suppressions will currently be ignored by subsequent calls to --save-baseline, but may be preserved in future Phan releases) +]; diff --git a/bundled-libs/phan/phan/.phan/bin/mkfilelist b/bundled-libs/phan/phan/.phan/bin/mkfilelist new file mode 100755 index 000000000..deb8b0188 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/bin/mkfilelist @@ -0,0 +1,17 @@ +#!/bin/bash + +if [[ -z $WORKSPACE ]] +then + export WORKSPACE=. +fi +cd $WORKSPACE + +for dir in \ + src \ + tests/Phan \ + vendor/phpunit/phpunit/src vendor/symfony/console +do + if [ -d "$dir" ]; then + find $dir -name '*.php' + fi +done diff --git a/bundled-libs/phan/phan/.phan/bin/phan b/bundled-libs/phan/phan/.phan/bin/phan new file mode 100755 index 000000000..b25f6e890 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/bin/phan @@ -0,0 +1,40 @@ +#!/bin/sh + +# Root directory of project +export ROOT=`git rev-parse --show-toplevel` + +# Phan's directory for executables +export BIN=$ROOT/.phan/bin + +# Phan's data directory +export DATA=$ROOT/.phan/data +mkdir -p $DATA; + +# Go to the root of this git repo +pushd $ROOT > /dev/null + + # Get the current hash of HEAD + export REV=`git rev-parse HEAD` + + # Create the data directory for this run if it + # doesn't exist yet + export RUN=$DATA/$REV + mkdir -p $RUN + + $BIN/mkfilelist > $RUN/files + + # Run the analysis, emitting output to the console + # and using a previous state file. + phan \ + --progress-bar \ + --project-root-directory $ROOT \ + --output $RUN/issues && exit $? + + # Re-link the latest directory + rm -f $ROOT/.phan/data/latest + ln -s $RUN $DATA/latest + + # Output any issues that were found + cat $RUN/issues + +popd > /dev/null diff --git a/bundled-libs/phan/phan/.phan/config.php b/bundled-libs/phan/phan/.phan/config.php new file mode 100644 index 000000000..e39e6b9c0 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/config.php @@ -0,0 +1,675 @@ + null, + + // The PHP version that will be used for feature/syntax compatibility warnings. + // Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`, `null`. + // If this is set to `null`, Phan will first attempt to infer the value from + // the project's composer.json's `{"require": {"php": "version range"}}` if possible. + // If that could not be determined, then Phan assumes `target_php_version`. + // + // For analyzing Phan 3.x, this is determined to be `'7.2'` from `"version": "^7.2.0"`. + 'minimum_target_php_version' => null, + + // Default: true. If this is set to true, + // and target_php_version is newer than the version used to run Phan, + // Phan will act as though functions added in newer PHP versions exist. + // + // NOTE: Currently, this only affects Closure::fromCallable + 'pretend_newer_core_functions_exist' => true, + + // If true, missing properties will be created when + // they are first seen. If false, we'll report an + // error message. + 'allow_missing_properties' => false, + + // Allow null to be cast as any type and for any + // type to be cast to null. + 'null_casts_as_any_type' => false, + + // Allow null to be cast as any array-like type + // This is an incremental step in migrating away from null_casts_as_any_type. + // If null_casts_as_any_type is true, this has no effect. + 'null_casts_as_array' => false, + + // Allow any array-like type to be cast to null. + // This is an incremental step in migrating away from null_casts_as_any_type. + // If null_casts_as_any_type is true, this has no effect. + 'array_casts_as_null' => false, + + // If enabled, Phan will warn if **any** type in a method invocation's object + // is definitely not an object, + // or if **any** type in an invoked expression is not a callable. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_method_checking' => true, + + // If enabled, Phan will warn if **any** type in the argument's union type + // cannot be cast to a type in the parameter's expected union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_param_checking' => true, + + // If enabled, Phan will warn if **any** type in a property assignment's union type + // cannot be cast to a type in the property's declared union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + // (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check) + 'strict_property_checking' => true, + + // If enabled, Phan will warn if **any** type in a returned value's union type + // cannot be cast to the declared return type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + // (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check) + 'strict_return_checking' => true, + + // If enabled, Phan will warn if **any** type of the object expression for a property access + // does not contain that property. + 'strict_object_checking' => true, + + // If enabled, scalars (int, float, bool, string, null) + // are treated as if they can cast to each other. + // This does not affect checks of array keys. See `scalar_array_key_cast`. + 'scalar_implicit_cast' => false, + + // If enabled, any scalar array keys (int, string) + // are treated as if they can cast to each other. + // E.g. `array` can cast to `array` and vice versa. + // Normally, a scalar type such as int could only cast to/from int and mixed. + 'scalar_array_key_cast' => false, + + // If this has entries, scalars (int, float, bool, string, null) + // are allowed to perform the casts listed. + // + // E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]` + // allows casting null to a string, but not vice versa. + // (subset of `scalar_implicit_cast`) + 'scalar_implicit_partial' => [], + + // If true, Phan will convert the type of a possibly undefined array offset to the nullable, defined equivalent. + // If false, Phan will convert the type of a possibly undefined array offset to the defined equivalent (without converting to nullable). + 'convert_possibly_undefined_offset_to_nullable' => false, + + // If true, seemingly undeclared variables in the global + // scope will be ignored. + // + // This is useful for projects with complicated cross-file + // globals that you have no hope of fixing. + 'ignore_undeclared_variables_in_global_scope' => false, + + // Backwards Compatibility Checking (This is very slow) + 'backward_compatibility_checks' => false, + + // If true, check to make sure the return type declared + // in the doc-block (if any) matches the return type + // declared in the method signature. + 'check_docblock_signature_return_type_match' => true, + + // If true, check to make sure the param types declared + // in the doc-block (if any) matches the param types + // declared in the method signature. + 'check_docblock_signature_param_type_match' => true, + + // If true, make narrowed types from phpdoc params override + // the real types from the signature, when real types exist. + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // + // Affects analysis of the body of the method and the param types passed in by callers. + // + // (*Requires `check_docblock_signature_param_type_match` to be true*) + 'prefer_narrowed_phpdoc_param_type' => true, + + // (*Requires `check_docblock_signature_return_type_match` to be true*) + // + // If true, make narrowed types from phpdoc returns override + // the real types from the signature, when real types exist. + // + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // Affects analysis of return statements in the body of the method and the return types passed in by callers. + 'prefer_narrowed_phpdoc_return_type' => true, + + // If enabled, check all methods that override a + // parent method to make sure its signature is + // compatible with the parent's. This check + // can add quite a bit of time to the analysis. + // This will also check if final methods are overridden, etc. + 'analyze_signature_compatibility' => true, + + // Set this to true to allow contravariance in real parameter types of method overrides (Introduced in php 7.2) + // See https://secure.php.net/manual/en/migration72.new-features.php#migration72.new-features.param-type-widening + // (Users may enable this if analyzing projects that support only php 7.2+) + // This is false by default. (Will warn if real parameter types are omitted in an override) + 'allow_method_param_type_widening' => false, + + // Set this to true to make Phan guess that undocumented parameter types + // (for optional parameters) have the same type as default values + // (Instead of combining that type with `mixed`). + // E.g. `function($x = 'val')` would make Phan infer that $x had a type of `string`, not `string|mixed`. + // Phan will not assume it knows specific types if the default value is false or null. + 'guess_unknown_parameter_type_using_default' => false, + + // Allow adding types to vague return types such as @return object, @return ?mixed in function/method/closure union types. + // Normally, Phan only adds inferred returned types when there is no `@return` type or real return type signature.. + // This setting can be disabled on individual methods by adding `@phan-hardcode-return-type` to the doc comment. + // + // Disabled by default. This is more useful with `--analyze-twice`. + 'allow_overriding_vague_return_types' => true, + + // When enabled, infer that the types of the properties of `$this` are equal to their default values at the start of `__construct()`. + // This will have some false positives due to Phan not checking for setters and initializing helpers. + // This does not affect inherited properties. + 'infer_default_properties_in_construct' => true, + + // Set this to true to enable the plugins that Phan uses to infer more accurate return types of `implode`, `json_decode`, and many other functions. + // + // Phan is slightly faster when these are disabled. + 'enable_extended_internal_return_type_plugins' => true, + + // This setting maps case-insensitive strings to union types. + // + // This is useful if a project uses phpdoc that differs from the phpdoc2 standard. + // + // If the corresponding value is the empty string, + // then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`) + // + // If the corresponding value is not empty, + // then Phan will act as though it saw the corresponding UnionTypes(s) + // when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc. + // + // This matches the **entire string**, not parts of the string. + // (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting) + // + // (These are not aliases, this setting is ignored outside of doc comments). + // (Phan does not check if classes with these names exist) + // + // Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']` + 'phpdoc_type_mapping' => [ ], + + // Set to true in order to attempt to detect dead + // (unreferenced) code. Keep in mind that the + // results will only be a guess given that classes, + // properties, constants and methods can be referenced + // as variables (like `$class->$property` or + // `$class->$method()`) in ways that we're unable + // to make sense of. + // + // To more aggressively detect dead code, + // you may want to set `dead_code_detection_prefer_false_negative` to `false`. + 'dead_code_detection' => false, + + // Set to true in order to attempt to detect unused variables. + // `dead_code_detection` will also enable unused variable detection. + // + // This has a few known false positives, e.g. for loops or branches. + 'unused_variable_detection' => true, + + // Set to true in order to force tracking references to elements + // (functions/methods/consts/protected). + // dead_code_detection is another option which also causes references + // to be tracked. + 'force_tracking_references' => false, + + // Set to true in order to attempt to detect redundant and impossible conditions. + // + // This has some false positives involving loops, + // variables set in branches of loops, and global variables. + 'redundant_condition_detection' => true, + + // Set to true in order to attempt to detect error-prone truthiness/falsiness checks. + // + // This is not suitable for all codebases. + 'error_prone_truthy_condition_detection' => true, + + // Enable this to warn about harmless redundant use for classes and namespaces such as `use Foo\bar` in namespace Foo. + // + // Note: This does not affect warnings about redundant uses in the global namespace. + 'warn_about_redundant_use_namespaced_class' => true, + + // If true, then run a quick version of checks that takes less time. + // False by default. + 'quick_mode' => false, + + // If true, then before analysis, try to simplify AST into a form + // which improves Phan's type inference in edge cases. + // + // This may conflict with 'dead_code_detection'. + // When this is true, this slows down analysis slightly. + // + // E.g. rewrites `if ($a = value() && $a > 0) {...}` + // into $a = value(); if ($a) { if ($a > 0) {...}}` + 'simplify_ast' => true, + + // If true, Phan will read `class_alias` calls in the global scope, + // then (1) create aliases from the *parsed* files if no class definition was found, + // and (2) emit issues in the global scope if the source or target class is invalid. + // (If there are multiple possible valid original classes for an aliased class name, + // the one which will be created is unspecified.) + // NOTE: THIS IS EXPERIMENTAL, and the implementation may change. + 'enable_class_alias_support' => false, + + // Enable or disable support for generic templated + // class types. + 'generic_types_enabled' => true, + + // If enabled, warn about throw statement where the exception types + // are not documented in the PHPDoc of functions, methods, and closures. + 'warn_about_undocumented_throw_statements' => true, + + // If enabled (and warn_about_undocumented_throw_statements is enabled), + // warn about function/closure/method calls that have (at)throws + // without the invoking method documenting that exception. + 'warn_about_undocumented_exceptions_thrown_by_invoked_functions' => true, + + // If this is a list, Phan will not warn about lack of documentation of (at)throws + // for any of the listed classes or their subclasses. + // This setting only matters when warn_about_undocumented_throw_statements is true. + // The default is the empty array (Warn about every kind of Throwable) + 'exception_classes_with_optional_throws_phpdoc' => [ + 'LogicException', + 'RuntimeException', + 'InvalidArgumentException', + 'AssertionError', + 'TypeError', + 'Phan\Exception\IssueException', // TODO: Make Phan aware that some arguments suppress certain issues + 'Phan\AST\TolerantASTConverter\InvalidNodeException', // This is used internally in TolerantASTConverter + + // TODO: Undo the suppressions for the below categories of issues: + 'Phan\Exception\CodeBaseException', + // phpunit + 'PHPUnit\Framework\ExpectationFailedException', + 'SebastianBergmann\RecursionContext\InvalidArgumentException', + ], + + // Increase this to properly analyze require_once statements + 'max_literal_string_type_length' => 1000, + + // Setting this to true makes the process assignment for file analysis + // as predictable as possible, using consistent hashing. + // Even if files are added or removed, or process counts change, + // relatively few files will move to a different group. + // (use when the number of files is much larger than the process count) + // NOTE: If you rely on Phan parsing files/directories in the order + // that they were provided in this config, don't use this) + // See https://github.com/phan/phan/wiki/Different-Issue-Sets-On-Different-Numbers-of-CPUs + 'consistent_hashing_file_order' => false, + + // If enabled, Phan will act as though it's certain of real return types of a subset of internal functions, + // even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version). + // + // Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect. + // As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x. + 'assume_real_types_for_internal_functions' => true, + + // Override to hardcode existence and types of (non-builtin) globals. + // Class names should be prefixed with '\\'. + // (E.g. ['_FOO' => '\\FooClass', 'page' => '\\PageClass', 'userId' => 'int']) + 'globals_type_map' => [], + + // The minimum severity level to report on. This can be + // set to Issue::SEVERITY_LOW, Issue::SEVERITY_NORMAL or + // Issue::SEVERITY_CRITICAL. + 'minimum_severity' => Issue::SEVERITY_LOW, + + // Add any issue types (such as `'PhanUndeclaredMethod'`) + // to this list to inhibit them from being reported. + 'suppress_issue_types' => [ + 'PhanUnreferencedClosure', // False positives seen with closures in arrays, TODO: move closure checks closer to what is done by unused variable plugin + 'PhanPluginNoCommentOnProtectedMethod', + 'PhanPluginDescriptionlessCommentOnProtectedMethod', + 'PhanPluginNoCommentOnPrivateMethod', + 'PhanPluginDescriptionlessCommentOnPrivateMethod', + 'PhanPluginDescriptionlessCommentOnPrivateProperty', + // TODO: Fix edge cases in --automatic-fix for PhanPluginRedundantClosureComment + 'PhanPluginRedundantClosureComment', + 'PhanPluginPossiblyStaticPublicMethod', + 'PhanPluginPossiblyStaticProtectedMethod', + // The types of ast\Node->children are all possibly unset. + 'PhanTypePossiblyInvalidDimOffset', + // TODO: Fix PhanParamNameIndicatingUnusedInClosure instances (low priority) + 'PhanParamNameIndicatingUnusedInClosure', + ], + + // If this list is empty, no filter against issues types will be applied. + // If this list is non-empty, only issues within the list + // will be emitted by Phan. + // + // See https://github.com/phan/phan/wiki/Issue-Types-Caught-by-Phan + // for the full list of issues that Phan detects. + // + // Phan is capable of detecting hundreds of types of issues. + // Projects should almost always use `suppress_issue_types` instead. + 'whitelist_issue_types' => [ + // 'PhanUndeclaredClass', + ], + + // A list of files to include in analysis + 'file_list' => [ + 'phan', + 'phan_client', + 'plugins/codeclimate/engine', + 'tool/make_stubs', + 'tool/pdep', + 'tool/phantasm', + 'tool/phoogle', + 'tool/phan_repl_helpers.php', + 'internal/dump_fallback_ast.php', + 'internal/dump_html_styles.php', + 'internal/extract_arg_info.php', + 'internal/internalsignatures.php', + 'internal/line_deleter.php', + 'internal/package.php', + 'internal/reflection_completeness_check.php', + 'internal/sanitycheck.php', + 'vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php', + 'vendor/phpdocumentor/reflection-docblock/src/DocBlockFactory.php', + 'vendor/phpdocumentor/reflection-docblock/src/DocBlock.php', + // 'vendor/phpunit/phpunit/src/Framework/TestCase.php', + ], + + // A regular expression to match files to be excluded + // from parsing and analysis and will not be read at all. + // + // This is useful for excluding groups of test or example + // directories/files, unanalyzable files, or files that + // can't be removed for whatever reason. + // (e.g. '@Test\.php$@', or '@vendor/.*/(tests|Tests)/@') + 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', + + // Enable this to enable checks of require/include statements referring to valid paths. + 'enable_include_path_checks' => true, + + // A list of include paths to check when checking if `require_once`, `include`, etc. are valid. + // + // To refer to the directory of the file being analyzed, use `'.'` + // To refer to the project root directory, you must use \Phan\Config::getProjectRootDirectory() + // + // (E.g. `['.', \Phan\Config::getProjectRootDirectory() . '/src/folder-added-to-include_path']`) + 'include_paths' => ['.'], + + // Enable this to warn about the use of relative paths in `require_once`, `include`, etc. + // Relative paths are harder to reason about, and opcache may have issues with relative paths in edge cases. + 'warn_about_relative_include_statement' => true, + + // A list of files that will be excluded from parsing and analysis + // and will not be read at all. + // + // This is useful for excluding hopelessly unanalyzable + // files that can't be removed for whatever reason. + 'exclude_file_list' => [ + 'internal/Sniffs/ValidUnderscoreVariableNameSniff.php', + ], + + // The number of processes to fork off during the analysis + // phase. + 'processes' => 1, + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in exclude_analysis_directory_list, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'internal/lib', + 'src', + 'tests/Phan', + 'vendor/composer/semver/src', + 'vendor/composer/xdebug-handler/src', + 'vendor/felixfbecker/advanced-json-rpc/lib', + 'vendor/microsoft/tolerant-php-parser/src', + 'vendor/netresearch/jsonmapper/src', + 'vendor/phpunit/phpunit/src', + 'vendor/psr/log/Psr', + 'vendor/sabre/event/lib', + 'vendor/symfony/console', + 'vendor/symfony/polyfill-php80', + '.phan/plugins', + '.phan/stubs', + ], + + // List of case-insensitive file extensions supported by Phan. + // (e.g. php, html, htm) + 'analyzed_file_extensions' => ['php'], + + // A directory list that defines files that will be excluded + // from static analysis, but whose class and method + // information should be included. + // + // Generally, you'll want to include the directories for + // third-party code (such as 'vendor/') in this list. + // + // n.b.: If you'd like to parse but not analyze 3rd + // party code, directories containing that code + // should be added to the `directory_list` as + // to `exclude_analysis_directory_list`. + 'exclude_analysis_directory_list' => [ + 'vendor/' + ], + + // By default, Phan will log error messages to stdout if PHP is using options that slow the analysis. + // (e.g. PHP is compiled with `--enable-debug` or when using Xdebug) + 'skip_slow_php_options_warning' => false, + + // You can put paths to internal stubs in this config option. + // Phan will continue using its detailed type annotations, but load the constants, classes, functions, and classes (and their Reflection types) from these stub files (doubling as valid php files). + // Use a different extension from php to avoid accidentally loading these. + // The 'tool/mkstubs' script can be used to generate your own stubs (compatible with php 7.2+ right now) + // + // Also see `include_extension_subset` to configure Phan to analyze a codebase as if a certain extension is not available. + 'autoload_internal_extension_signatures' => [ + 'ast' => '.phan/internal_stubs/ast.phan_php', + 'ctype' => '.phan/internal_stubs/ctype.phan_php', + 'igbinary' => '.phan/internal_stubs/igbinary.phan_php', + 'mbstring' => '.phan/internal_stubs/mbstring.phan_php', + 'pcntl' => '.phan/internal_stubs/pcntl.phan_php', + 'phar' => '.phan/internal_stubs/phar.phan_php', + 'posix' => '.phan/internal_stubs/posix.phan_php', + 'readline' => '.phan/internal_stubs/readline.phan_php', + 'simplexml' => '.phan/internal_stubs/simplexml.phan_php', + 'sysvmsg' => '.phan/internal_stubs/sysvmsg.phan_php', + 'sysvsem' => '.phan/internal_stubs/sysvsem.phan_php', + 'sysvshm' => '.phan/internal_stubs/sysvshm.phan_php', + ], + + // This can be set to a list of extensions to limit Phan to using the reflection information of. + // If this is a list, then Phan will not use the reflection information of extensions outside of this list. + // The extensions loaded for a given php installation can be seen with `php -m` or `get_loaded_extensions(true)`. + // + // Note that this will only prevent Phan from loading reflection information for extensions outside of this set. + // If you want to add stubs, see `autoload_internal_extension_signatures`. + // + // If this is used, 'core', 'date', 'pcre', 'reflection', 'spl', and 'standard' will be automatically added. + // + // When this is an array, `ignore_undeclared_functions_with_known_signatures` will always be set to false. + // (because many of those functions will be outside of the configured list) + // + // Also see `ignore_undeclared_functions_with_known_signatures` to warn about using unknown functions. + // E.g. this is what Phan would use for self-analysis + /* + 'included_extension_subset' => [ + 'core', + 'standard', + 'filter', + 'json', + 'tokenizer', // parsing php code + 'ast', // parsing php code + + 'ctype', // misc uses, also polyfilled + 'dom', // checkstyle output format + 'iconv', // symfony mbstring polyfill + 'igbinary', // serializing/unserializing polyfilled ASTs + 'libxml', // internal tools for extracting stubs + 'mbstring', // utf-8 support + 'pcntl', // daemon/language server and parallel analysis + 'phar', // packaging + 'posix', // parallel analysis + 'readline', // internal debugging utility, rarely used + 'simplexml', // report generation + 'sysvmsg', // parallelism + 'sysvsem', + 'sysvshm', + ], + */ + + // Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for, + // but aren't available in the codebase, or from Reflection. + // (may lead to false positives if an extension isn't loaded) + // + // If this is true(default), then Phan will not warn. + // + // Even when this is false, Phan will still infer return values and check parameters of internal functions + // if Phan has the signatures. + 'ignore_undeclared_functions_with_known_signatures' => false, + + 'plugin_config' => [ + // A list of 1 or more PHP binaries (Absolute path or program name found in $PATH) + // to use to analyze your files with PHP's native `--syntax-check`. + // + // This can be used to simultaneously run PHP's syntax checks with multiple PHP versions. + // e.g. `'plugin_config' => ['php_native_syntax_check_binaries' => ['php72', 'php70', 'php56']]` + // if all of those programs can be found in $PATH + + // 'php_native_syntax_check_binaries' => [PHP_BINARY], + + // The maximum number of `php --syntax-check` processes to run at any point in time (Minimum: 1). + // This may be temporarily higher if php_native_syntax_check_binaries has more elements than this process count. + 'php_native_syntax_check_max_processes' => 4, + + // List of methods to suppress warnings about for HasPHPDocPlugin + 'has_phpdoc_method_ignore_regex' => '@^Phan\\\\Tests\\\\.*::(test.*|.*Provider)$@', + // Warn about duplicate descriptions for methods and property groups within classes. + // (This skips over deprecated methods) + // This may not apply to all code bases, + // but is useful in avoiding copied and pasted descriptions that may be inapplicable or too vague. + 'has_phpdoc_check_duplicates' => true, + + // If true, then never allow empty statement lists, even if there is a TODO/FIXME/"deliberately empty" comment. + 'empty_statement_list_ignore_todos' => true, + + // Automatically infer which methods are pure (i.e. should have no side effects) in UseReturnValuePlugin. + 'infer_pure_methods' => true, + + // Warn if newline is allowed before end of string for `$` (the default unless the `D` modifier (`PCRE_DOLLAR_ENDONLY`) is passed in). + // This is specific to coding styles. + 'regex_warn_if_newline_allowed_at_end' => true, + ], + + // A list of plugin files to execute + // NOTE: values can be the base name without the extension for plugins bundled with Phan (E.g. 'AlwaysReturnPlugin') + // or relative/absolute paths to the plugin (Relative to the project root). + 'plugins' => [ + 'AlwaysReturnPlugin', + 'DollarDollarPlugin', + 'UnreachableCodePlugin', + 'DuplicateArrayKeyPlugin', + '.phan/plugins/PregRegexCheckerPlugin.php', + 'PrintfCheckerPlugin', + 'PHPUnitAssertionPlugin', // analyze assertSame/assertInstanceof/assertTrue/assertFalse + 'UseReturnValuePlugin', + + // UnknownElementTypePlugin warns about unknown types in element signatures. + 'UnknownElementTypePlugin', + 'DuplicateExpressionPlugin', + // warns about carriage returns("\r"), trailing whitespace, and tabs in PHP files. + 'WhitespacePlugin', + // Warn about inline HTML anywhere in the files. + 'InlineHTMLPlugin', + //////////////////////////////////////////////////////////////////////// + // Plugins for Phan's self-analysis + //////////////////////////////////////////////////////////////////////// + + // Warns about the usage of assert() for Phan's self-analysis. See https://github.com/phan/phan/issues/288 + 'NoAssertPlugin', + 'PossiblyStaticMethodPlugin', + + 'HasPHPDocPlugin', + 'PHPDocToRealTypesPlugin', // suggests replacing (at)return void with `: void` in the declaration, etc. + 'PHPDocRedundantPlugin', + 'PreferNamespaceUsePlugin', + 'EmptyStatementListPlugin', + + // Report empty (not overridden or overriding) methods and functions + // 'EmptyMethodAndFunctionPlugin', + + // This should only be enabled if the code being analyzed contains Phan plugins. + 'PhanSelfCheckPlugin', + // Warn about using the same loop variable name as a loop variable of an outer loop. + 'LoopVariableReusePlugin', + // Warn about assigning the value the variable already had to that variable. + 'RedundantAssignmentPlugin', + // These are specific to Phan's coding style + 'StrictComparisonPlugin', + // Warn about `$var == SOME_INT_OR_STRING_CONST` due to unintuitive behavior such as `0 == 'a'` + 'StrictLiteralComparisonPlugin', + 'ShortArrayPlugin', + 'SimplifyExpressionPlugin', + // 'UnknownClassElementAccessPlugin' is more useful with batch analysis than in an editor. + // It's used in tests/run_test __FakeSelfFallbackTest + + // This checks that there are no accidental echos/printfs left inside Phan's code. + 'RemoveDebugStatementPlugin', + '.phan/plugins/UnsafeCodePlugin.php', + '.phan/plugins/DeprecateAliasPlugin.php', + + //////////////////////////////////////////////////////////////////////// + // End plugins for Phan's self-analysis + //////////////////////////////////////////////////////////////////////// + + // 'SleepCheckerPlugin' is useful for projects which heavily use the __sleep() method. Phan doesn't use __sleep(). + // InvokePHPNativeSyntaxCheckPlugin invokes 'php --no-php-ini --syntax-check ${abs_path_to_analyzed_file}.php' and reports any error messages. + // Using this can cause phan's overall analysis time to more than double. + // 'InvokePHPNativeSyntaxCheckPlugin', + + // 'PHPUnitNotDeadCodePlugin', // Marks PHPUnit test case subclasses and test cases as referenced code. This is only useful for runs when dead code detection is enabled. + + // 'PHPDocInWrongCommentPlugin', // Useful to warn about using "/*" instead of ""/**" where phpdoc annotations are used. This is slow due to needing to tokenize files. + + // NOTE: This plugin only produces correct results when + // Phan is run on a single core (-j1). + // 'UnusedSuppressionPlugin', + ], +]; diff --git a/bundled-libs/phan/phan/.phan/internal_stubs/README.md b/bundled-libs/phan/phan/.phan/internal_stubs/README.md new file mode 100644 index 000000000..27754acfa --- /dev/null +++ b/bundled-libs/phan/phan/.phan/internal_stubs/README.md @@ -0,0 +1,4 @@ +This folder will eventually contain stubs for the latest versions of various extensions. +If the extension is loaded in the binary to run phan, then phan will do nothing. +The plan is to make phan load these files and act as though internal classes, constants, +and functions existed with the same signatures as these php files. diff --git a/bundled-libs/phan/phan/.phan/internal_stubs/ast.phan_php b/bundled-libs/phan/phan/.phan/internal_stubs/ast.phan_php new file mode 100644 index 000000000..ceabb1d7f --- /dev/null +++ b/bundled-libs/phan/phan/.phan/internal_stubs/ast.phan_php @@ -0,0 +1,216 @@ +getFQSEN() !== $method->getDefiningFQSEN()) { + // Check if this was inherited by a descendant class. + return; + } + + if (self::returnTypeOfFunctionLikeAllowsNull($method)) { + return; + } + if (!BlockExitStatusChecker::willUnconditionallyThrowOrReturn($stmts_list)) { + if (!$method->checkHasSuppressIssueAndIncrementCount('PhanPluginAlwaysReturnMethod')) { + self::emitIssue( + $code_base, + $method->getContext(), + 'PhanPluginAlwaysReturnMethod', + "Method {METHOD} has a return type of {TYPE}, but may fail to return a value", + [(string)$method->getFQSEN(), (string)$method->getUnionType()] + ); + } + } + } + + /** + * @param CodeBase $code_base + * The code base in which the function exists + * + * @param Func $function + * A function or closure being analyzed + * + * @override + */ + public function analyzeFunction( + CodeBase $code_base, + Func $function + ): void { + $stmts_list = self::getStatementListToAnalyze($function); + if ($stmts_list === null) { + // check for abstract methods, generators, etc. + return; + } + + if (self::returnTypeOfFunctionLikeAllowsNull($function)) { + return; + } + if (!BlockExitStatusChecker::willUnconditionallyThrowOrReturn($stmts_list)) { + if (!$function->checkHasSuppressIssueAndIncrementCount('PhanPluginAlwaysReturnFunction')) { + self::emitIssue( + $code_base, + $function->getContext(), + 'PhanPluginAlwaysReturnFunction', + "Function {FUNCTION} has a return type of {TYPE}, but may fail to return a value", + [(string)$function->getFQSEN(), (string)$function->getUnionType()] + ); + } + } + } + + /** + * @param Func|Method $func + * @return ?Node - returns null if there's no statement list to analyze + */ + private static function getStatementListToAnalyze($func): ?Node + { + if (!$func->hasNode()) { + return null; + } elseif ($func->hasYield()) { + // generators always return Generator. + return null; + } + $node = $func->getNode(); + if (!$node) { + return null; + } + return $node->children['stmts']; + } + + /** + * @param FunctionInterface $func + * @return bool - Is void(absence of a return type) an acceptable return type. + * NOTE: projects can customize this as needed. + */ + private static function returnTypeOfFunctionLikeAllowsNull(FunctionInterface $func): bool + { + $real_return_type = $func->getRealReturnType(); + if (!$real_return_type->isEmpty() && !$real_return_type->isType(VoidType::instance(false))) { + return false; + } + $return_type = $func->getUnionType(); + return ($return_type->isEmpty() + || $return_type->containsNullableLabeled() + || $return_type->hasType(VoidType::instance(false)) + || $return_type->hasType(NullType::instance(false))); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new AlwaysReturnPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/AvoidableGetterPlugin.php b/bundled-libs/phan/phan/.phan/plugins/AvoidableGetterPlugin.php new file mode 100644 index 000000000..0137fcce7 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/AvoidableGetterPlugin.php @@ -0,0 +1,130 @@ +getProperty()` when the property is accessible, and the getter is not overridden. + */ +class AvoidableGetterPlugin extends PluginV3 implements + PostAnalyzeNodeCapability +{ + + /** + * @return class-string - name of PluginAwarePostAnalysisVisitor subclass + */ + public static function getPostAnalyzeNodeVisitorClassName(): string + { + return AvoidableGetterVisitor::class; + } +} + +/** + * This visitor analyzes node kinds that can be the root of expressions + * containing duplicate expressions, and is called on nodes in post-order. + */ +class AvoidableGetterVisitor extends PluginAwarePostAnalysisVisitor +{ + /** + * @var array maps getter method names to property names. + */ + private $getter_to_property_map = []; + + public function visitClass(Node $node): void + { + if (!$this->context->isInClassScope()) { + // should be impossible + return; + } + $code_base = $this->code_base; + $class = $this->context->getClassInScope($code_base); + $getters = $class->getGettersMap($code_base); + if (!$getters) { + return; + } + $getter_to_property_map = []; + foreach ($getters as $prop_name => $methods) { + $prop_name = (string)$prop_name; + if (!$class->hasPropertyWithName($code_base, $prop_name)) { + continue; + } + if (!$class->getPropertyByName($code_base, $prop_name)->isAccessibleFromClass($code_base, $class->getFQSEN())) { + continue; + } + foreach ($methods as $method) { + if ($method->isOverriddenByAnother()) { + continue; + } + $getter_to_property_map[$method->getName()] = $prop_name; + } + } + if (!$getter_to_property_map) { + return; + } + $this->getter_to_property_map = $getter_to_property_map; + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + $this->recursivelyCheck($node->children['stmts']); + } + + private function recursivelyCheck(Node $node): void + { + switch ($node->kind) { + // TODO: Handle phan-closure-scope. + // case ast\AST_CLOSURE: + // case ast\AST_ARROW_FUNC: + case ast\AST_FUNC_DECL: + case ast\AST_CLASS: + return; + // This only supports instance method getters, not static getters (AST_STATIC_CALL) + case ast\AST_METHOD_CALL: + if (!ConditionVisitorUtil::isThisVarNode($node->children['expr'])) { + break; + } + $method_name = $node->children['method']; + if (is_string($method_name)) { + $property_name = $this->getter_to_property_map[$method_name] ?? null; + if ($property_name !== null) { + $this->warnCanReplaceGetterWithProperty($node, $property_name); + return; + } + } + break; + } + foreach ($node->children as $child_node) { + if ($child_node instanceof Node) { + $this->recursivelyCheck($child_node); + } + } + } + + private function warnCanReplaceGetterWithProperty(Node $node, string $property_name): void + { + $class = $this->context->getClassInScope($this->code_base); + if ($class->isTrait()) { + $issue_name = 'PhanPluginAvoidableGetterInTrait'; + } else { + $issue_name = 'PhanPluginAvoidableGetter'; + } + + $this->emitPluginIssue( + $this->code_base, + (clone($this->context))->withLineNumberStart($node->lineno), + $issue_name, + "Can replace {METHOD} with {PROPERTY}", + [ASTReverter::toShortString($node), '$this->' . $property_name] + ); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. + +return new AvoidableGetterPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/ConstantVariablePlugin.php b/bundled-libs/phan/phan/.phan/plugins/ConstantVariablePlugin.php new file mode 100644 index 000000000..787445136 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/ConstantVariablePlugin.php @@ -0,0 +1,97 @@ +flags & PhanAnnotationAdder::FLAG_INITIALIZES || isset($node->is_reference)) { + return; + } + $var_name = $node->children['name']; + if (!is_string($var_name)) { + return; + } + if ($this->context->isInLoop() || $this->context->isInGlobalScope()) { + return; + } + $parent_node = end($this->parent_node_list); + if ($parent_node instanceof Node) { + switch ($parent_node->kind) { + case ast\AST_IF_ELEM: + // Phan modifies type to match condition before plugins are called. + // --redundant-condition-detection would warn + return; + case ast\AST_ASSIGN_OP: + if ($parent_node->children['var'] === $node) { + return; + } + break; + } + } + $variable = $this->context->getScope()->getVariableByNameOrNull($var_name); + if (!$variable) { + return; + } + $type = $variable->getUnionType(); + if ($type->isPossiblyUndefined()) { + return; + } + $value = $type->getRealUnionType()->asSingleScalarValueOrNullOrSelf(); + if (is_object($value)) { + return; + } + // TODO: Account for methods expecting references + if (is_bool($value)) { + $issue_type = 'PhanPluginConstantVariableBool'; + } elseif (is_null($value)) { + $issue_type = 'PhanPluginConstantVariableNull'; + } else { + $issue_type = 'PhanPluginConstantVariableScalar'; + } + $this->emitPluginIssue( + $this->code_base, + $this->context, + $issue_type, + 'Variable ${VARIABLE} is probably constant with a value of {TYPE}', + [$var_name, $type] + ); + } +} + +return new ConstantVariablePlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/DemoPlugin.php b/bundled-libs/phan/phan/.phan/plugins/DemoPlugin.php new file mode 100644 index 000000000..658860c7e --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/DemoPlugin.php @@ -0,0 +1,228 @@ +getName() === 'Class') { + self::emitIssue( + $code_base, + $class->getContext(), + 'DemoPluginClassName', + "Class {CLASS} cannot be called `Class`", + [(string)$class->getFQSEN()] + ); + } + } + + /** + * @param CodeBase $code_base + * The code base in which the method exists + * + * @param Method $method + * A method being analyzed + * + * @override + */ + public function analyzeMethod( + CodeBase $code_base, + Method $method + ): void { + // As an example, we test to see if the name of the + // method is `function`, and emit an issue if it is. + // NOTE: Placeholders can be found in \Phan\Issue::uncolored_format_string_for_replace + if ($method->getName() === 'function') { + self::emitIssue( + $code_base, + $method->getContext(), + 'DemoPluginMethodName', + "Method {METHOD} cannot be called `function`", + [(string)$method->getFQSEN()] + ); + } + } + + /** + * @param CodeBase $code_base + * The code base in which the function exists + * + * @param Func $function + * A function being analyzed + * + * @override + */ + public function analyzeFunction( + CodeBase $code_base, + Func $function + ): void { + // As an example, we test to see if the name of the + // function is `function`, and emit an issue if it is. + if ($function->getName() === 'function') { + self::emitIssue( + $code_base, + $function->getContext(), + 'DemoPluginFunctionName', + "Function {FUNCTION} cannot be called `function`", + [(string)$function->getFQSEN()] + ); + } + } + + /** + * @param CodeBase $code_base + * The code base in which the property exists + * + * @param Property $property + * A property being analyzed + * + * @override + */ + public function analyzeProperty( + CodeBase $code_base, + Property $property + ): void { + // As an example, we test to see if the name of the + // property is `property`, and emit an issue if it is. + if ($property->getName() === 'property') { + self::emitIssue( + $code_base, + $property->getContext(), + 'DemoPluginPropertyName', + "Property {PROPERTY} should not be called `property`", + [(string)$property->getFQSEN()] + ); + } + } +} + +/** + * When __invoke on this class is called with a node, a method + * will be dispatched based on the `kind` of the given node. + * + * Visitors such as this are useful for defining lots of different + * checks on a node based on its kind. + */ +class DemoNodeVisitor extends PluginAwarePostAnalysisVisitor +{ + // Subclasses should declare protected $parent_node_list as an instance property if they need to know the list. + + // @var list - Set after the constructor is called if an instance property with this name is declared + // protected $parent_node_list; + + // A plugin's visitors should NOT implement visit(), unless they need to. + + /** + * @param Node $node + * A node of kind ast\AST_INSTANCEOF to analyze + * + * @override + */ + public function visitInstanceof(Node $node): void + { + // Debug::printNode($node); + + $class_name = $node->children['class']->children['name'] ?? null; + + // If we can't figure out the name of the class, don't + // bother continuing. + if (!is_string($class_name)) { + return; + } + + // As an example, enforce that we cannot call + // instanceof against 'object'. + if ($class_name === 'object') { + $this->emit( + 'PhanPluginInstanceOfObject', + "Cannot call instanceof against `object`" + ); + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new DemoPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/DeprecateAliasPlugin.php b/bundled-libs/phan/phan/.phan/plugins/DeprecateAliasPlugin.php new file mode 100644 index 000000000..f6b2a2743 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/DeprecateAliasPlugin.php @@ -0,0 +1,393 @@ + 'gettext', + 'add' => 'swfmovie_add', + //'add' => 'swfsprite_add and others', + 'addaction' => 'swfbutton_addAction', + 'addcolor' => 'swfdisplayitem_addColor', + 'addentry' => 'swfgradient_addEntry', + 'addfill' => 'swfshape_addfill', + 'addshape' => 'swfbutton_addShape', + 'addstring' => 'swftext_addString and others', + //'addstring' => 'swftextfield_addString', + 'align' => 'swftextfield_align', + 'chop' => 'rtrim', + 'close' => 'closedir', + 'com_get' => 'com_propget', + 'com_propset' => 'com_propput', + 'com_set' => 'com_propput', + 'die' => 'exit', + 'diskfreespace' => 'disk_free_space', + 'doubleval' => 'floatval', + 'drawarc' => 'swfshape_drawarc', + 'drawcircle' => 'swfshape_drawcircle', + 'drawcubic' => 'swfshape_drawcubic', + 'drawcubicto' => 'swfshape_drawcubicto', + 'drawcurve' => 'swfshape_drawcurve', + 'drawcurveto' => 'swfshape_drawcurveto', + 'drawglyph' => 'swfshape_drawglyph', + 'drawline' => 'swfshape_drawline', + 'drawlineto' => 'swfshape_drawlineto', + 'fbsql' => 'fbsql_db_query', + 'fputs' => 'fwrite', + 'getascent' => 'swffont_getAscent and others', + //'getascent' => 'swftext_getAscent', + 'getdescent' => 'swffont_getDescent and others', + //'getdescent' => 'swftext_getDescent', + 'getheight' => 'swfbitmap_getHeight', + 'getleading' => 'swffont_getLeading and others and others', + //'getleading' => 'swftext_getLeading', + 'getshape1' => 'swfmorph_getShape1', + 'getshape2' => 'swfmorph_getShape2', + 'getwidth' => 'swfbitmap_getWidth and others', + //'getwidth' => 'swffont_getWidth', + //'getwidth' => 'swftext_getWidth', + 'gzputs' => 'gzwrite', + 'i18n_convert' => 'mb_convert_encoding', + 'i18n_discover_encoding' => 'mb_detect_encoding', + 'i18n_http_input' => 'mb_http_input', + 'i18n_http_output' => 'mb_http_output', + 'i18n_internal_encoding' => 'mb_internal_encoding', + 'i18n_ja_jp_hantozen' => 'mb_convert_kana', + 'i18n_mime_header_decode' => 'mb_decode_mimeheader', + 'i18n_mime_header_encode' => 'mb_encode_mimeheader', + 'imap_create' => 'imap_createmailbox', + 'imap_fetchtext' => 'imap_body', + 'imap_getmailboxes' => 'imap_list_full', + 'imap_getsubscribed' => 'imap_lsub_full', + 'imap_header' => 'imap_headerinfo', + 'imap_listmailbox' => 'imap_list', + 'imap_listsubscribed' => 'imap_lsub', + 'imap_rename' => 'imap_renamemailbox', + 'imap_scan' => 'imap_listscan', + 'imap_scanmailbox' => 'imap_listscan', + 'ini_alter' => 'ini_set', + 'is_double' => 'is_float', + 'is_integer' => 'is_int', + 'is_long' => 'is_int', + 'is_real' => 'is_float', + 'is_writeable' => 'is_writable', + 'join' => 'implode', + 'key_exists' => 'array_key_exists', + 'labelframe' => 'swfmovie_labelFrame and others', + //'labelframe' => 'swfsprite_labelFrame', + 'ldap_close' => 'ldap_unbind', + 'magic_quotes_runtime' => 'set_magic_quotes_runtime', + 'mbstrcut' => 'mb_strcut', + 'mbstrlen' => 'mb_strlen', + 'mbstrpos' => 'mb_strpos', + 'mbstrrpos' => 'mb_strrpos', + 'mbsubstr' => 'mb_substr', + 'ming_setcubicthreshold' => 'ming_setCubicThreshold', + 'ming_setscale' => 'ming_setScale', + 'move' => 'swfdisplayitem_move', + 'movepen' => 'swfshape_movepen', + 'movepento' => 'swfshape_movepento', + 'moveto' => 'swfdisplayitem_moveTo and others', + //'moveto' => 'swffill_moveTo', + //'moveto' => 'swftext_moveTo', + 'msql' => 'msql_db_query', + 'msql_createdb' => 'msql_create_db', + 'msql_dbname' => 'msql_result', + 'msql_dropdb' => 'msql_drop_db', + 'msql_fieldflags' => 'msql_field_flags', + 'msql_fieldlen' => 'msql_field_len', + 'msql_fieldname' => 'msql_field_name', + 'msql_fieldtable' => 'msql_field_table', + 'msql_fieldtype' => 'msql_field_type', + 'msql_freeresult' => 'msql_free_result', + 'msql_listdbs' => 'msql_list_dbs', + 'msql_listfields' => 'msql_list_fields', + 'msql_listtables' => 'msql_list_tables', + 'msql_numfields' => 'msql_num_fields', + 'msql_numrows' => 'msql_num_rows', + 'msql_regcase' => 'sql_regcase', + 'msql_selectdb' => 'msql_select_db', + 'msql_tablename' => 'msql_result', + 'mssql_affected_rows' => 'sybase_affected_rows', + 'mssql_close' => 'sybase_close', + 'mssql_connect' => 'sybase_connect', + 'mssql_data_seek' => 'sybase_data_seek', + 'mssql_fetch_array' => 'sybase_fetch_array', + 'mssql_fetch_field' => 'sybase_fetch_field', + 'mssql_fetch_object' => 'sybase_fetch_object', + 'mssql_fetch_row' => 'sybase_fetch_row', + 'mssql_field_seek' => 'sybase_field_seek', + 'mssql_free_result' => 'sybase_free_result', + 'mssql_get_last_message' => 'sybase_get_last_message', + 'mssql_min_client_severity' => 'sybase_min_client_severity', + 'mssql_min_error_severity' => 'sybase_min_error_severity', + 'mssql_min_message_severity' => 'sybase_min_message_severity', + 'mssql_min_server_severity' => 'sybase_min_server_severity', + 'mssql_num_fields' => 'sybase_num_fields', + 'mssql_num_rows' => 'sybase_num_rows', + 'mssql_pconnect' => 'sybase_pconnect', + 'mssql_query' => 'sybase_query', + 'mssql_result' => 'sybase_result', + 'mssql_select_db' => 'sybase_select_db', + 'multcolor' => 'swfdisplayitem_multColor', + 'mysql' => 'mysql_db_query', + 'mysql_createdb' => 'mysql_create_db', + 'mysql_db_name' => 'mysql_result', + 'mysql_dbname' => 'mysql_result', + 'mysql_dropdb' => 'mysql_drop_db', + 'mysql_fieldflags' => 'mysql_field_flags', + 'mysql_fieldlen' => 'mysql_field_len', + 'mysql_fieldname' => 'mysql_field_name', + 'mysql_fieldtable' => 'mysql_field_table', + 'mysql_fieldtype' => 'mysql_field_type', + 'mysql_freeresult' => 'mysql_free_result', + 'mysql_listdbs' => 'mysql_list_dbs', + 'mysql_listfields' => 'mysql_list_fields', + 'mysql_listtables' => 'mysql_list_tables', + 'mysql_numfields' => 'mysql_num_fields', + 'mysql_numrows' => 'mysql_num_rows', + 'mysql_selectdb' => 'mysql_select_db', + 'mysql_tablename' => 'mysql_result', + 'nextframe' => 'swfmovie_nextFrame and others', + //'nextframe' => 'swfsprite_nextFrame', + 'ociassignelem' => 'OCI-Collection::assignElem', + 'ocibindbyname' => 'oci_bind_by_name', + 'ocicancel' => 'oci_cancel', + 'ocicloselob' => 'OCI-Lob::close', + 'ocicollappend' => 'OCI-Collection::append', + 'ocicollassign' => 'OCI-Collection::assign', + 'ocicollmax' => 'OCI-Collection::max', + 'ocicollsize' => 'OCI-Collection::size', + 'ocicolltrim' => 'OCI-Collection::trim', + 'ocicolumnisnull' => 'oci_field_is_null', + 'ocicolumnname' => 'oci_field_name', + 'ocicolumnprecision' => 'oci_field_precision', + 'ocicolumnscale' => 'oci_field_scale', + 'ocicolumnsize' => 'oci_field_size', + 'ocicolumntype' => 'oci_field_type', + 'ocicolumntyperaw' => 'oci_field_type_raw', + 'ocicommit' => 'oci_commit', + 'ocidefinebyname' => 'oci_define_by_name', + 'ocierror' => 'oci_error', + 'ociexecute' => 'oci_execute', + 'ocifetch' => 'oci_fetch', + 'ocifetchinto' => 'oci_fetch_array,', + 'ocifetchstatement' => 'oci_fetch_all', + 'ocifreecollection' => 'OCI-Collection::free', + 'ocifreecursor' => 'oci_free_statement', + 'ocifreedesc' => 'oci_free_descriptor', + 'ocifreestatement' => 'oci_free_statement', + 'ocigetelem' => 'OCI-Collection::getElem', + 'ociinternaldebug' => 'oci_internal_debug', + 'ociloadlob' => 'OCI-Lob::load', + 'ocilogon' => 'oci_connect', + 'ocinewcollection' => 'oci_new_collection', + 'ocinewcursor' => 'oci_new_cursor', + 'ocinewdescriptor' => 'oci_new_descriptor', + 'ocinlogon' => 'oci_new_connect', + 'ocinumcols' => 'oci_num_fields', + 'ociparse' => 'oci_parse', + 'ocipasswordchange' => 'oci_password_change', + 'ociplogon' => 'oci_pconnect', + 'ociresult' => 'oci_result', + 'ocirollback' => 'oci_rollback', + 'ocisavelob' => 'OCI-Lob::save', + 'ocisavelobfile' => 'OCI-Lob::import', + 'ociserverversion' => 'oci_server_version', + 'ocisetprefetch' => 'oci_set_prefetch', + 'ocistatementtype' => 'oci_statement_type', + 'ociwritelobtofile' => 'OCI-Lob::export', + 'ociwritetemporarylob' => 'OCI-Lob::writeTemporary', + 'odbc_do' => 'odbc_exec', + 'odbc_field_precision' => 'odbc_field_len', + 'output' => 'swfmovie_output', + 'pdf_add_outline' => 'pdf_add_bookmark', + 'pg_clientencoding' => 'pg_client_encoding', + 'pg_setclientencoding' => 'pg_set_client_encoding', + 'pos' => 'current', + 'recode' => 'recode_string', + 'remove' => 'swfmovie_remove and others', + // 'remove' => 'swfsprite_remove', + 'rotate' => 'swfdisplayitem_rotate', + 'rotateto' => 'swfdisplayitem_rotateTo and others', + // 'rotateto' => 'swffill_rotateTo', + 'save' => 'swfmovie_save', + 'savetofile' => 'swfmovie_saveToFile', + 'scale' => 'swfdisplayitem_scale', + 'scaleto' => 'swfdisplayitem_scaleTo and others', + // 'scaleto' => 'swffill_scaleTo', + 'setaction' => 'swfbutton_setAction', + 'setbackground' => 'swfmovie_setBackground', + 'setbounds' => 'swftextfield_setBounds', + 'setcolor' => 'swftext_setColor and others', + // 'setcolor' => 'swftextfield_setColor', + 'setdepth' => 'swfdisplayitem_setDepth', + 'setdimension' => 'swfmovie_setDimension', + 'setdown' => 'swfbutton_setDown', + 'setfont' => 'swftext_setFont and others', + // 'setfont' => 'swftextfield_setFont', + 'setframes' => 'swfmovie_setFrames and others', + // 'setframes' => 'swfsprite_setFrames', + 'setheight' => 'swftext_setHeight and others', + // 'setheight' => 'swftextfield_setHeight', + 'sethit' => 'swfbutton_setHit', + 'setindentation' => 'swftextfield_setIndentation', + 'setleftfill' => 'swfshape_setleftfill', + 'setleftmargin' => 'swftextfield_setLeftMargin', + 'setline' => 'swfshape_setline', + 'setlinespacing' => 'swftextfield_setLineSpacing', + 'setmargins' => 'swftextfield_setMargins', + 'setmatrix' => 'swfdisplayitem_setMatrix', + 'setname' => 'swfdisplayitem_setName and others', + // 'setname' => 'swftextfield_setName', + 'setover' => 'swfbutton_setOver', + 'setrate' => 'swfmovie_setRate', + 'setratio' => 'swfdisplayitem_setRatio', + 'setrightfill' => 'swfshape_setrightfill', + 'setrightmargin' => 'swftextfield_setRightMargin', + 'setspacing' => 'swftext_setSpacing', + 'setup' => 'swfbutton_setUp', + 'show_source' => 'highlight_file', + 'sizeof' => 'count', + 'skewx' => 'swfdisplayitem_skewX', + 'skewxto' => 'swfdisplayitem_skewXTo', + // 'skewxto' => 'swffill_skewXTo', + 'skewy' => 'swfdisplayitem_skewY and others', + 'skewyto' => 'swfdisplayitem_skewYTo and others', + // 'skewyto' => 'swffill_skewYTo', + 'snmpwalkoid' => 'snmprealwalk', + 'strchr' => 'strstr', + 'streammp3' => 'swfmovie_streamMp3', + 'swfaction' => 'swfaction_init', + 'swfbitmap' => 'swfbitmap_init', + 'swfbutton' => 'swfbutton_init', + 'swffill' => 'swffill_init', + 'swffont' => 'swffont_init', + 'swfgradient' => 'swfgradient_init', + 'swfmorph' => 'swfmorph_init', + 'swfmovie' => 'swfmovie_init', + 'swfshape' => 'swfshape_init', + 'swfsprite' => 'swfsprite_init', + 'swftext' => 'swftext_init', + 'swftextfield' => 'swftextfield_init', + 'xptr_new_context' => 'xpath_new_context', + // miscellaneous + 'bzclose' => 'fclose', + 'bzflush' => 'fflush', + 'bzwrite' => 'fwrite', + 'checkdnsrr' => 'dns_check_record', + 'dir' => 'getdir', + 'ftp_quit' => 'ftp_close', + 'getmxrr' => 'dns_get_mx', + // 'getrandmax' => 'mt_getrandmax', // confusing because rand is not an alias of mt_rand + 'get_required_files' => 'get_included_files', + 'gmp_div' => 'gmp_div_q', + // This may change in the future + // 'gzclose' => 'fclose', + // 'gzeof' => 'feof', + // 'gzgetc' => 'fgetc', + // 'gzgets' => 'fgets', + // 'gzpassthru' => 'fpassthru', + // 'gzread' => 'fread', + // 'gzrewind' => 'rewind', + // 'gzseek' => 'fseek', + // 'gztell' => 'ftell', + // 'gzwrite' => 'fwrite', + 'ldap_get_values' => 'ldap_get_values_len', + 'ldap_modify' => 'ldap_mod_replace', + 'mysqli_escape_string' => 'mysqli_real_escape_string', + 'mysqli_execute' => 'mysqli_stmt_execute', + 'mysqli_set_opt' => 'mysqli_options', + 'oci_free_cursor' => 'oci_free_statement', + 'openssl_get_privatekey' => 'openssl_pkey_get_private', + 'openssl_get_publickey' => 'openssl_pkey_get_public', + 'pcntl_errno' => 'pcntl_get_last_error', + 'pg_cmdtuples' => 'pg_affected_rows', + 'pg_errormessage' => 'pg_last_error', + 'pg_exec' => 'pg_query', + 'pg_fieldisnull' => 'pg_field_is_null', + 'pg_fieldname' => 'pg_field_name', + 'pg_fieldnum' => 'pg_field_num', + 'pg_fieldprtlen' => 'pg_field_prtlen', + 'pg_fieldsize' => 'pg_field_size', + 'pg_fieldtype' => 'pg_field_type', + 'pg_freeresult' => 'pg_free_result', + 'pg_getlastoid' => 'pg_last_oid', + 'pg_loclose' => 'pg_lo_close', + 'pg_locreate' => 'pg_lo_create', + 'pg_loexport' => 'pg_lo_export', + 'pg_loimport' => 'pg_lo_import', + 'pg_loopen' => 'pg_lo_open', + 'pg_loreadall' => 'pg_lo_read_all', + 'pg_loread' => 'pg_lo_read', + 'pg_lounlink' => 'pg_lo_unlink', + 'pg_lowrite' => 'pg_lo_write', + 'pg_numfields' => 'pg_num_fields', + 'pg_numrows' => 'pg_num_rows', + 'pg_result' => 'pg_fetch_result', + 'posix_errno' => 'posix_get_last_error', + 'session_commit' => 'session_write_close', + 'set_file_buffer' => 'stream_set_write_buffer', + 'snmp_set_oid_numeric_print' => 'snmp_set_oid_output_format', + 'socket_getopt' => 'socket_get_option', + 'socket_get_status' => 'stream_get_meta_data', + 'socket_set_blocking' => 'stream_set_blocking', + 'socket_setopt' => 'socket_set_option', + 'socket_set_timeout' => 'stream_set_timeout', + 'sodium_crypto_scalarmult_base' => 'sodium_crypto_box_publickey_from_secretkey', + 'srand' => 'mt_srand', + 'stream_register_wrapper' => 'stream_wrapper_register', + 'user_error' => 'trigger_error', + ]; + + public function beforeAnalyzePhase(CodeBase $code_base): void + { + foreach (self::KNOWN_ALIASES as $alias => $original_name) { + try { + $fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString($alias); + } catch (Exception $_) { + continue; + } + if (!$code_base->hasFunctionWithFQSEN($fqsen)) { + continue; + } + $function = $code_base->getFunctionByFQSEN($fqsen); + if (!$function->isPHPInternal()) { + continue; + } + $function->setIsDeprecated(true); + if (!$function->getDocComment()) { + $function->setDocComment('/** @deprecated DeprecateAliasPlugin marked this as an alias of ' . + $original_name . (strpos($original_name, ' ') === false ? '()' : '') . '*/'); + } + } + } +} + +if (Config::isIssueFixingPluginEnabled()) { + require_once __DIR__ . '/DeprecateAliasPlugin/fixers.php'; +} + + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new DeprecateAliasPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/DeprecateAliasPlugin/fixers.php b/bundled-libs/phan/phan/.phan/plugins/DeprecateAliasPlugin/fixers.php new file mode 100644 index 000000000..da95464c2 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/DeprecateAliasPlugin/fixers.php @@ -0,0 +1,72 @@ +getLine(); + $reason = (string)$instance->getTemplateParameters()[1]; + if (!preg_match('/Deprecated because: DeprecateAliasPlugin marked this as an alias of (\w+)\(\)/', $reason, $match)) { + return null; + } + $new_name = (string)$match[1]; + + $function_repr = (string)$instance->getTemplateParameters()[0]; + if (!preg_match('/\\\\(\w+)\(\)/', $function_repr, $match)) { + return null; + } + $expected_name = $match[1]; + $edits = []; + foreach ($contents->getNodesAtLine($line) as $node) { + if (!$node instanceof QualifiedName) { + continue; + } + $is_actual_call = $node->parent instanceof CallExpression; + if (!$is_actual_call) { + continue; + } + $file_contents = $contents->getContents(); + $actual_name = strtolower((new NodeUtils($file_contents))->phpParserNameToString($node)); + if ($actual_name !== $expected_name) { + continue; + } + //fwrite(STDERR, "name is: " . get_class($node->parent) . "\n"); + + // They are case-sensitively identical. + // Generate a fix. + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + $start = $node->getStart(); + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + $end = $node->getEndPosition(); + $edits[] = new FileEdit($start, $end, (($file_contents[$start] ?? '') === '\\' ? '\\' : '') . $new_name); + } + if ($edits) { + return new FileEditSet($edits); + } + return null; + }; + IssueFixer::registerFixerClosure( + 'PhanDeprecatedFunctionInternal', + $fix + ); +}); diff --git a/bundled-libs/phan/phan/.phan/plugins/DollarDollarPlugin.php b/bundled-libs/phan/phan/.phan/plugins/DollarDollarPlugin.php new file mode 100644 index 000000000..3564402ef --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/DollarDollarPlugin.php @@ -0,0 +1,78 @@ +children['name'] instanceof Node) { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginDollarDollar', + "$$ Variables are not allowed.", + [] + ); + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new DollarDollarPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/DuplicateArrayKeyPlugin.php b/bundled-libs/phan/phan/.phan/plugins/DuplicateArrayKeyPlugin.php new file mode 100644 index 000000000..fd8acc5ce --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/DuplicateArrayKeyPlugin.php @@ -0,0 +1,365 @@ + value, with `value,`. + * + * @see DollarDollarPlugin for generic plugin documentation. + */ +class DuplicateArrayKeyPlugin extends PluginV3 implements PostAnalyzeNodeCapability +{ + /** + * @return string - name of PluginAwarePostAnalysisVisitor subclass + * @override + */ + public static function getPostAnalyzeNodeVisitorClassName(): string + { + return DuplicateArrayKeyVisitor::class; + } +} + +/** + * This class has visitArray called on all array literals in files to check for potential problems with keys. + * + * When __invoke on this class is called with a node, a method + * will be dispatched based on the `kind` of the given node. + * + * Visitors such as this are useful for defining lots of different + * checks on a node based on its kind. + */ +class DuplicateArrayKeyVisitor extends PluginAwarePostAnalysisVisitor +{ + private const HASH_PREFIX = "\x00__phan_dnu_"; + + // Do not define the visit() method unless a plugin has code and needs to visit most/all node types. + + /** + * @param Node $node + * A switch statement's case statement(AST_SWITCH_LIST) node to analyze + * @override + */ + public function visitSwitchList(Node $node): void + { + $children = $node->children; + if (count($children) <= 1) { + // This plugin will never emit errors if there are 0 or 1 elements. + return; + } + + $case_constant_set = []; + $values_to_check = []; + foreach ($children as $i => $case_node) { + if (!$case_node instanceof Node) { + throw new AssertionError("Switch list must contain nodes"); + } + $case_cond = $case_node->children['cond']; + if ($case_cond === null) { + continue; // This is `default:`. php --syntax-check already checks for duplicates. + } + // Skip array entries without literal keys. (Do it before resolving the key value) + if (!is_scalar($case_cond)) { + $original_case_cond = $case_cond; + $case_cond = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $case_cond)->asSingleScalarValueOrNullOrSelf(); + if (is_object($case_cond)) { + $case_cond = $original_case_cond; + } + } + if (is_string($case_cond)) { + $cond_key = "s$case_cond"; + $values_to_check[$i] = $case_cond; + } elseif (is_int($case_cond)) { + $cond_key = $case_cond; + $values_to_check[$i] = $case_cond; + } elseif (is_bool($case_cond)) { + $cond_key = $case_cond ? "T" : "F"; + $values_to_check[$i] = $case_cond; + } else { + // could be literal null? + $cond_key = ASTHasher::hash($case_cond); + if (!is_object($case_cond)) { + $values_to_check[$i] = $case_cond; + } + } + if (isset($case_constant_set[$cond_key])) { + $normalized_case_cond = is_object($case_cond) ? ASTReverter::toShortString($case_cond) : self::normalizeSwitchKey($case_cond); + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($case_node->lineno), + 'PhanPluginDuplicateSwitchCase', + "Duplicate/Equivalent switch case({STRING_LITERAL}) detected in switch statement - the later entry will be ignored in favor of case {CODE} at line {LINE}.", + [$normalized_case_cond, ASTReverter::toShortString($case_constant_set[$cond_key]->children['cond']), $case_constant_set[$cond_key]->lineno], + Issue::SEVERITY_NORMAL, + Issue::REMEDIATION_A, + 15071 + ); + // Add a fake value to indicate loose equality checks are redundant + $values_to_check[-1] = true; + } + $case_constant_set[$cond_key] = $case_node; + } + if (!isset($values_to_check[-1]) && count($values_to_check) > 1 && !self::areAllSwitchCasesTheSameType($values_to_check)) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument array keys are integers for switch + $this->extendedLooseEqualityCheck($values_to_check, $children); + } + } + + /** + * @param array $values_to_check scalar constant values of case statements + */ + private static function areAllSwitchCasesTheSameType(array $values_to_check): bool + { + $categories = 0; + foreach ($values_to_check as $value) { + if (is_int($value)) { + $categories |= 1; + if ($categories !== 1) { + return false; + } + } elseif (is_string($value)) { + if (is_numeric($value)) { + // This includes float-like strings such as `"1e0"`, which adds ambiguity ("1e0" == "1") + return false; + } + $categories |= 2; + if ($categories !== 2) { + return false; + } + } else { + return false; + } + } + return true; + } + + /** + * Perform a heuristic check if any element is `==` a previous element. + * + * This is intended to perform well for large arrays. + * + * TODO: Do a better job for small arrays. + * @param array $values_to_check + * @param list $children an array of scalars + */ + private function extendedLooseEqualityCheck(array $values_to_check, array $children): void + { + $numeric_set = []; + $fuzzy_numeric_set = []; + foreach ($values_to_check as $i => $value) { + if (is_numeric($value)) { + // For `"1"`, search for `"1foo"`, `"1bar"`, etc. + $value = is_float($value) ? $value : filter_var($value, FILTER_VALIDATE_FLOAT); + $old_index = $numeric_set[$value] ?? $fuzzy_numeric_set[$value] ?? null; + $numeric_set[$value] = $i; + } else { + $value = (float)$value; + // For `"1foo"`, search for `1` but not `"1bar"` + $old_index = $numeric_set[$value] ?? null; + // @phan-suppress-next-line PhanTypeMismatchDimAssignment + $fuzzy_numeric_set[$value] = $i; + } + if ($old_index !== null) { + $this->emitPluginIssue( + $this->code_base, + (clone($this->context))->withLineNumberStart($children[$i]->lineno), + 'PhanPluginDuplicateSwitchCaseLooseEquality', + "Switch case({STRING_LITERAL}) is loosely equivalent (==) to an earlier case ({STRING_LITERAL}) in switch statement - the earlier entry may be chosen instead.", + [self::normalizeSwitchKey($values_to_check[$i]), self::normalizeSwitchKey($values_to_check[$old_index])], + Issue::SEVERITY_NORMAL, + Issue::REMEDIATION_A, + 15072 + ); + } + } + } + + /** + * @param Node $node + * A match expressions's arms list (AST_MATCH_ARM_LIST) node to analyze + * @override + * @suppress PhanPossiblyUndeclaredProperty + */ + public function visitMatchArmList(Node $node): void + { + $children = $node->children; + if (!$children) { + // This plugin will never emit errors if there are 0 elements. + return; + } + + $arm_expr_constant_set = []; + foreach ($children as $arm_node) { + foreach ($arm_node->children['cond']->children ?? [] as $arm_expr_cond) { + if ($arm_expr_cond === null) { + continue; // This is `default:`. php --syntax-check already checks for duplicates. + } + $lineno = $arm_expr_cond->lineno ?? $arm_node->lineno; + // Skip array entries without literal keys. (Do it before resolving the key value) + if (is_object($arm_expr_cond) && ParseVisitor::isConstExpr($arm_expr_cond)) { + // Only infer the value for values not affected by conditions - that will change after the expressions are analyzed + $original_cond = $arm_expr_cond; + $arm_expr_cond = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $arm_expr_cond)->asSingleScalarValueOrNullOrSelf(); + if (is_object($arm_expr_cond)) { + $arm_expr_cond = $original_cond; + } + } + if (is_string($arm_expr_cond)) { + $cond_key = "s$arm_expr_cond"; + } elseif (is_int($arm_expr_cond)) { + $cond_key = $arm_expr_cond; + } elseif (is_bool($arm_expr_cond)) { + $cond_key = $arm_expr_cond ? "T" : "F"; + } else { + // TODO: This seems like it'd be flaky with ast\Node->flags and lineno? + $cond_key = ASTHasher::hash($arm_expr_cond); + } + if (isset($arm_expr_constant_set[$cond_key])) { + $normalized_arm_expr_cond = ASTReverter::toShortString($arm_expr_cond); + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($lineno), + 'PhanPluginDuplicateMatchArmExpression', + "Duplicate match arm expression({STRING_LITERAL}) detected in match expression - the later entry will be ignored in favor of expression {CODE} at line {LINE}.", + [$normalized_arm_expr_cond, ASTReverter::toShortString($arm_expr_constant_set[$cond_key][0]), $arm_expr_constant_set[$cond_key][1]], + Issue::SEVERITY_NORMAL, + Issue::REMEDIATION_A, + 15071 + ); + } + $arm_expr_constant_set[$cond_key] = [$arm_expr_cond, $arm_node->lineno]; + } + } + } + + /** + * @param Node $node + * An array literal(AST_ARRAY) node to analyze + * @override + */ + public function visitArray(Node $node): void + { + $children = $node->children; + if (count($children) <= 1) { + // This plugin will never emit errors if there are 0 or 1 elements. + return; + } + + $has_entry_without_key = false; + $key_set = []; + foreach ($children as $entry) { + if (!($entry instanceof Node)) { + continue; // Triggered by code such as `list(, $a) = $expr`. In php 7.1, the array and list() syntax was unified. + } + $key = $entry->children['key'] ?? null; + // Skip array entries without literal keys. (Do it before resolving the key value) + if (is_null($key)) { + $has_entry_without_key = true; + continue; + } + if (is_object($key)) { + $key = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $key)->asSingleScalarValueOrNullOrSelf(); + if (is_object($key)) { + $key = self::HASH_PREFIX . ASTHasher::hash($entry->children['key']); + } + } + + if (isset($key_set[$key])) { + // @phan-suppress-next-line PhanTypeMismatchDimFetchNullable + $this->warnAboutDuplicateArrayKey($entry, $key, $key_set[$key]); + } + // @phan-suppress-next-line PhanTypeMismatchDimAssignment + $key_set[$key] = $entry; + } + if ($has_entry_without_key && count($key_set) > 0) { + // This is probably a typo in most codebases. (e.g. ['foo' => 'bar', 'baz']) + // In phan, InternalFunctionSignatureMap.php does this deliberately with the first parameter being the return type. + $this->emit( + 'PhanPluginMixedKeyNoKey', + "Should not mix array entries of the form [key => value,] with entries of the form [value,].", + [], + Issue::SEVERITY_NORMAL, + Issue::REMEDIATION_A, + 15071 + ); + } + } + + /** + * @param int|string|float|bool|null $key + */ + private function warnAboutDuplicateArrayKey(Node $entry, $key, Node $old_entry): void + { + if (is_string($key) && strncmp($key, self::HASH_PREFIX, strlen(self::HASH_PREFIX)) === 0) { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($entry->lineno), + 'PhanPluginDuplicateArrayKeyExpression', + "Duplicate dynamic array key expression ({CODE}) detected in array - the earlier entry at line {LINE} will be ignored if the expression had the same value.", + [ASTReverter::toShortString($entry->children['key']), $old_entry->lineno], + Issue::SEVERITY_NORMAL, + Issue::REMEDIATION_A, + 15071 + ); + return; + } + $normalized_key = self::normalizeKey($key); + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($entry->lineno), + 'PhanPluginDuplicateArrayKey', + "Duplicate/Equivalent array key value({STRING_LITERAL}) detected in array - the earlier entry {CODE} at line {LINE} will be ignored.", + [$normalized_key, ASTReverter::toShortString($old_entry->children['key']), $old_entry->lineno], + Issue::SEVERITY_NORMAL, + Issue::REMEDIATION_A, + 15071 + ); + } + + /** + * Converts a key to the value it would be if used as a case. + * E.g. 0, 0.5, and "0" all become the same value(0) when used as an array key. + * + * @param int|string|float|bool|null $key - The array key literal to be normalized. + * @return string - The normalized representation. + */ + private static function normalizeSwitchKey($key): string + { + if (is_int($key)) { + return (string)$key; + } elseif (!is_string($key)) { + return (string)json_encode($key); + } + $tmp = [$key => true]; + return ASTReverter::toShortString(key($tmp)); + } + + /** + * Converts a key to the value it would be if used as an array key. + * E.g. 0, 0.5, and "0" all become the same value(0) when used as an array key. + * + * @param int|string|float|bool|null $key - The array key literal to be normalized. + * @return string - The normalized representation. + */ + private static function normalizeKey($key): string + { + if (is_int($key)) { + return (string)$key; + } + $tmp = [$key => true]; + return ASTReverter::toShortString(key($tmp)); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new DuplicateArrayKeyPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/DuplicateConstantPlugin.php b/bundled-libs/phan/phan/.phan/plugins/DuplicateConstantPlugin.php new file mode 100644 index 000000000..056727abc --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/DuplicateConstantPlugin.php @@ -0,0 +1,114 @@ +children) <= 1) { + return; + } + $declarations = []; + + foreach ($node->children as $child) { + if (!$child instanceof Node) { + continue; + } + if ($child->kind === ast\AST_CONST_DECL) { + foreach ($child->children as $const) { + if (!$const instanceof Node) { + continue; + } + $name = (string) $const->children['name']; + if (isset($declarations[$name])) { + $this->warnDuplicateConstant($name, $declarations[$name], $const); + } else { + $declarations[$name] = $const; + } + } + } elseif ($child->kind === ast\AST_CALL) { + $expr = $child->children['expr']; + if ($expr instanceof Node && $expr->kind === ast\AST_NAME && strcasecmp((string) $expr->children['name'], 'define') === 0) { + $name = $child->children['args']->children[0] ?? null; + if (is_string($name)) { + if (isset($declarations[$name])) { + $this->warnDuplicateConstant($name, $declarations[$name], $expr); + } else { + $declarations[$name] = $expr; + } + } + } + } + } + } + + private function warnDuplicateConstant(string $name, Node $original_def, Node $new_def): void + { + $this->emitPluginIssue( + $this->code_base, + (clone $this->context)->withLineNumberStart($new_def->lineno), + 'PhanPluginDuplicateConstant', + 'Constant {CONST} was previously declared at line {LINE} - the previous declaration will be used instead', + [$name, $original_def->lineno] + ); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new DuplicateConstantPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/DuplicateExpressionPlugin.php b/bundled-libs/phan/phan/.phan/plugins/DuplicateExpressionPlugin.php new file mode 100644 index 000000000..1abc652be --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/DuplicateExpressionPlugin.php @@ -0,0 +1,572 @@ + true, + flags\BINARY_BOOL_OR => true, + flags\BINARY_BOOL_XOR => true, + flags\BINARY_BITWISE_OR => true, + flags\BINARY_BITWISE_AND => true, + flags\BINARY_BITWISE_XOR => true, + flags\BINARY_SUB => true, + flags\BINARY_DIV => true, + flags\BINARY_MOD => true, + flags\BINARY_IS_IDENTICAL => true, + flags\BINARY_IS_NOT_IDENTICAL => true, + flags\BINARY_IS_EQUAL => true, + flags\BINARY_IS_NOT_EQUAL => true, + flags\BINARY_IS_SMALLER => true, + flags\BINARY_IS_SMALLER_OR_EQUAL => true, + flags\BINARY_IS_GREATER => true, + flags\BINARY_IS_GREATER_OR_EQUAL => true, + flags\BINARY_SPACESHIP => true, + flags\BINARY_COALESCE => true, + ]; + + /** + * A subset of REDUNDANT_BINARY_OP_SET. + * + * These binary operations will make this plugin warn if both sides are literals. + */ + private const BINARY_OP_BOTH_LITERAL_WARN_SET = [ + flags\BINARY_BOOL_AND => true, + flags\BINARY_BOOL_OR => true, + flags\BINARY_BOOL_XOR => true, + flags\BINARY_IS_IDENTICAL => true, + flags\BINARY_IS_NOT_IDENTICAL => true, + flags\BINARY_IS_EQUAL => true, + flags\BINARY_IS_NOT_EQUAL => true, + flags\BINARY_IS_SMALLER => true, + flags\BINARY_IS_SMALLER_OR_EQUAL => true, + flags\BINARY_IS_GREATER => true, + flags\BINARY_IS_GREATER_OR_EQUAL => true, + flags\BINARY_SPACESHIP => true, + flags\BINARY_COALESCE => true, + ]; + + /** + * @param Node $node + * A binary operation node to analyze + * @override + * @suppress PhanAccessClassConstantInternal + */ + public function visitBinaryOp(Node $node): void + { + $flags = $node->flags; + if (!\array_key_exists($flags, self::REDUNDANT_BINARY_OP_SET)) { + // Nothing to warn about + return; + } + $left = $node->children['left']; + $right = $node->children['right']; + if (ASTHasher::hash($left) === ASTHasher::hash($right)) { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginDuplicateExpressionBinaryOp', + 'Both sides of the binary operator {OPERATOR} are the same: {CODE}', + [ + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + ASTReverter::toShortString($left), + ] + ); + return; + } + if (!\array_key_exists($flags, self::BINARY_OP_BOTH_LITERAL_WARN_SET)) { + return; + } + if ($left instanceof Node) { + $left = self::resolveLiteralValue($left); + if ($left instanceof Node) { + return; + } + } + if ($right instanceof Node) { + $right = self::resolveLiteralValue($right); + if ($right instanceof Node) { + return; + } + } + try { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument TODO: handle + $result_representation = ASTReverter::toShortString(InferValue::computeBinaryOpResult($left, $right, $flags)); + } catch (Error $_) { + $result_representation = '(unknown)'; + } + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginBothLiteralsBinaryOp', + 'Suspicious usage of a binary operator where both operands are literals. Expression: {CODE} {OPERATOR} {CODE} (result is {CODE})', + [ + ASTReverter::toShortString($left), + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$flags], + ASTReverter::toShortString($right), + $result_representation, + ] + ); + } + + /** + * @param Node $node + * An assignment operation node to analyze + * @override + */ + public function visitAssignRef(Node $node): void + { + $this->visitAssign($node); + } + + private const ASSIGN_OP_FLAGS = [ + flags\BINARY_BITWISE_OR => '|', + flags\BINARY_BITWISE_AND => '&', + flags\BINARY_BITWISE_XOR => '^', + flags\BINARY_CONCAT => '.', + flags\BINARY_ADD => '+', + flags\BINARY_SUB => '-', + flags\BINARY_MUL => '*', + flags\BINARY_DIV => '/', + flags\BINARY_MOD => '%', + flags\BINARY_POW => '**', + flags\BINARY_SHIFT_LEFT => '<<', + flags\BINARY_SHIFT_RIGHT => '>>', + flags\BINARY_COALESCE => '??', + ]; + + /** + * @param Node $node + * An assignment operation node to analyze + * @override + */ + public function visitAssign(Node $node): void + { + $expr = $node->children['expr']; + if (!$expr instanceof Node) { + // Guaranteed not to contain duplicate expressions in valid php assignments. + return; + } + $var = $node->children['var']; + if ($expr->kind === ast\AST_BINARY_OP) { + $op_str = self::ASSIGN_OP_FLAGS[$expr->flags] ?? null; + if (is_string($op_str) && ASTHasher::hash($var) === ASTHasher::hash($expr->children['left'])) { + $message = 'Can simplify this assignment to {CODE} {OPERATOR} {CODE}'; + if ($expr->flags === ast\flags\BINARY_COALESCE) { + if (Config::get_closest_minimum_target_php_version_id() < 70400) { + return; + } + $message .= ' (requires php version 7.4 or newer)'; + } + + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginDuplicateExpressionAssignmentOperation', + $message, + [ + ASTReverter::toShortString($var), + $op_str . '=', + ASTReverter::toShortString($expr->children['right']), + ] + ); + } + return; + } + if (ASTHasher::hash($var) === ASTHasher::hash($expr)) { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginDuplicateExpressionAssignment', + 'Both sides of the assignment {OPERATOR} are the same: {CODE}', + [ + $node->kind === ast\AST_ASSIGN_REF ? '=&' : '=', + ASTReverter::toShortString($var), + ] + ); + return; + } + } + + /** + * @return bool|null|Node the resolved value of $node, or $node if it could not be resolved + * This could be more permissive about what constants are allowed (e.g. user-defined constants, real constants like PI, etc.), + * but that may cause more false positives. + */ + private static function resolveLiteralValue(Node $node) + { + if ($node->kind !== ast\AST_CONST) { + return $node; + } + // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal + switch (\strtolower($node->children['name']->children['name'] ?? null)) { + case 'false': + return false; + case 'true': + return true; + case 'null': + return null; + default: + return $node; + } + } + + /** + * @param Node $node + * A binary operation node to analyze + * @override + */ + public function visitConditional(Node $node): void + { + $cond_node = $node->children['cond']; + $true_node_hash = ASTHasher::hash($node->children['true']); + + if (ASTHasher::hash($cond_node) === $true_node_hash) { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginDuplicateConditionalTernaryDuplication', + '"X ? X : Y" can usually be simplified to "X ?: Y". The duplicated expression X was {CODE}', + [ASTReverter::toShortString($cond_node)] + ); + return; + } + $false_node_hash = ASTHasher::hash($node->children['false']); + if ($true_node_hash === $false_node_hash) { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginDuplicateConditionalUnnecessary', + '"X ? Y : Y" results in the same expression Y no matter what X evaluates to. Y was {CODE}', + [ASTReverter::toShortString($cond_node)] + ); + return; + } + + if (!$cond_node instanceof Node) { + return; + } + switch ($cond_node->kind) { + case ast\AST_ISSET: + if (ASTHasher::hash($cond_node->children['var']) === $true_node_hash) { + $this->warnDuplicateConditionalNullCoalescing('isset(X) ? X : Y', $node->children['true']); + } + break; + case ast\AST_BINARY_OP: + $this->checkBinaryOpOfConditional($cond_node, $true_node_hash); + break; + case ast\AST_UNARY_OP: + $this->checkUnaryOpOfConditional($cond_node, $true_node_hash); + break; + } + } + + /** + * @param Node $node + * A statement list of kind ast\AST_STMT_LIST to analyze. + * @override + */ + public function visitStmtList(Node $node): void + { + $children = $node->children; + if (count($children) < 2) { + return; + } + $prev_hash = null; + foreach ($children as $child) { + $hash = ASTHasher::hash($child); + if ($hash === $prev_hash) { + $this->emitPluginIssue( + $this->code_base, + (clone($this->context))->withLineNumberStart($child->lineno ?? $node->lineno), + 'PhanPluginDuplicateAdjacentStatement', + "Statement {CODE} is a duplicate of the statement on the above line. Suppress this issue instance if there's a good reason for this.", + [ASTReverter::toShortString($child)] + ); + } + $prev_hash = $hash; + } + } + + /** + * @param int|string $true_node_hash + */ + private function checkBinaryOpOfConditional(Node $cond_node, $true_node_hash): void + { + if ($cond_node->flags !== ast\flags\BINARY_IS_NOT_IDENTICAL) { + return; + } + $left_node = $cond_node->children['left']; + $right_node = $cond_node->children['right']; + if (self::isNullConstantNode($left_node)) { + if (ASTHasher::hash($right_node) === $true_node_hash) { + $this->warnDuplicateConditionalNullCoalescing('null !== X ? X : Y', $right_node); + } + } elseif (self::isNullConstantNode($right_node)) { + if (ASTHasher::hash($left_node) === $true_node_hash) { + $this->warnDuplicateConditionalNullCoalescing('X !== null ? X : Y', $left_node); + } + } + } + + /** + * @param int|string $true_node_hash + */ + private function checkUnaryOpOfConditional(Node $cond_node, $true_node_hash): void + { + if ($cond_node->flags !== ast\flags\UNARY_BOOL_NOT) { + return; + } + $expr = $cond_node->children['expr']; + if (!$expr instanceof Node) { + return; + } + if ($expr->kind === ast\AST_CALL) { + $function = $expr->children['expr']; + if (!$function instanceof Node || + $function->kind !== ast\AST_NAME || + strcasecmp((string)($function->children['name'] ?? ''), 'is_null') !== 0 + ) { + return; + } + $args = $expr->children['args']->children; + if (count($args) !== 1) { + return; + } + if (ASTHasher::hash($args[0]) === $true_node_hash) { + $this->warnDuplicateConditionalNullCoalescing('!is_null(X) ? X : Y', $args[0]); + } + } + } + + /** + * @param Node|mixed $node + */ + private static function isNullConstantNode($node): bool + { + if (!$node instanceof Node) { + return false; + } + return $node->kind === ast\AST_CONST && strcasecmp((string)($node->children['name']->children['name'] ?? ''), 'null') === 0; + } + + /** + * @param ?(Node|string|int|float) $x_node + */ + private function warnDuplicateConditionalNullCoalescing(string $expr, $x_node): void + { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginDuplicateConditionalNullCoalescing', + '"' . $expr . '" can usually be simplified to "X ?? Y" in PHP 7. The duplicated expression X was {CODE}', + [ASTReverter::toShortString($x_node)] + ); + } +} + +/** + * This visitor analyzes node kinds that can be the root of expressions + * containing duplicate expressions, and is called on nodes in pre-order. + */ +class RedundantNodePreAnalysisVisitor extends PluginAwarePreAnalysisVisitor +{ + /** + * @override + */ + public function visitIf(Node $node): void + { + if (count($node->children) <= 1) { + // There can't be any duplicates. + return; + } + // @phan-suppress-next-line PhanUndeclaredProperty + if (isset($node->is_inside_else)) { + return; + } + $children = self::extractIfElseifChain($node); + // The checks of visitIf are done in pre-order (parent nodes analyzed before child nodes) + // so that checked_duplicate_if can be set, to avoid redundant work. + // @phan-suppress-next-line PhanUndeclaredProperty + if (isset($node->checked_duplicate_if)) { + return; + } + // @phan-suppress-next-line PhanUndeclaredProperty + $node->checked_duplicate_if = true; + ['cond' => $prev_cond /*, 'stmts' => $prev_stmts */] = $children[0]->children; + // $prev_stmts_hash = ASTHasher::hash($prev_cond); + $condition_set = [ASTHasher::hash($prev_cond) => true]; + $N = count($children); + for ($i = 1; $i < $N; $i++) { + ['cond' => $cond /*, 'stmts' => $stmts */] = $children[$i]->children; + $cond_hash = ASTHasher::hash($cond); + if (isset($condition_set[$cond_hash])) { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($cond->lineno ?? $children[$i]->lineno), + 'PhanPluginDuplicateIfCondition', + 'Saw the same condition {CODE} in an earlier if/elseif statement', + [ASTReverter::toShortString($cond)] + ); + } else { + $condition_set[$cond_hash] = true; + } + } + if (!isset($cond)) { + $stmts = $children[$N - 1]->children['stmts']; + if (($stmts->children ?? null) && ASTHasher::hash($stmts) === ASTHasher::hash($children[$N - 2]->children['stmts'])) { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($children[$N - 1]->lineno), + 'PhanPluginDuplicateIfStatements', + 'The statements of the else duplicate the statements of the previous if/elseif statement with condition {CODE}', + [ASTReverter::toShortString($children[$N - 2]->children['cond'])] + ); + } + } + } + + /** + * Visit a node of kind ast\AST_TRY, to check for adjacent catch blocks + * + * @override + * @suppress PhanPossiblyUndeclaredProperty + */ + public function visitTry(Node $node): void + { + if (Config::get_closest_target_php_version_id() < 70100) { + return; + } + $catches = $node->children['catches']->children ?? []; + $n = count($catches); + if ($n <= 1) { + // There can't be any duplicates. + return; + } + $prev_hash = ASTHasher::hash($catches[0]->children['stmts']) . ASTHasher::hash($catches[0]->children['var']); + for ($i = 1; $i < $n; $prev_hash = $cur_hash, $i++) { + $cur_hash = ASTHasher::hash($catches[$i]->children['stmts']) . ASTHasher::hash($catches[$i]->children['var']); + if ($prev_hash === $cur_hash) { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($catches[$i]->lineno), + 'PhanPluginDuplicateCatchStatementBody', + 'The implementation of catch({CODE}) and catch({CODE}) are identical, and can be combined if the application only needs to supports php 7.1 and newer', + [ + ASTReverter::toShortString($catches[$i - 1]->children['class']), + ASTReverter::toShortString($catches[$i]->children['class']), + ] + ); + } + } + } + + /** + * @param Node $node a node of kind ast\AST_IF + * @return list the list of AST_IF_ELEM nodes making up the chain of if/elseif/else if conditions. + * @suppress PhanPartialTypeMismatchReturn + */ + private static function extractIfElseifChain(Node $node): array + { + $children = $node->children; + if (count($children) <= 1) { + return $children; + } + $last_child = \end($children); + // Loop over the `} else {` blocks. + // @phan-suppress-next-line PhanPossiblyUndeclaredProperty + while ($last_child->children['cond'] === null) { + $first_stmt = $last_child->children['stmts']->children[0] ?? null; + if (!($first_stmt instanceof Node)) { + break; + } + if ($first_stmt->kind !== ast\AST_IF) { + break; + } + // @phan-suppress-next-line PhanUndeclaredProperty + $first_stmt->is_inside_else = true; + \array_pop($children); + \array_push($children, ...$first_stmt->children); + $last_child = \end($children); + } + return $children; + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. + +return new DuplicateExpressionPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/EmptyMethodAndFunctionPlugin.php b/bundled-libs/phan/phan/.phan/plugins/EmptyMethodAndFunctionPlugin.php new file mode 100644 index 000000000..bf920bf3e --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/EmptyMethodAndFunctionPlugin.php @@ -0,0 +1,125 @@ +children['stmts'] ?? null; + + if (!$stmts_node || $stmts_node->children) { + return; + } + $method = $this->context->getFunctionLikeInScope($this->code_base); + if (!($method instanceof Method)) { + throw new AssertionError("Expected $method to be a method"); + } + if ($method->isNewConstructor()) { + foreach ($node->children['params']->children as $param) { + if ($param instanceof Node && ($param->flags & Parameter::PARAM_MODIFIER_VISIBILITY_FLAGS)) { + // This uses constructor property promotion + return; + } + } + } + + if (!$method->isOverriddenByAnother() + && !$method->isOverride() + && !$method->isDeprecated() + ) { + $this->emitIssue( + self::getIssueTypeForEmptyMethod($method), + $node->lineno, + $method->getRepresentationForIssue() + ); + } + } + + public function visitFuncDecl(Node $node): void + { + $this->analyzeFunction($node); + } + + public function visitClosure(Node $node): void + { + $this->analyzeFunction($node); + } + + // No need for visitArrowFunc. + // By design, `fn($args) => expr` can't have an empty statement list because it must have an expression. + // It's always equivalent to `return expr;` + + private function analyzeFunction(Node $node): void + { + $stmts_node = $node->children['stmts'] ?? null; + + if ($stmts_node && !$stmts_node->children) { + $function = $this->context->getFunctionLikeInScope($this->code_base); + if (!($function instanceof Func)) { + throw new AssertionError("Expected $function to be Func\n"); + } + + if (!$function->isDeprecated()) { + $this->emitIssue( + $function->isClosure() ? Issue::EmptyClosure : Issue::EmptyFunction, + $node->lineno, + $function->getRepresentationForIssue() + ); + } + } + } + + private static function getIssueTypeForEmptyMethod(FunctionInterface $method): string + { + if (!$method instanceof Method) { + throw new \InvalidArgumentException("\$method is not an instance of Method"); + } + + if ($method->isPrivate()) { + return Issue::EmptyPrivateMethod; + } + + if ($method->isProtected()) { + return Issue::EmptyProtectedMethod; + } + + return Issue::EmptyPublicMethod; + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new EmptyMethodAndFunctionPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/EmptyStatementListPlugin.php b/bundled-libs/phan/phan/.phan/plugins/EmptyStatementListPlugin.php new file mode 100644 index 000000000..715b8ea8b --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/EmptyStatementListPlugin.php @@ -0,0 +1,360 @@ + set by plugin framework + * @suppress PhanReadOnlyProtectedProperty + */ + protected $parent_node_list; + + /** + * @param Node $node + * A node to analyze + * @override + */ + public function visitIf(Node $node): void + { + // @phan-suppress-next-line PhanUndeclaredProperty set by ASTSimplifier + if (isset($node->is_simplified)) { + $first_child = end($node->children); + if (!$first_child instanceof Node || $first_child->children['cond'] === null) { + return; + } + $last_if_elem = reset($node->children); + } else { + $last_if_elem = end($node->children); + } + if (!$last_if_elem instanceof Node) { + // probably impossible + return; + } + $stmts_node = $last_if_elem->children['stmts']; + if (!$stmts_node instanceof Node) { + // probably impossible + return; + } + if ($stmts_node->children) { + // the last if element has statements + return; + } + if ($last_if_elem->children['cond'] === null) { + // Don't bother warning about else + return; + } + if ($this->hasTODOComment($stmts_node->lineno, $node)) { + // Don't warn if there is a FIXME/TODO comment in/around the empty statement list + return; + } + + $this->emitPluginIssue( + $this->code_base, + (clone($this->context))->withLineNumberStart($last_if_elem->children['stmts']->lineno ?? $last_if_elem->lineno), + 'PhanPluginEmptyStatementIf', + 'Empty statement list statement detected for the last if/elseif statement', + [] + ); + } + + private function hasTODOComment(int $lineno, Node $analyzed_node, ?int $end_lineno = null): bool + { + if (EmptyStatementListPlugin::$ignore_todos) { + return false; + } + $file = FileCache::getOrReadEntry($this->context->getFile()); + $lines = $file->getLines(); + $end_lineno = max($lineno, $end_lineno ?? $this->findEndLine($lineno, $analyzed_node)); + for ($i = $lineno; $i <= $end_lineno; $i++) { + $line = $lines[$i] ?? null; + if (!is_string($line)) { + break; + } + if (preg_match('/todo|fixme|deliberately empty/i', $line) > 0) { + return true; + } + } + return false; + } + + private function findEndLine(int $lineno, Node $search_node): int + { + for ($node_index = count($this->parent_node_list) - 1; $node_index >= 0; $node_index--) { + $node = $this->parent_node_list[$node_index] ?? null; + if (!$node) { + continue; + } + if (isset($node->endLineno)) { + // Return the end line of the function declaration. + return $node->endLineno; + } + if ($node->kind === ast\AST_STMT_LIST) { + foreach ($node->children as $i => $c) { + if ($c === $search_node) { + $next_node = $node->children[$i + 1] ?? null; + if ($next_node instanceof Node) { + return $next_node->lineno - 1; + } + break; + } + } + } + $search_node = $node; + } + // Give up and guess. + return $lineno + 5; + } + + /** + * @param Node $node + * A node of kind ast\AST_FOR to analyze + * @override + */ + public function visitFor(Node $node): void + { + $stmts_node = $node->children['stmts']; + if (!$stmts_node instanceof Node) { + // impossible + return; + } + if ($stmts_node->children || ($node->children['loop']->children ?? null)) { + // the for loop has statements, in the body and/or in the loop condition. + return; + } + if ($this->hasTODOComment($stmts_node->lineno, $node)) { + // Don't warn if there is a FIXME/TODO comment in/around the empty statement list + return; + } + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($stmts_node->lineno ?? $node->lineno), + 'PhanPluginEmptyStatementForLoop', + 'Empty statement list statement detected for the for loop', + [] + ); + } + + /** + * @param Node $node + * A node to analyze + * @override + */ + public function visitWhile(Node $node): void + { + $stmts_node = $node->children['stmts']; + if (!$stmts_node instanceof Node) { + return; // impossible + } + if ($stmts_node->children) { + // the while loop has statements + return; + } + if ($this->hasTODOComment($stmts_node->lineno, $node)) { + // Don't warn if there is a FIXME/TODO comment in/around the empty statement list + return; + } + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($stmts_node->lineno ?? $node->lineno), + 'PhanPluginEmptyStatementWhileLoop', + 'Empty statement list statement detected for the while loop', + [] + ); + } + + /** + * @param Node $node + * A node to analyze + * @override + */ + public function visitDoWhile(Node $node): void + { + $stmts_node = $node->children['stmts']; + if (!$stmts_node instanceof Node) { + return; // impossible + } + if ($stmts_node->children ?? null) { + // the while loop has statements + return; + } + if ($this->hasTODOComment($stmts_node->lineno, $node)) { + // Don't warn if there is a FIXME/TODO comment in/around the empty statement list + return; + } + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($stmts_node->lineno), + 'PhanPluginEmptyStatementDoWhileLoop', + 'Empty statement list statement detected for the do-while loop', + [] + ); + } + + /** + * @param Node $node + * A node to analyze + * @override + */ + public function visitForeach(Node $node): void + { + $stmts_node = $node->children['stmts']; + if (!$stmts_node instanceof Node) { + // impossible + return; + } + if ($stmts_node->children) { + // the while loop has statements + return; + } + if ($this->hasTODOComment($stmts_node->lineno, $node)) { + // Don't warn if there is a FIXME/TODO comment in/around the empty statement list + return; + } + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($stmts_node->lineno), + 'PhanPluginEmptyStatementForeachLoop', + 'Empty statement list statement detected for the foreach loop', + [] + ); + } + + /** + * @param Node $node + * A node to analyze + * @override + */ + public function visitTry(Node $node): void + { + ['try' => $try_node, 'finally' => $finally_node] = $node->children; + if (!$try_node->children) { + if (!$this->hasTODOComment($try_node->lineno, $node, $node->children['catches']->children[0]->lineno ?? $finally_node->lineno ?? null)) { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($try_node->lineno), + 'PhanPluginEmptyStatementTryBody', + 'Empty statement list statement detected for the try statement\'s body', + [] + ); + } + } + if ($finally_node instanceof Node && !$finally_node->children) { + if (!$this->hasTODOComment($finally_node->lineno, $node)) { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($finally_node->lineno), + 'PhanPluginEmptyStatementTryFinally', + 'Empty statement list statement detected for the try\'s finally body', + [] + ); + } + } + } + + /** + * @param Node $node + * A node of kind ast\AST_SWITCH to analyze + * @override + */ + public function visitSwitch(Node $node): void + { + // Check all case statements and return if something that isn't a no-op is seen. + foreach ($node->children['stmts']->children ?? [] as $c) { + if (!$c instanceof Node) { + // impossible + continue; + } + + $children = $c->children['stmts']->children ?? null; + if ($children) { + if (count($children) > 1) { + return; + } + $only_node = $children[0]; + if ($only_node instanceof Node) { + if (!in_array($only_node->kind, [ast\AST_CONTINUE, ast\AST_BREAK], true)) { + return; + } + if (($only_node->children['depth'] ?? 1) !== 1) { + // not a no-op + return; + } + } + } + if (!ParseVisitor::isConstExpr($c->children['cond'])) { + return; + } + } + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($node->lineno), + 'PhanPluginEmptyStatementSwitch', + 'No side effects seen for any cases of this switch statement', + [] + ); + } +} +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new EmptyStatementListPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/FFIAnalysisPlugin.php b/bundled-libs/phan/phan/.phan/plugins/FFIAnalysisPlugin.php new file mode 100644 index 000000000..d6d28f695 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/FFIAnalysisPlugin.php @@ -0,0 +1,153 @@ +children['var']; + if (!($left instanceof Node)) { + return; + } + if ($left->kind !== ast\AST_VAR) { + return; + } + $var_name = $left->children['name']; + if (!is_string($var_name)) { + return; + } + $scope = $this->context->getScope(); + if (!$scope->hasVariableWithName($var_name)) { + return; + } + $var = $scope->getVariableByName($var_name); + $category = self::containsFFICDataType($var->getUnionType()); + if (!$category) { + return; + } + // @phan-suppress-next-line PhanUndeclaredProperty + $node->is_ffi = $category; + } + + public const PARTIALLY_FFI_CDATA = 1; + public const ENTIRELY_FFI_CDATA = 2; + + /** + * Check if the type contains FFI\CData + */ + private static function containsFFICDataType(UnionType $union_type): int + { + foreach ($union_type->getTypeSet() as $type) { + if (strcasecmp('\FFI', $type->getNamespace()) !== 0) { + continue; + } + if (strcasecmp('CData', $type->getName()) !== 0) { + continue; + } + if ($type->isNullable()) { + return self::PARTIALLY_FFI_CDATA; + } + if ($union_type->typeCount() > 1) { + return self::PARTIALLY_FFI_CDATA; + } + return self::ENTIRELY_FFI_CDATA; + } + return 0; + } +} + +/** + * This visitor restores FFI\CData types after assignments if the original value was FFI\CData + */ +class FFIPostAnalysisVisitor extends PluginAwarePostAnalysisVisitor +{ + /** + * @override + */ + public function visitAssign(Node $node): void + { + // @phan-suppress-next-line PhanUndeclaredProperty + if (isset($node->is_ffi)) { + $this->analyzeFFIAssign($node); + } + } + + private function analyzeFFIAssign(Node $node): void + { + $var_name = $node->children['var']->children['name'] ?? null; + if (!is_string($var_name)) { + return; + } + $cdata_type = UnionType::fromFullyQualifiedPHPDocString('\FFI\CData'); + $scope = $this->context->getScope(); + // @phan-suppress-next-line PhanUndeclaredProperty + if ($node->is_ffi !== FFIPreAnalysisVisitor::ENTIRELY_FFI_CDATA) { + if ($scope->hasVariableWithName($var_name)) { + $cdata_type = $cdata_type->withUnionType($scope->getVariableByName($var_name)->getUnionType()); + } + } + $this->context->getScope()->addVariable( + new Variable($this->context, $var_name, $cdata_type, 0) + ); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. + +return new FFIAnalysisPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/HasPHPDocPlugin.php b/bundled-libs/phan/phan/.phan/plugins/HasPHPDocPlugin.php new file mode 100644 index 000000000..22f2af209 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/HasPHPDocPlugin.php @@ -0,0 +1,402 @@ +isAnonymous()) { + // Probably not useful in many cases to document a short anonymous class. + return; + } + $doc_comment = $class->getDocComment(); + if (!StringUtil::isNonZeroLengthString($doc_comment)) { + self::emitIssue( + $code_base, + $class->getContext(), + 'PhanPluginNoCommentOnClass', + 'Class {CLASS} has no doc comment', + [$class->getFQSEN()] + ); + return; + } + $description = MarkupDescription::extractDescriptionFromDocComment($class); + if (!StringUtil::isNonZeroLengthString($description)) { + if (strpos($doc_comment, '@deprecated') !== false) { + return; + } + self::emitIssue( + $code_base, + $class->getContext(), + 'PhanPluginDescriptionlessCommentOnClass', + 'Class {CLASS} has no readable description: {STRING_LITERAL}', + [$class->getFQSEN(), self::getDocCommentRepresentation($doc_comment)] + ); + return; + } + } + + /** + * @param CodeBase $code_base + * The code base in which the function exists + * + * @param Func $function + * A function being analyzed + * @override + */ + public function analyzeFunction( + CodeBase $code_base, + Func $function + ): void { + if ($function->isPHPInternal()) { + // This isn't user-defined, there's no reason to warn or way to change it. + return; + } + if ($function->isNSInternal($code_base)) { + // (at)internal are internal to the library, and there's less of a need to document them + return; + } + if ($function->isClosure()) { + // Probably not useful in many cases to document a short closure passed to array_map, etc. + return; + } + $doc_comment = $function->getDocComment(); + if (!StringUtil::isNonZeroLengthString($doc_comment)) { + self::emitIssue( + $code_base, + $function->getContext(), + "PhanPluginNoCommentOnFunction", + "Function {FUNCTION} has no doc comment", + [$function->getFQSEN()] + ); + return; + } + $description = MarkupDescription::extractDescriptionFromDocComment($function); + if (!StringUtil::isNonZeroLengthString($description)) { + self::emitIssue( + $code_base, + $function->getContext(), + "PhanPluginDescriptionlessCommentOnFunction", + "Function {FUNCTION} has no readable description: {STRING_LITERAL}", + [$function->getFQSEN(), self::getDocCommentRepresentation($doc_comment)] + ); + return; + } + } + + /** + * Encode the doc comment in a one-line form that can be used in Phan's issue message. + * @internal + */ + public static function getDocCommentRepresentation(string $doc_comment): string + { + return (string)json_encode(MarkupDescription::getDocCommentWithoutWhitespace($doc_comment), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + public static function getPostAnalyzeNodeVisitorClassName(): string + { + return (bool)(Config::getValue('plugin_config')['has_phpdoc_check_duplicates'] ?? false) + ? DuplicatePHPDocCheckerPlugin::class + : BasePHPDocCheckerPlugin::class; + } +} + +/** Infer property and class doc comments and warn */ +class BasePHPDocCheckerPlugin extends PluginAwarePostAnalysisVisitor +{ + /** @return array{0:list,1:list} */ + public function visitClass(Node $node): array + { + $class = $this->context->getClassInScope($this->code_base); + $property_descriptions = []; + $method_descriptions = []; + foreach ($node->children['stmts']->children ?? [] as $element) { + if (!($element instanceof Node)) { + throw new AssertionError("All properties of ast\AST_CLASS's statement list must be nodes, saw " . gettype($element)); + } + switch ($element->kind) { + case ast\AST_METHOD: + $entry = $this->checkMethodDescription($class, $element); + if ($entry) { + $method_descriptions[] = $entry; + } + break; + case ast\AST_PROP_GROUP: + $entry = $this->checkPropGroupDescription($class, $element); + if ($entry) { + $property_descriptions[] = $entry; + } + break; + } + } + return [$property_descriptions, $method_descriptions]; + } + + /** + * @param Node $node a node of kind ast\AST_METHOD + */ + private function checkMethodDescription(Clazz $class, Node $node): ?ClassElementEntry + { + $method_name = (string)$node->children['name']; + $method = $class->getMethodByName($this->code_base, $method_name); + if ($method->isMagic()) { + // Ignore construct + return null; + } + if ($method->isOverride()) { + return null; + } + $method_filter = HasPHPDocPlugin::$method_filter; + if (is_string($method_filter)) { + $fqsen_string = ltrim((string)$method->getFQSEN(), '\\'); + if (preg_match($method_filter, $fqsen_string) > 0) { + return null; + } + } + + $doc_comment = $method->getDocComment(); + if (!StringUtil::isNonZeroLengthString($doc_comment)) { + $visibility_upper = ucfirst($method->getVisibilityName()); + self::emitPluginIssue( + $this->code_base, + $method->getContext(), + "PhanPluginNoCommentOn${visibility_upper}Method", + "$visibility_upper method {METHOD} has no doc comment", + [$method->getFQSEN()] + ); + return null; + } + $description = MarkupDescription::extractDescriptionFromDocComment($method); + if (!StringUtil::isNonZeroLengthString($description)) { + $visibility_upper = ucfirst($method->getVisibilityName()); + self::emitPluginIssue( + $this->code_base, + $method->getContext(), + "PhanPluginDescriptionlessCommentOn${visibility_upper}Method", + "$visibility_upper method {METHOD} has no readable description: {STRING_LITERAL}", + [$method->getFQSEN(), HasPHPDocPlugin::getDocCommentRepresentation($doc_comment)] + ); + return null; + } + return new ClassElementEntry($method, \trim(\preg_replace('/\s+/', ' ', $description))); + } + + /** + * @param Node $node a node of type ast\AST_PROP_GROUP + */ + private function checkPropGroupDescription(Clazz $class, Node $node): ?ClassElementEntry + { + $property_name = $node->children['props']->children[0]->children['name'] ?? null; + if (!is_string($property_name)) { + return null; + } + $property = $class->getPropertyByName($this->code_base, $property_name); + $doc_comment = $property->getDocComment(); + if (!StringUtil::isNonZeroLengthString($doc_comment)) { + $visibility_upper = ucfirst($property->getVisibilityName()); + self::emitPluginIssue( + $this->code_base, + $property->getContext(), + "PhanPluginNoCommentOn${visibility_upper}Property", + "$visibility_upper property {PROPERTY} has no doc comment", + [$property->getRepresentationForIssue()] + ); + return null; + } + // @phan-suppress-next-line PhanAccessMethodInternal + $description = MarkupDescription::extractDocComment($doc_comment, Comment::ON_PROPERTY, null, true); + if (!StringUtil::isNonZeroLengthString($description)) { + $visibility_upper = ucfirst($property->getVisibilityName()); + self::emitPluginIssue( + $this->code_base, + $property->getContext(), + "PhanPluginDescriptionlessCommentOn${visibility_upper}Property", + "$visibility_upper property {PROPERTY} has no readable description: {STRING_LITERAL}", + [$property->getRepresentationForIssue(), HasPHPDocPlugin::getDocCommentRepresentation($doc_comment)] + ); + return null; + } + return new ClassElementEntry($property, \trim(\preg_replace('/\s+/', ' ', $description))); + } +} + +/** + * Describes a property group or a method node and the associated description + * @phan-immutable + * @internal + */ +final class ClassElementEntry +{ + /** @var ClassElement the element (or element group) */ + public $element; + /** @var string the phpdoc description */ + public $description; + + public function __construct(ClassElement $element, string $description) + { + $this->element = $element; + $this->description = $description; + } +} + +/** + * Check if phpdoc of property groups and methods are duplicated + * @internal + */ +final class DuplicatePHPDocCheckerPlugin extends BasePHPDocCheckerPlugin +{ + /** No-op */ + public function visitClass(Node $node): array + { + [$property_descriptions, $method_descriptions] = parent::visitClass($node); + foreach (self::findGroups($property_descriptions) as $entries) { + $first_entry = array_shift($entries); + if (!$first_entry instanceof ClassElementEntry) { + throw new AssertionError('Expected $entries of $property_descriptions to be a group of 1 or more entries'); + } + $first_property = $first_entry->element; + foreach ($entries as $entry) { + $property = $entry->element; + self::emitPluginIssue( + $this->code_base, + $property->getContext(), + "PhanPluginDuplicatePropertyDescription", + "Property {PROPERTY} has the same description as the property \${PROPERTY} on line {LINE}: {COMMENT}", + [$property->getRepresentationForIssue(), $first_property->getName(), $first_property->getContext()->getLineNumberStart(), $first_entry->description] + ); + } + } + foreach (self::findGroups($method_descriptions) as $entries) { + $first_entry = array_shift($entries); + if (!$first_entry instanceof ClassElementEntry) { + throw new AssertionError('Expected $entries of $property_descriptions to be a group of 1 or more entries'); + } + $first_method = $first_entry->element; + foreach ($entries as $entry) { + $method = $entry->element; + self::emitPluginIssue( + $this->code_base, + $method->getContext(), + "PhanPluginDuplicateMethodDescription", + "Method {METHOD} has the same description as the method {METHOD} on line {LINE}: {COMMENT}", + [$method->getRepresentationForIssue(), $first_method->getName() . '()', $first_method->getContext()->getLineNumberStart(), $first_entry->description] + ); + } + } + return [$property_descriptions, $method_descriptions]; + } + + /** + * @param list $values + * @return array> + */ + private static function findGroups(array $values): array + { + $result = []; + foreach ($values as $v) { + if ($v->element->isDeprecated()) { + continue; + } + $result[$v->description][] = $v; + } + foreach ($result as $description => $keys) { + if (count($keys) <= 1) { + unset($result[$description]); + } + } + return $result; + } +} +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new HasPHPDocPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/InlineHTMLPlugin.php b/bundled-libs/phan/phan/.phan/plugins/InlineHTMLPlugin.php new file mode 100644 index 000000000..2a8dcb739 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/InlineHTMLPlugin.php @@ -0,0 +1,178 @@ + set of files that have echo statements */ + public static $file_set_to_analyze = []; + + /** @var ?string */ + private $whitelist_regex; + /** @var ?string */ + private $blacklist_regex; + + public function __construct() + { + $plugin_config = Config::getValue('plugin_config'); + $this->whitelist_regex = $plugin_config['inline_html_whitelist_regex'] ?? null; + $this->blacklist_regex = $plugin_config['inline_html_blacklist_regex'] ?? null; + } + + private function shouldCheckFile(string $path): bool + { + if (is_string($this->blacklist_regex)) { + if (CLI::isPathMatchedByRegex($this->blacklist_regex, $path)) { + return false; + } + } + if (is_string($this->whitelist_regex)) { + return CLI::isPathMatchedByRegex($this->whitelist_regex, $path); + } + return true; + } + + /** + * @param CodeBase $code_base + * The code base in which the node exists + * + * @param Context $context @phan-unused-param + * A context with the file name for $file_contents and the scope after analyzing $node. + * + * @param string $file_contents the unmodified file contents @phan-unused-param + * @param Node $node the node @phan-unused-param + * @override + * @throws Error if a process fails to shut down + */ + public function afterAnalyzeFile( + CodeBase $code_base, + Context $context, + string $file_contents, + Node $node + ): void { + $file = $context->getFile(); + if (!isset(self::$file_set_to_analyze[$file])) { + // token_get_all is noticeably slow when there are a lot of files, so we check for the existence of echo statements in the parsed AST as a heuristic to avoid calling token_get_all. + return; + } + if (!self::shouldCheckFile($file)) { + return; + } + $file_contents = Parser::removeShebang($file_contents); + $tokens = token_get_all($file_contents); + foreach ($tokens as $i => $token) { + if (!is_array($token)) { + continue; + } + if ($token[0] !== T_INLINE_HTML) { + continue; + } + $N = count($tokens); + $this->warnAboutInlineHTML($code_base, $context, $token, $i, $N); + if ($i < $N - 1) { + // Make sure to always check if the last token is inline HTML + $token = $tokens[$N - 1] ?? null; + if (!is_array($token)) { + break; + } + if ($token[0] !== T_INLINE_HTML) { + break; + } + $this->warnAboutInlineHTML($code_base, $context, $token, $N - 1, $N); + } + break; + } + } + + /** + * @param array{0:int,1:string,2:int} $token a token from token_get_all + */ + private function warnAboutInlineHTML(CodeBase $code_base, Context $context, array $token, int $i, int $n): void + { + if ($i === 0) { + $issue = self::InlineHTMLLeading; + $message = 'Saw inline HTML at the start of the file: {STRING_LITERAL}'; + } elseif ($i >= $n - 1) { + $issue = self::InlineHTMLTrailing; + $message = 'Saw inline HTML at the end of the file: {STRING_LITERAL}'; + } else { + $issue = self::InlineHTML; + $message = 'Saw inline HTML between the first and last token: {STRING_LITERAL}'; + } + $this->emitIssue( + $code_base, + clone($context)->withLineNumberStart($token[2]), + $issue, + $message, + [StringUtil::jsonEncode(self::truncate($token[1]))] + ); + } + + private static function truncate(string $token): string + { + if (strlen($token) > 20) { + return mb_substr($token, 0, 20) . "..."; + } + return $token; + } + + /** + * @return string - name of PluginAwarePostAnalysisVisitor subclass + * + * @override + */ + public static function getPostAnalyzeNodeVisitorClassName(): string + { + return InlineHTMLVisitor::class; + } +} + +/** + * Records existence of AST_ECHO within a file, marking the file as one that should be checked. + * + * php-ast (and the underlying AST implementation) doesn't provide a way to distinguish inline HTML from other types of echos. + */ +class InlineHTMLVisitor extends PluginAwarePostAnalysisVisitor +{ + /** + * @override + * @param Node $node @unused-param + * @return void + */ + public function visitEcho(Node $node) + { + InlineHTMLPlugin::$file_set_to_analyze[$this->context->getFile()] = true; + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +if (!function_exists('token_get_all')) { + throw new UnloadablePluginException("InlineHTMLPlugin requires the tokenizer extension, which is not enabled (this plugin uses token_get_all())"); +} +return new InlineHTMLPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/InvalidVariableIssetPlugin.php b/bundled-libs/phan/phan/.phan/plugins/InvalidVariableIssetPlugin.php new file mode 100644 index 000000000..648c73eb0 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/InvalidVariableIssetPlugin.php @@ -0,0 +1,127 @@ +children['var']; + $variable = $argument; + + // get variable name from argument + while (!isset($variable->children['name'])) { + if (!$variable instanceof Node) { + // e.g. 'foo' in `isset('foo'[$i])` or `isset('foo'->bar)`. + $this->emit( + 'PhanPluginInvalidVariableIsset', + "Unexpected expression in isset()", + [] + ); + return $this->context; + } + if (in_array($variable->kind, self::EXPRESSIONS, true)) { + $variable = $variable->children['expr']; + } elseif (in_array($variable->kind, self::CLASSES, true)) { + $variable = $variable->children['class']; + } else { + return $this->context; + } + } + if (!$variable instanceof Node) { + $this->emit( + 'PhanPluginUnexpectedExpressionIsset', + "Unexpected expression in isset()", + [] + ); + return $this->context; + } + $name = $variable->children['name'] ?? null; + + // emit issue if name is not declared + // Check for edge cases such as isset($$var) + if (is_string($name)) { + if ($variable->kind !== ast\AST_VAR) { + // e.g. ast\AST_NAME of an ast\AST_CONST + return $this->context; + } + if (!Variable::isHardcodedVariableInScopeWithName($name, $this->context->isInGlobalScope()) + && !$this->context->getScope()->hasVariableWithName($name) + && !( + $this->context->isInGlobalScope() && Config::getValue('ignore_undeclared_variables_in_global_scope') + ) + ) { + $this->emit( + 'PhanPluginUndeclaredVariableIsset', + 'undeclared variable ${VARIABLE} in isset()', + [$name] + ); + } + } elseif ($variable->kind !== ast\AST_VAR) { + // emit issue if argument is not array access + $this->emit( + 'PhanPluginInvalidVariableIsset', + "non array/property access in isset()", + [] + ); + return $this->context; + } elseif (!is_string($name)) { + // emit issue if argument is not array access + $this->emit( + 'PhanPluginComplexVariableInIsset', + "Unanalyzable complex variable expression in isset", + [] + ); + } + return $this->context; + } +} + +return new InvalidVariableIssetPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/InvokePHPNativeSyntaxCheckPlugin.php b/bundled-libs/phan/phan/.phan/plugins/InvokePHPNativeSyntaxCheckPlugin.php new file mode 100644 index 000000000..ca7d717d6 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/InvokePHPNativeSyntaxCheckPlugin.php @@ -0,0 +1,429 @@ + ['php_native_syntax_check_binaries' => ['php72', 'php70', 'php56']] + * Note: This may cause Phan to take over twice as long. This is recommended for use with `--processes N`. + * + * Known issues: + * - short_open_tags may make php --syntax-check --no-php-ini behave differently from php --syntax-check, e.g. for ' + * A list of invoked processes that this plugin created. + * This plugin creates 0 or more processes(up to a maximum number can run at a time) + * and then waits for the execution of those processes to finish. + */ + private $processes = []; + + /** + * @param CodeBase $code_base @phan-unused-param + * The code base in which the node exists + * + * @param Context $context + * A context with the file name for $file_contents and the scope after analyzing $node. + * + * @param string $file_contents the unmodified file contents @phan-unused-param + * @param Node $node the node @phan-unused-param + * @override + */ + public function beforeAnalyzeFile( + CodeBase $code_base, + Context $context, + string $file_contents, + Node $node + ): void { + $php_binaries = (Config::getValue('plugin_config')['php_native_syntax_check_binaries'] ?? null) ?: [PHP_BINARY]; + + foreach ($php_binaries as $binary) { + $this->processes[] = new InvokeExecutionPromise($binary, $file_contents, $context); + } + } + + /** + * @param CodeBase $code_base + * The code base in which the node exists + * + * @param Context $context @phan-unused-param + * A context with the file name for $file_contents and the scope after analyzing $node. + * + * @param string $file_contents the unmodified file contents @phan-unused-param + * @param Node $node the node @phan-unused-param + * @override + * @throws Error if a process fails to shut down + */ + public function afterAnalyzeFile( + CodeBase $code_base, + Context $context, + string $file_contents, + Node $node + ): void { + $configured_max_incomplete_processes = (int)(Config::getValue('plugin_config')['php_native_syntax_check_max_processes'] ?? 1) - 1; + $max_incomplete_processes = max(0, $configured_max_incomplete_processes); + $this->awaitIncompleteProcesses($code_base, $max_incomplete_processes); + } + + /** + * @throws Error if a syntax check process fails to shut down + */ + private function awaitIncompleteProcesses(CodeBase $code_base, int $max_incomplete_processes): void + { + foreach ($this->processes as $i => $process) { + if (!$process->read()) { + continue; + } + unset($this->processes[$i]); + self::handleError($code_base, $process); + } + $max_incomplete_processes = max(0, $max_incomplete_processes); + while (count($this->processes) > $max_incomplete_processes) { + $process = array_pop($this->processes); + if (!$process) { + throw new AssertionError("Process list should be non-empty"); + } + $process->blockingRead(); + self::handleError($code_base, $process); + } + } + + /** + * @override + * @throws Error if a syntax check process fails to shut down. + */ + public function finalizeProcess(CodeBase $code_base): void + { + $this->awaitIncompleteProcesses($code_base, 0); + } + + private static function handleError(CodeBase $code_base, InvokeExecutionPromise $process): void + { + $check_error_message = $process->getError(); + if (!is_string($check_error_message)) { + return; + } + $context = $process->getContext(); + $binary = $process->getBinary(); + $lineno = 1; + if (preg_match(self::LINE_NUMBER_REGEX, $check_error_message, $matches)) { + $lineno = (int)$matches[1]; + $check_error_message = trim(preg_replace(self::LINE_NUMBER_REGEX, '', $check_error_message)); + } + $check_error_message = preg_replace(self::STDIN_FILENAME_REGEX, '', $check_error_message); + + self::emitIssue( + $code_base, + clone($context)->withLineNumberStart($lineno), + 'PhanNativePHPSyntaxCheckPlugin', + 'Saw error or notice for {FILE} --syntax-check: {DETAILS}', + [ + $binary === PHP_BINARY ? 'php' : $binary, + json_encode($check_error_message), + + ], + Issue::SEVERITY_CRITICAL + ); + } +} + +/** + * This wraps a `php --syntax-check` process, + * and contains methods to start the process and await the result + * (and check for failures) + */ +class InvokeExecutionPromise +{ + /** @var string path to the php binary invoked */ + private $binary; + + /** @var bool is the process finished executing */ + private $done = false; + + /** @var resource the result of `proc_open()` */ + private $process; + + /** @var array{0:resource,1:resource,2:resource} stdin, stdout, stderr */ + private $pipes; + + /** @var ?string an error message */ + private $error = null; + + /** @var string the raw bytes from stdout with serialized data */ + private $raw_stdout = ''; + + /** @var Context has the file name being analyzed */ + private $context; + + /** @var ?string the temporary path, if needed for Windows. */ + private $tmp_path; + + public function __construct(string $binary, string $file_contents, Context $context) + { + $this->context = clone($context); + $new_file_contents = Parser::removeShebang($file_contents); + // TODO: Use symfony process + // Note: We might have invalid utf-8, ensure that the streams are opened in binary mode. + // I'm not sure if this is necessary. + if (DIRECTORY_SEPARATOR === "\\") { + $cmd = escapeshellarg($binary) . ' --syntax-check --no-php-ini'; + $abs_path = $this->getAbsPathForFileContents($new_file_contents, $file_contents !== $new_file_contents); + if (!is_string($abs_path)) { + // The helper function has set the error and done flags + return; + } + + // Possibly https://bugs.php.net/bug.php?id=51800 + // NOTE: Work around this by writing from the original file. This may not work as expected in LSP mode + $abs_path = str_replace("/", "\\", $abs_path); + + $cmd .= ' < ' . escapeshellarg($abs_path); + + $descriptorspec = [ + 1 => ['pipe', 'wb'], + ]; + $this->binary = $binary; + // https://superuser.com/questions/1213094/how-to-escape-in-cmd-exe-c-parameters/1213100#1213100 + // + // > Otherwise, old behavior is to see if the first character is + // > a quote character and if so, strip the leading character and + // > remove the last quote character on the command line, preserving + // > any text after the last quote character. + // + // e.g. `""C:\php 7.4.3\php.exe" --syntax-check --no-php-ini < "C:\some project\test.php""` + // gets unescaped as `"C:\php 7.4.3\php.exe" --syntax-check --no-php-ini < "C:\some project\test.php"` + if (PHP_VERSION_ID < 80000) { + // In PHP 8.0.0, proc_open started always escaping arguments with additional quotes, so doing it twice would be a bug. + $cmd = "\"$cmd\""; + } + $process = proc_open("$cmd", $descriptorspec, $pipes); + if (!is_resource($process)) { + $this->done = true; + $this->error = "Failed to run proc_open in " . __METHOD__; + return; + } + $this->process = $process; + } else { + $cmd = [$binary, '--syntax-check', '--no-php-ini']; + if (PHP_VERSION_ID < 70400) { + $cmd = implode(' ', array_map('escapeshellarg', $cmd)); + } + $descriptorspec = [ + ['pipe', 'rb'], + ['pipe', 'wb'], + ]; + $this->binary = $binary; + // @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal + $process = proc_open($cmd, $descriptorspec, $pipes); + if (!is_resource($process)) { + $this->done = true; + $this->error = "Failed to run proc_open in " . __METHOD__; + return; + } + $this->process = $process; + + self::streamPutContents($pipes[0], $new_file_contents); + } + $this->pipes = $pipes; + + if (!stream_set_blocking($pipes[1], false)) { + $this->error = "unable to set read stdout to non-blocking"; + } + } + + private function getAbsPathForFileContents(string $new_file_contents, bool $force_tmp_file): ?string + { + $file_name = $this->context->getFile(); + if ($force_tmp_file || CLI::isDaemonOrLanguageServer()) { + // This is inefficient, but + // - Windows has problems with using stdio/stdout at the same time + // - During regular analysis, we won't need to create temporary files. + $tmp_path = tempnam(sys_get_temp_dir(), 'phan'); + if (!is_string($tmp_path)) { + $this->done = true; + $this->error = "Could not create temporary path for $file_name"; + return null; + } + file_put_contents($tmp_path, $new_file_contents); + $this->tmp_path = $tmp_path; + return $tmp_path; + } + $abs_path = Config::projectPath($file_name); + if (!file_exists($abs_path)) { + $this->done = true; + $this->error = "File does not exist"; + return null; + } + return $abs_path; + } + + /** + * @param resource $stream stream to write $file_contents to before fclose() + * @param string $file_contents + * @return void + * See https://bugs.php.net/bug.php?id=39598 + */ + private static function streamPutContents($stream, string $file_contents): void + { + try { + while (strlen($file_contents) > 0) { + $bytes_written = fwrite($stream, $file_contents); + if ($bytes_written === false) { + error_log('failed to write in ' . __METHOD__); + return; + } + if ($bytes_written === 0) { + $read_streams = []; + $write_streams = [$stream]; + $except_streams = []; + // Wait for the stream to be available for write with a timeout of 1 second. + stream_select($read_streams, $write_streams, $except_streams, 1); + if (!$write_streams) { + usleep(1000); // Probably unnecessary, but leaving it in anyway + // This is blocked? + continue; + } + // $stream is ready to be written to? + $bytes_written = fwrite($stream, $file_contents); + if (!$bytes_written) { + error_log('failed to write in ' . __METHOD__ . ' but the stream should be ready'); + return; + } + } + if ($bytes_written > 0) { + $file_contents = \substr($file_contents, $bytes_written); + } + } + } finally { + fclose($stream); + } + } + + /** + * @return bool false if an error was encountered when trying to read more output from the syntax check process. + */ + public function read(): bool + { + if ($this->done) { + return true; + } + $stdout = $this->pipes[1]; + while (!feof($stdout)) { + $bytes = fread($stdout, 4096); + if ($bytes === false) { + break; + } + if (strlen($bytes) === 0) { + break; + } + $this->raw_stdout .= $bytes; + } + if (!feof($stdout)) { + return false; + } + fclose($stdout); + + $this->done = true; + + $exit_code = proc_close($this->process); + if ($exit_code === 0) { + $this->error = null; + return true; + } + $output = str_replace("\r", "", trim($this->raw_stdout)); + $first_line = explode("\n", $output)[0]; + $this->error = $first_line; + return true; + } + + /** + * @throws Error if reading failed + */ + public function blockingRead(): void + { + if ($this->done) { + return; + } + if (!stream_set_blocking($this->pipes[1], true)) { + throw new Error("Unable to make stdout blocking"); + } + if (!$this->read()) { + throw new Error("Failed to read"); + } + } + + /** + * @throws RangeException if this was called before the process finished + */ + public function getError(): ?string + { + if (!$this->done) { + throw new RangeException("Called " . __METHOD__ . " too early"); + } + return $this->error; + } + + /** + * Returns the context containing the name of the file being syntax checked + */ + public function getContext(): Context + { + return $this->context; + } + + /** + * @return string the path to the PHP interpreter binary. (e.g. `/usr/bin/php`) + */ + public function getBinary(): string + { + return $this->binary; + } + + public function __wakeup() + { + $this->tmp_path = null; + throw new RuntimeException("Cannot unserialize"); + } + + public function __destruct() + { + // We created a temporary path for Windows + if (is_string($this->tmp_path)) { + unlink($this->tmp_path); + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new InvokePHPNativeSyntaxCheckPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/LoopVariableReusePlugin.php b/bundled-libs/phan/phan/.phan/plugins/LoopVariableReusePlugin.php new file mode 100644 index 000000000..4105f4287 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/LoopVariableReusePlugin.php @@ -0,0 +1,7 @@ + maps function/method/closure FQSEN to function info and the set of union types they return */ + public static $method_return_types; + + /** @var Set the set of function/method/closure FQSENs that don't need to be more specific. */ + public static $method_blacklist; + + /** + * @return class-string - name of PluginAwarePostAnalysisVisitor subclass + */ + public static function getPostAnalyzeNodeVisitorClassName(): string + { + return MoreSpecificElementTypeVisitor::class; + } + + /** + * Record that $function contains a return statement which returns an expression of type $return_type. + * + * This may be called multiple times for the same return statement (Phan recursively analyzes functions with underspecified param types by default) + */ + public static function recordType(FunctionInterface $function, UnionType $return_type): void + { + $fqsen = $function->getFQSEN(); + if (self::$method_blacklist->offsetExists($fqsen)) { + return; + } + if ($return_type->isEmpty()) { + self::$method_blacklist->attach($fqsen); + self::$method_return_types->offsetUnset($fqsen); + return; + } + if (self::$method_return_types->offsetExists($fqsen)) { + self::$method_return_types->offsetGet($fqsen)->types->attach($return_type); + } else { + self::$method_return_types->offsetSet($fqsen, new ElementTypeInfo($function, [$return_type])); + } + } + + private static function shouldWarnAboutMoreSpecificType(CodeBase $code_base, UnionType $actual_type, UnionType $declared_return_type): bool + { + if ($declared_return_type->isEmpty()) { + // There was no phpdoc type declaration, so let UnknownElementTypePlugin warn about that instead of this. + // This plugin warns about `@return mixed` but not the absence of a declaration because the former normally prevents phan from inferring something more specific. + return false; + } + if ($declared_return_type->containsNullable() && !$actual_type->containsNullable()) { + // Warn about `Subclass1|Subclass2` being the real return type of `?BaseClass` + // because the actual returned type is non-null + return true; + } + if ($declared_return_type->typeCount() === 1) { + if ($declared_return_type->getTypeSet()[0]->isObjectWithKnownFQSEN()) { + if ($actual_type->typeCount() >= 2) { + // Don't warn about Subclass1|Subclass2 being more specific than BaseClass + return false; + } + } + } + if ($declared_return_type->isStrictSubtypeOf($code_base, $actual_type)) { + return false; + } + if (!$actual_type->asExpandedTypes($code_base)->canCastToUnionType($declared_return_type)) { + // Don't warn here about type mismatches such as int->string or object->array, but do warn about SubClass->BaseClass. + // Phan should warn elsewhere about those mismatches + return false; + } + if ($declared_return_type->hasTopLevelArrayShapeTypeInstances()) { + return false; + } + $real_actual_type = $actual_type->getRealUnionType(); + if (!$real_actual_type->isEmpty() && $declared_return_type->isStrictSubtypeOf($code_base, $real_actual_type)) { + // TODO: Provide a way to disable this heuristic. + return false; + } + return true; + } + + private static function containsObjectWithKnownFQSEN(UnionType $union_type): bool + { + foreach ($union_type->getTypesRecursively() as $type) { + if ($type->isObjectWithKnownFQSEN()) { + return true; + } + } + return false; + } + + /** + * After all return statements are gathered, suggest a more specific type for the various functions. + */ + public function finalizeProcess(CodeBase $code_base): void + { + foreach (self::$method_return_types as $type_info) { + $function = $type_info->function; + $function_context = $function->getContext(); + // TODO: Do a better job for Traversable and iterable + $actual_type = UnionType::merge($type_info->types->toArray())->withStaticResolvedInContext($function_context)->eraseTemplatesRecursive()->asNormalizedTypes(); + $declared_return_type = $function->getOriginalReturnType()->withStaticResolvedInContext($function_context)->eraseTemplatesRecursive()->asNormalizedTypes(); + if (!self::shouldWarnAboutMoreSpecificType($code_base, $actual_type, $declared_return_type)) { + continue; + } + if (self::containsObjectWithKnownFQSEN($actual_type) && !self::containsObjectWithKnownFQSEN($declared_return_type)) { + $issue_type = 'PhanPluginMoreSpecificActualReturnTypeContainsFQSEN'; + $issue_message = 'Phan inferred that {FUNCTION} documented to have return type {TYPE} (without an FQSEN) returns the more specific type {TYPE} (with an FQSEN)'; + } else { + $issue_type = 'PhanPluginMoreSpecificActualReturnType'; + $issue_message = 'Phan inferred that {FUNCTION} documented to have return type {TYPE} returns the more specific type {TYPE}'; + } + + $this->emitIssue( + $code_base, + $function->getContext(), + $issue_type, + $issue_message, + [ + $function->getRepresentationForIssue(), + $declared_return_type, + $actual_type->getDebugRepresentation() + ] + ); + } + } +} + +/** + * Represents the actual return types seen during analysis + * (including recursive analysis) + */ +class ElementTypeInfo +{ + /** @var FunctionInterface the function with the return values*/ + public $function; + /** @var Set the set of observed return types */ + public $types; + /** + * @param list $return_types + */ + public function __construct(FunctionInterface $function, array $return_types) + { + $this->function = $function; + $this->types = new Set($return_types); + } +} +MoreSpecificElementTypePlugin::$method_blacklist = new Set(); +MoreSpecificElementTypePlugin::$method_return_types = new Map(); + +/** + * This visitor analyzes node kinds that can be the root of expressions + * containing duplicate expressions, and is called on nodes in post-order. + */ +class MoreSpecificElementTypeVisitor extends PluginAwarePostAnalysisVisitor +{ + /** + * @param Node $node a node of kind ast\AST_RETURN, representing a return statement. + */ + public function visitReturn(Node $node): void + { + if (!$this->context->isInFunctionLikeScope()) { + return; + } + try { + $function = $this->context->getFunctionLikeInScope($this->code_base); + } catch (Exception $_) { + return; + } + if ($function->hasYield()) { + // TODO: Support analyzing yield key/value types of generators? + return; + } + if ($function instanceof Method) { + // Skip functions that are overrides or are overridden. + // They may be documenting a less specific return type to deal with the inheritance hierarchy. + if ($function->isOverride() || $function->isOverriddenByAnother()) { + return; + } + } + try { + // Fetch the list of valid classes, and warn about any undefined classes. + // (We have more specific issue types such as PhanNonClassMethodCall below, don't emit PhanTypeExpected*) + $union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']); + } catch (Exception $_) { + // Phan should already throw for this + return; + } + MoreSpecificElementTypePlugin::recordType($function, $union_type->withFlattenedArrayShapeOrLiteralTypeInstances()); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new MoreSpecificElementTypePlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/NoAssertPlugin.php b/bundled-libs/phan/phan/.phan/plugins/NoAssertPlugin.php new file mode 100644 index 000000000..212e8564b --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/NoAssertPlugin.php @@ -0,0 +1,85 @@ +children['expr']->children['name'] ?? null; + if (!is_string($name)) { + return; + } + if (strcasecmp($name, 'assert') !== 0) { + return; + } + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginNoAssert', + // phpcs:ignore Generic.Files.LineLength.MaxExceeded + 'assert() is discouraged. Although phan supports using assert() for type annotations, PHP\'s documentation recommends assertions only for debugging, and assert() has surprising behaviors.', + [] + ); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new NoAssertPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/NonBoolBranchPlugin.php b/bundled-libs/phan/phan/.phan/plugins/NonBoolBranchPlugin.php new file mode 100644 index 000000000..7ee39e402 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/NonBoolBranchPlugin.php @@ -0,0 +1,88 @@ +children as $if_node) { + if (!$if_node instanceof Node) { + throw new AssertionError("Expected if statement to be a node"); + } + $condition = $if_node->children['cond']; + + // dig nodes to avoid the NOT('!') operation converting its value to a boolean type. + // Also, use right-hand side of assignments such as `$x = (expr)` + while (($condition instanceof Node) && ( + ($condition->flags === ast\flags\UNARY_BOOL_NOT && $condition->kind === ast\AST_UNARY_OP) + || (\in_array($condition->kind, [\ast\AST_ASSIGN, \ast\AST_ASSIGN_REF], true))) + ) { + $condition = $condition->children['expr']; + } + + if ($condition === null) { + // $condition === null will be appeared in else-clause, then avoid them + continue; + } + + if ($condition instanceof Node) { + $this->context = $this->context->withLineNumberStart($condition->lineno); + } + // evaluate the type of conditional expression + try { + $union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $condition); + } catch (IssueException $_) { + return $this->context; + } + if (!$union_type->isEmpty() && !$union_type->isExclusivelyBoolTypes()) { + $this->emit( + 'PhanPluginNonBoolBranch', + 'Non bool value of type {TYPE} evaluated in if clause', + [(string)$union_type] + ); + } + } + return $this->context; + } +} + +return new NonBoolBranchPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/NonBoolInLogicalArithPlugin.php b/bundled-libs/phan/phan/.phan/plugins/NonBoolInLogicalArithPlugin.php new file mode 100644 index 000000000..40a8e4aba --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/NonBoolInLogicalArithPlugin.php @@ -0,0 +1,95 @@ +flags, self::BINARY_BOOL_OPERATORS, true)) { + // get left node and parse it + // (dig nodes to avoid NOT('!') operator's converting its value to boolean type) + $left_node = $node->children['left']; + while (isset($left_node->flags) && $left_node->flags === ast\flags\UNARY_BOOL_NOT) { + $left_node = $left_node->children['expr']; + } + + // get right node and parse it + $right_node = $node->children['right']; + while (isset($right_node->flags) && $right_node->flags === ast\flags\UNARY_BOOL_NOT) { + $right_node = $right_node->children['expr']; + } + + // get the type of two nodes + $left_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left_node); + $right_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $right_node); + + // if left or right type is NOT boolean, emit issue + if (!$left_type->isExclusivelyBoolTypes()) { + if ($left_node instanceof Node) { + $this->context = $this->context->withLineNumberStart($left_node->lineno); + } + $this->emit( + 'PhanPluginNonBoolInLogicalArith', + 'Non bool value of type {TYPE} in logical arithmetic', + [(string)$left_type] + ); + } + if (!$right_type->isExclusivelyBoolTypes()) { + if ($right_node instanceof Node) { + $this->context = $this->context->withLineNumberStart($right_node->lineno); + } + $this->emit( + 'PhanPluginNonBoolInLogicalArith', + 'Non bool value of type {TYPE} in logical arithmetic', + [(string)$right_type] + ); + } + } + return $this->context; + } +} + +return new NonBoolInLogicalArithPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/NotFullyQualifiedUsagePlugin.php b/bundled-libs/phan/phan/.phan/plugins/NotFullyQualifiedUsagePlugin.php new file mode 100644 index 000000000..d689fafd7 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/NotFullyQualifiedUsagePlugin.php @@ -0,0 +1,200 @@ + - Set after the constructor is called if an instance property with this name is declared + // protected $parent_node_list; + + // A plugin's visitors should NOT implement visit(), unless they need to. + + // phpcs:disable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase + public const NotFullyQualifiedFunctionCall = 'PhanPluginNotFullyQualifiedFunctionCall'; + public const NotFullyQualifiedOptimizableFunctionCall = 'PhanPluginNotFullyQualifiedOptimizableFunctionCall'; + public const NotFullyQualifiedGlobalConstant = 'PhanPluginNotFullyQualifiedGlobalConstant'; + // phpcs:enable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase + + /** + * Source of functions: `zend_try_compile_special_func` from https://github.com/php/php-src/blob/master/Zend/zend_compile.c + */ + private const OPTIMIZABLE_FUNCTIONS = [ + 'array_key_exists' => true, + 'array_slice' => true, + 'boolval' => true, + 'call_user_func' => true, + 'call_user_func_array' => true, + 'chr' => true, + 'count' => true, + 'defined' => true, + 'doubleval' => true, + 'floatval' => true, + 'func_get_args' => true, + 'func_num_args' => true, + 'get_called_class' => true, + 'get_class' => true, + 'gettype' => true, + 'in_array' => true, + 'intval' => true, + 'is_array' => true, + 'is_bool' => true, + 'is_double' => true, + 'is_float' => true, + 'is_int' => true, + 'is_integer' => true, + 'is_long' => true, + 'is_null' => true, + 'is_object' => true, + 'is_real' => true, + 'is_resource' => true, + 'is_string' => true, + 'ord' => true, + 'strlen' => true, + 'strval' => true, + ]; + + /** + * @param Node $node + * A node to analyze of type ast\AST_CALL (call to a global function) + * @override + */ + public function visitCall(Node $node): void + { + $expression = $node->children['expr']; + if (!($expression instanceof Node) || $expression->kind !== ast\AST_NAME) { + return; + } + if (($expression->flags & ast\flags\NAME_NOT_FQ) !== ast\flags\NAME_NOT_FQ) { + // This is namespace\foo() or \NS\foo() + return; + } + if ($this->context->getNamespace() === '\\') { + // This is in the global namespace and is always fully qualified + return; + } + $function_name = $expression->children['name']; + if (!is_string($function_name)) { + // Possibly redundant. + return; + } + // TODO: Probably wrong for ast\parse_code - should check namespace map of USE_NORMAL for 'ast' there. + // Same for ContextNode->getFunction() + if ($this->context->hasNamespaceMapFor(\ast\flags\USE_FUNCTION, $function_name)) { + return; + } + $this->warnNotFullyQualifiedFunctionCall($function_name, $expression); + } + + private function warnNotFullyQualifiedFunctionCall(string $function_name, Node $expression): void + { + if (array_key_exists(strtolower($function_name), self::OPTIMIZABLE_FUNCTIONS)) { + $issue_type = self::NotFullyQualifiedOptimizableFunctionCall; + $issue_msg = 'Expected function call to {FUNCTION}() to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}' + . ' (opcache can optimize fully qualified calls to this function in recent php versions)'; + } else { + $issue_type = self::NotFullyQualifiedFunctionCall; + $issue_msg = 'Expected function call to {FUNCTION}() to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}'; + } + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($expression->lineno), + $issue_type, + $issue_msg, + [$function_name, $this->context->getNamespace()] + ); + } + + /** + * @param Node $node + * A node to analyze of type ast\AST_CONST (reference to a constant) + * @override + */ + public function visitConst(Node $node): void + { + $expression = $node->children['name']; + if (!($expression instanceof Node) || $expression->kind !== ast\AST_NAME) { + return; + } + if (($expression->flags & ast\flags\NAME_NOT_FQ) !== ast\flags\NAME_NOT_FQ) { + // This is namespace\SOME_CONST or \NS\SOME_CONST + return; + } + if ($this->context->getNamespace() === '\\') { + // This is in the global namespace and is always fully qualified + return; + } + $constant_name = $expression->children['name']; + if (!is_string($constant_name)) { + // Possibly redundant. + return; + } + $constant_name_lower = strtolower($constant_name); + if ($constant_name_lower === 'true' || $constant_name_lower === 'false' || $constant_name_lower === 'null') { + // These are keywords and are the same in any namespace + return; + } + + // TODO: Probably wrong for ast\AST_NAME - should check namespace map of USE_NORMAL for 'ast' there. + // Same for ContextNode->getConst() + if ($this->context->hasNamespaceMapFor(\ast\flags\USE_CONST, $constant_name)) { + return; + } + $this->warnNotFullyQualifiedConstantUsage($constant_name, $expression); + } + + private function warnNotFullyQualifiedConstantUsage(string $constant_name, Node $expression): void + { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($expression->lineno), + self::NotFullyQualifiedGlobalConstant, + 'Expected usage of {CONST} to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}', + [$constant_name, $this->context->getNamespace()] + ); + } +} + +if (Config::isIssueFixingPluginEnabled()) { + require_once __DIR__ . '/NotFullyQualifiedUsagePlugin/fixers.php'; +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new NotFullyQualifiedUsagePlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/NotFullyQualifiedUsagePlugin/fixers.php b/bundled-libs/phan/phan/.phan/plugins/NotFullyQualifiedUsagePlugin/fixers.php new file mode 100644 index 000000000..d2fcd3d89 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/NotFullyQualifiedUsagePlugin/fixers.php @@ -0,0 +1,94 @@ +getLine(); + $expected_name = $instance->getTemplateParameters()[0]; + $edits = []; + foreach ($contents->getNodesAtLine($line) as $node) { + if (!$node instanceof QualifiedName) { + continue; + } + if ($node->globalSpecifier || $node->relativeSpecifier) { + // This is already qualified + continue; + } + $actual_name = (new NodeUtils($contents->getContents()))->phpParserNameToString($node); + if ($actual_name !== $expected_name) { + continue; + } + $is_actual_call = $node->parent instanceof CallExpression; + $is_expected_call = $instance->getIssue()->getType() !== NotFullyQualifiedUsageVisitor::NotFullyQualifiedGlobalConstant; + if ($is_actual_call !== $is_expected_call) { + IssueFixer::debug("skip check mismatch actual expected are call vs constants\n"); + // don't warn about constants with the same names as functions or vice-versa + continue; + } + try { + if ($is_expected_call) { + // Don't do this if the global function this refers to doesn't exist. + // TODO: Support namespaced functions + if (!$code_base->hasFunctionWithFQSEN(FullyQualifiedFunctionName::fromFullyQualifiedString($actual_name))) { + IssueFixer::debug("skip attempt to fix $actual_name() because function was not found in the global scope\n"); + return null; + } + } else { + // Don't do this if the global function this refers to doesn't exist. + // TODO: Support namespaced functions + if (!$code_base->hasGlobalConstantWithFQSEN(FullyQualifiedGlobalConstantName::fromFullyQualifiedString($actual_name))) { + IssueFixer::debug("skip attempt to fix $actual_name because the constant was not found in the global scope\n"); + return null; + } + } + } catch (Exception $_) { + continue; + } + //fwrite(STDERR, "name is: " . get_class($node->parent) . "\n"); + + // They are case-sensitively identical. + // Generate a fix. + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + $start = $node->getStart(); + $edits[] = new FileEdit($start, $start, '\\'); + } + if ($edits) { + return new FileEditSet($edits); + } + return null; + }; + IssueFixer::registerFixerClosure( + NotFullyQualifiedUsageVisitor::NotFullyQualifiedGlobalConstant, + $fix + ); + IssueFixer::registerFixerClosure( + NotFullyQualifiedUsageVisitor::NotFullyQualifiedFunctionCall, + $fix + ); + IssueFixer::registerFixerClosure( + NotFullyQualifiedUsageVisitor::NotFullyQualifiedOptimizableFunctionCall, + $fix + ); +}); diff --git a/bundled-libs/phan/phan/.phan/plugins/NumericalComparisonPlugin.php b/bundled-libs/phan/phan/.phan/plugins/NumericalComparisonPlugin.php new file mode 100644 index 000000000..db1acce3b --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/NumericalComparisonPlugin.php @@ -0,0 +1,86 @@ +children['left']; + $left_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left_node); + $right_node = $node->children['right']; + $right_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $right_node); + + // non numerical values are not allowed in the operator equal(==, !=) + if (in_array($node->flags, self::BINARY_EQUAL_OPERATORS, true)) { + if (!$left_type->isNonNullNumberType() && !$right_type->isNonNullNumberType()) { + $this->emit( + 'PhanPluginNumericalComparison', + "non numerical values compared by the operators '==' or '!='", + [] + ); + } + // numerical values are not allowed in the operator identical('===', '!==') + } elseif (in_array($node->flags, self::BINARY_IDENTICAL_OPERATORS, true)) { + if ($left_type->isNonNullNumberType() || $right_type->isNonNullNumberType()) { + // TODO: different name for this issue type? + $this->emit( + 'PhanPluginNumericalComparison', + "numerical values compared by the operators '===' or '!=='", + [] + ); + } + } + return $this->context; + } +} + +return new NumericalComparisonPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PHP53CompatibilityPlugin.php b/bundled-libs/phan/phan/.phan/plugins/PHP53CompatibilityPlugin.php new file mode 100644 index 000000000..6a66e4c6a --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PHP53CompatibilityPlugin.php @@ -0,0 +1,116 @@ +flags === ast\flags\ARRAY_SYNTAX_SHORT) { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginCompatibilityShortArray', + "Short arrays ({CODE}) require support for php 5.4+", + [ASTReverter::toShortString($node)] + ); + } + } + + /** + * @param Node $node + * A node to analyze of kind ast\AST_ARG_LIST + * @override + */ + public function visitArgList(Node $node): void + { + $lastArg = end($node->children); + if ($lastArg instanceof Node && $lastArg->kind === ast\AST_UNPACK) { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginCompatibilityArgumentUnpacking', + "Argument unpacking ({CODE}) requires support for php 5.6+", + [ASTReverter::toShortString($lastArg)] + ); + } + } + + /** + * @param Node $node + * A node to analyze of kind ast\AST_PARAM + * @override + */ + public function visitParam(Node $node): void + { + if ($node->flags & ast\flags\PARAM_VARIADIC) { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginCompatibilityVariadicParam', + "Variadic functions ({CODE}) require support for php 5.6+", + [ASTReverter::toShortString($node)] + ); + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new PHP53CompatibilityPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PHPDocInWrongCommentPlugin.php b/bundled-libs/phan/phan/.phan/plugins/PHPDocInWrongCommentPlugin.php new file mode 100644 index 000000000..45eabf26d --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PHPDocInWrongCommentPlugin.php @@ -0,0 +1,98 @@ +emitIssue( + $code_base, + (clone($context))->withLineNumberStart($token[2]), + 'PhanPluginPHPDocHashComment', + 'Saw comment starting with {COMMENT} in {COMMENT} - consider using {COMMENT} instead to avoid confusion with php 8.0 {COMMENT} attributes', + ['#', StringUtil::jsonEncode(self::truncate(trim($comment_string))), '//', '#['] + ); + } + continue; + } + if (strpos($comment_string, '@') === false) { + continue; + } + $lineno = $token[2]; + + // @phan-suppress-next-line PhanAccessClassConstantInternal + $comment = Comment::fromStringInContext("/**" . $comment_string, $code_base, $context, $lineno, Comment::ON_ANY); + + if ($comment instanceof NullComment) { + continue; + } + $this->emitIssue( + $code_base, + (clone($context))->withLineNumberStart($token[2]), + 'PhanPluginPHPDocInWrongComment', + 'Saw possible phpdoc annotation in ordinary block comment {COMMENT}. PHPDoc comments should start with "/**" (followed by whitespace), not "/*"', + [StringUtil::jsonEncode(self::truncate($comment_string))] + ); + } + } + + private static function truncate(string $token): string + { + if (strlen($token) > 200) { + return mb_substr($token, 0, 200) . "..."; + } + return $token; + } +} +if (!function_exists('token_get_all')) { + throw new UnloadablePluginException("PHPDocInWrongCommentPlugin requires the tokenizer extension, which is not enabled (this plugin uses token_get_all())"); +} +return new PHPDocInWrongCommentPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PHPDocRedundantPlugin.php b/bundled-libs/phan/phan/.phan/plugins/PHPDocRedundantPlugin.php new file mode 100644 index 000000000..c22f55c52 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PHPDocRedundantPlugin.php @@ -0,0 +1,235 @@ +isMagic() || $method->isPHPInternal()) { + return; + } + if ($method->getFQSEN() !== $method->getDefiningFQSEN()) { + return; + } + self::analyzeFunctionLike($code_base, $method); + } + + /** + * @suppress PhanAccessClassConstantInternal + */ + private static function isRedundantFunctionComment(FunctionInterface $method, string $doc_comment): bool + { + $lines = explode("\n", $doc_comment); + foreach ($lines as $line) { + $line = trim($line, " \r\n\t*/"); + if ($line === '') { + continue; + } + if ($line[0] !== '@') { + return false; + } + if (!preg_match('/^@(phan-)?(param|return)\s/', $line)) { + return false; + } + if (preg_match(Builder::PARAM_COMMENT_REGEX, $line, $matches)) { + if ($matches[0] !== $line) { + // There's a description after the (at)param annotation + return false; + } + } elseif (preg_match(Builder::RETURN_COMMENT_REGEX, $line, $matches)) { + if ($matches[0] !== $line) { + // There's a description after the (at)return annotation + return false; + } + } else { + // This is not a valid annotation. It might be documentation. + return false; + } + } + $comment = $method->getComment(); + if (!$comment) { + // unparseable? + return false; + } + if ($comment->hasReturnUnionType()) { + $comment_return_type = $comment->getReturnType(); + if (!$comment_return_type->isEmpty() && !$comment_return_type->asNormalizedTypes()->isEqualTo($method->getRealReturnType())) { + return false; + } + } + if (count($comment->getParameterList()) > 0) { + return false; + } + foreach ($comment->getParameterMap() as $comment_param_name => $param) { + $comment_param_type = $param->getUnionType()->asNormalizedTypes(); + if ($comment_param_type->isEmpty()) { + return false; + } + foreach ($method->getRealParameterList() as $real_param) { + if ($real_param->getName() === $comment_param_name) { + if ($real_param->getUnionType()->isEqualTo($comment_param_type)) { + // This is redundant, check remaining parameters. + continue 2; + } + } + } + // could not find that comment param, Phan warns elsewhere. + // Assume this is not redundant. + return false; + } + return true; + } + + private static function analyzeFunctionLike(CodeBase $code_base, FunctionInterface $method): void + { + if (Phan::isExcludedAnalysisFile($method->getContext()->getFile())) { + // This has no side effects, so we can skip files that don't need to be analyzed + return; + } + $comment = $method->getDocComment(); + if (!StringUtil::isNonZeroLengthString($comment)) { + return; + } + if (!self::isRedundantFunctionComment($method, $comment)) { + self::checkIsRedundantReturn($code_base, $method, $comment); + return; + } + $encoded_comment = StringUtil::encodeValue($comment); + if ($method instanceof Method) { + self::emitIssue( + $code_base, + $method->getContext(), + self::RedundantMethodComment, + 'Redundant doc comment on method {METHOD}(). Either add a description or remove the comment: {COMMENT}', + [$method->getName(), $encoded_comment] + ); + } elseif ($method instanceof Func && $method->isClosure()) { + self::emitIssue( + $code_base, + $method->getContext(), + self::RedundantClosureComment, + 'Redundant doc comment on closure {FUNCTION}. Either add a description or remove the comment: {COMMENT}', + [$method->getNameForIssue(), $encoded_comment] + ); + } else { + self::emitIssue( + $code_base, + $method->getContext(), + self::RedundantFunctionComment, + 'Redundant doc comment on function {FUNCTION}(). Either add a description or remove the comment: {COMMENT}', + [$method->getName(), $encoded_comment] + ); + } + } + + private static function checkIsRedundantReturn(CodeBase $code_base, FunctionInterface $method, string $doc_comment): void + { + if (strpos($doc_comment, '@return') === false) { + return; + } + $comment = $method->getComment(); + if (!$comment) { + // unparseable? + return; + } + if ($method->getRealReturnType()->isEmpty()) { + return; + } + if (!$comment->hasReturnUnionType()) { + return; + } + $comment_return_type = $comment->getReturnType(); + if (!$comment_return_type->asNormalizedTypes()->isEqualTo($method->getRealReturnType())) { + return; + } + $lines = explode("\n", $doc_comment); + for ($i = count($lines) - 1; $i >= 0; $i--) { + $line = $lines[$i]; + $line = trim($line, " \r\n\t*/"); + if ($line === '') { + continue; + } + if ($line[0] !== '@') { + return; + } + if (!preg_match('/^@(phan-)?return\s/', $line)) { + continue; + } + // @phan-suppress-next-line PhanAccessClassConstantInternal + if (!preg_match(Builder::RETURN_COMMENT_REGEX, $line, $matches)) { + return; + } + if ($matches[0] !== $line) { + // There's a description after the (at)return annotation + return; + } + self::emitIssue( + $code_base, + $method->getContext()->withLineNumberStart($comment->getReturnLineno()), + self::RedundantReturnComment, + 'Redundant @return {TYPE} on function {FUNCTION}. Either add a description or remove the @return annotation: {COMMENT}', + [$comment_return_type, $method->getNameForIssue(), $line] + ); + return; + } + } + + /** + * @return array + */ + public function getAutomaticFixers(): array + { + require_once __DIR__ . '/PHPDocRedundantPlugin/Fixers.php'; + $function_like_fixer = Closure::fromCallable([Fixers::class, 'fixRedundantFunctionLikeComment']); + return [ + self::RedundantFunctionComment => $function_like_fixer, + self::RedundantMethodComment => $function_like_fixer, + self::RedundantClosureComment => $function_like_fixer, + self::RedundantReturnComment => Closure::fromCallable([Fixers::class, 'fixRedundantReturnComment']), + ]; + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new PHPDocRedundantPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PHPDocRedundantPlugin/Fixers.php b/bundled-libs/phan/phan/.phan/plugins/PHPDocRedundantPlugin/Fixers.php new file mode 100644 index 000000000..73f89c19d --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PHPDocRedundantPlugin/Fixers.php @@ -0,0 +1,187 @@ +getTemplateParameters(); + $name = $params[0]; + $encoded_comment = $params[1]; + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $name); + if (!$declaration) { + return null; + } + return self::computeEditsToRemoveFunctionLikeComment($contents, $declaration, (string)$encoded_comment); + } + + private static function computeEditsToRemoveFunctionLikeComment(FileCacheEntry $contents, FunctionLike $declaration, string $encoded_comment): ?FileEditSet + { + if (!$declaration instanceof PhpParser\Node) { + // impossible + return null; + } + $comment_token = self::getDocCommentToken($declaration); + if (!$comment_token) { + return null; + } + $file_contents = $contents->getContents(); + $comment = $comment_token->getText($file_contents); + $actual_encoded_comment = StringUtil::encodeValue($comment); + if ($actual_encoded_comment !== $encoded_comment) { + return null; + } + return self::computeEditSetToDeleteComment($file_contents, $comment_token); + } + + private static function computeEditSetToDeleteComment(string $file_contents, Token $comment_token): FileEditSet + { + // get the byte where the `)` of the argument list ends + $last_byte_index = $comment_token->getEndPosition(); + $first_byte_index = $comment_token->start; + // Skip leading whitespace and the previous newline, if those were found + for (; $first_byte_index > 0; $first_byte_index--) { + $prev_byte = $file_contents[$first_byte_index - 1]; + switch ($prev_byte) { + case " ": + case "\t": + // keep skipping previous bytes of whitespace + break; + case "\n": + $first_byte_index--; + if ($first_byte_index > 0 && $file_contents[$first_byte_index - 1] === "\r") { + $first_byte_index--; + } + break 2; + case "\r": + $first_byte_index--; + break 2; + default: + // This is not whitespace, so stop. + break 2; + } + } + $file_edit = new FileEdit($first_byte_index, $last_byte_index, ''); + return new FileEditSet([$file_edit]); + } + + /** + * Add a missing return type to the real signature + * @param CodeBase $code_base @unused-param + */ + public static function fixRedundantReturnComment( + CodeBase $code_base, + FileCacheEntry $contents, + IssueInstance $instance + ): ?FileEditSet { + $lineno = $instance->getLine(); + $file_lines = $contents->getLines(); + + $line = \trim($file_lines[$lineno]); + // @phan-suppress-next-line PhanAccessClassConstantInternal + if (!\preg_match(Builder::RETURN_COMMENT_REGEX, $line)) { + return null; + } + $first_deleted_line = $lineno; + $last_deleted_line = $lineno; + $is_blank_comment_line = static function (int $i) use ($file_lines): bool { + return \trim($file_lines[$i] ?? '') === '*'; + }; + while ($is_blank_comment_line($first_deleted_line - 1)) { + $first_deleted_line--; + } + while ($is_blank_comment_line($last_deleted_line + 1)) { + $last_deleted_line++; + } + $start_offset = $contents->getLineOffset($first_deleted_line); + $end_offset = $contents->getLineOffset($last_deleted_line + 1); + if (!$start_offset || !$end_offset) { + return null; + } + // Return an edit to delete the `(at)return RedundantType` and the surrounding blank comment lines + return new FileEditSet([new FileEdit($start_offset, $end_offset, '')]); + } + + /** + * @suppress PhanThrowTypeAbsentForCall + * @suppress PhanUndeclaredClassMethod + * @suppress UnusedSuppression false positive for PhpTokenizer with polyfill due to https://github.com/Microsoft/tolerant-php-parser/issues/292 + */ + private static function getDocCommentToken(PhpParser\Node $node): ?Token + { + $leadingTriviaText = $node->getLeadingCommentAndWhitespaceText(); + $leadingTriviaTokens = PhpTokenizer::getTokensArrayFromContent( + $leadingTriviaText, + ParseContext::SourceElements, + $node->getFullStart(), + false + ); + for ($i = \count($leadingTriviaTokens) - 1; $i >= 0; $i--) { + $token = $leadingTriviaTokens[$i]; + if ($token->kind === TokenKind::DocCommentToken) { + return $token; + } + } + return null; + } + + private static function findFunctionLikeDeclaration( + FileCacheEntry $contents, + int $line, + string $name + ): ?FunctionLike { + $candidates = []; + foreach ($contents->getNodesAtLine($line) as $node) { + if ($node instanceof FunctionDeclaration || $node instanceof MethodDeclaration) { + $name_node = $node->name; + if (!$name_node) { + continue; + } + $declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($name_node); + if ($declaration_name === $name) { + $candidates[] = $node; + } + } elseif ($node instanceof AnonymousFunctionCreationExpression) { + if (\preg_match('/^Closure\(/', $name)) { + $candidates[] = $node; + } + } + } + if (\count($candidates) === 1) { + return $candidates[0]; + } + return null; + } +} diff --git a/bundled-libs/phan/phan/.phan/plugins/PHPDocToRealTypesPlugin.php b/bundled-libs/phan/phan/.phan/plugins/PHPDocToRealTypesPlugin.php new file mode 100644 index 000000000..e1d3b21af --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PHPDocToRealTypesPlugin.php @@ -0,0 +1,161 @@ + */ + private $deferred_analysis_methods = []; + + /** + * @return array + */ + public function getAutomaticFixers(): array + { + require_once __DIR__ . '/PHPDocToRealTypesPlugin/Fixers.php'; + $param_closure = Closure::fromCallable([Fixers::class, 'fixParamType']); + $return_closure = Closure::fromCallable([Fixers::class, 'fixReturnType']); + return [ + self::CanUsePHP71Void => $return_closure, + self::CanUseReturnType => $return_closure, + self::CanUseNullableReturnType => $return_closure, + self::CanUseNullableParamType => $param_closure, + self::CanUseParamType => $param_closure, + ]; + } + + public function analyzeFunction(CodeBase $code_base, Func $function): void + { + self::analyzeFunctionLike($code_base, $function); + } + + /** + * @param CodeBase $code_base @unused-param + */ + public function analyzeMethod(CodeBase $code_base, Method $method): void + { + if ($method->isFromPHPDoc() || $method->isMagic() || $method->isPHPInternal()) { + return; + } + if ($method->getFQSEN() !== $method->getDefiningFQSEN()) { + return; + } + $this->deferred_analysis_methods[$method->getFQSEN()->__toString()] = $method; + } + + public function beforeAnalyzePhase(CodeBase $code_base): void + { + $ignore_overrides = (bool)getenv('PHPDOC_TO_REAL_TYPES_IGNORE_INHERITANCE'); + foreach ($this->deferred_analysis_methods as $method) { + if ($method->isOverride() || $method->isOverriddenByAnother()) { + if (!$ignore_overrides) { + continue; + } + } + self::analyzeFunctionLike($code_base, $method); + } + } + + private static function analyzeFunctionLike(CodeBase $code_base, FunctionInterface $method): void + { + if (Phan::isExcludedAnalysisFile($method->getContext()->getFile())) { + // This has no side effects, so we can skip files that don't need to be analyzed + return; + } + if ($method->getRealReturnType()->isEmpty()) { + self::analyzeReturnTypeOfFunctionLike($code_base, $method); + } + $phpdoc_param_list = $method->getParameterList(); + foreach ($method->getRealParameterList() as $i => $parameter) { + if (!$parameter->getNonVariadicUnionType()->isEmpty()) { + continue; + } + $phpdoc_param = $phpdoc_param_list[$i]; + if (!$phpdoc_param) { + continue; + } + $union_type = $phpdoc_param->getNonVariadicUnionType()->asNormalizedTypes(); + if ($union_type->typeCount() !== 1) { + continue; + } + $type = $union_type->getTypeSet()[0]; + if (!$type->canUseInRealSignature()) { + continue; + } + self::emitIssue( + $code_base, + $method->getContext(), + $type->isNullable() ? self::CanUseNullableParamType : self::CanUseParamType, + 'Can use {TYPE} as the type of parameter ${PARAMETER} of {METHOD}', + [$type->asSignatureType(), $parameter->getName(), $method->getName()] + ); + } + } + + private static function analyzeReturnTypeOfFunctionLike(CodeBase $code_base, FunctionInterface $method): void + { + $union_type = $method->getUnionType(); + if ($union_type->isVoidType()) { + self::emitIssue( + $code_base, + $method->getContext(), + self::CanUsePHP71Void, + 'Can use php 7.1\'s {TYPE} as a return type of {METHOD}', + ['void', $method->getName()] + ); + return; + } + $union_type = $union_type->asNormalizedTypes(); + if ($union_type->typeCount() !== 1) { + return; + } + $type = $union_type->getTypeSet()[0]; + if (!$type->canUseInRealSignature()) { + return; + } + self::emitIssue( + $code_base, + $method->getContext(), + $type->isNullable() ? self::CanUseNullableReturnType : self::CanUseReturnType, + 'Can use {TYPE} as a return type of {METHOD}', + [$type->asSignatureType(), $method->getName()] + ); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new PHPDocToRealTypesPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PHPDocToRealTypesPlugin/Fixers.php b/bundled-libs/phan/phan/.phan/plugins/PHPDocToRealTypesPlugin/Fixers.php new file mode 100644 index 000000000..6d74c90f1 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PHPDocToRealTypesPlugin/Fixers.php @@ -0,0 +1,133 @@ +getTemplateParameters(); + $return_type = $params[0]; + $name = $params[1]; + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $name); + if (!$declaration) { + return null; + } + return self::computeEditsForReturnTypeDeclaration($declaration, (string)$return_type); + } + + /** + * Add a missing param type to the real signature + * @unused-param $code_base + */ + public static function fixParamType( + CodeBase $code_base, + FileCacheEntry $contents, + IssueInstance $instance + ): ?FileEditSet { + $params = $instance->getTemplateParameters(); + $param_type = $params[0]; + $param_name = $params[1]; + $method_name = $params[2]; + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $method_name); + if (!$declaration) { + return null; + } + return self::computeEditsForParamTypeDeclaration($contents, $declaration, (string)$param_name, (string)$param_type); + } + + private static function computeEditsForReturnTypeDeclaration(FunctionLike $declaration, string $return_type): ?FileEditSet + { + if ($return_type === '') { + return null; + } + // @phan-suppress-next-line PhanUndeclaredProperty + $close_bracket = $declaration->anonymousFunctionUseClause->closeParen ?? $declaration->closeParen; + if (!$close_bracket instanceof Token) { + return null; + } + // get the byte where the `)` of the argument list ends + $last_byte_index = $close_bracket->getEndPosition(); + $file_edit = new FileEdit($last_byte_index, $last_byte_index, " : $return_type"); + return new FileEditSet([$file_edit]); + } + + private static function computeEditsForParamTypeDeclaration(FileCacheEntry $contents, FunctionLike $declaration, string $param_name, string $param_type): ?FileEditSet + { + if ($param_type === '') { + return null; + } + // @phan-suppress-next-line PhanUndeclaredProperty + $parameter_node_list = $declaration->parameters->children ?? []; + foreach ($parameter_node_list as $param) { + if (!$param instanceof PhpParser\Node\Parameter) { + continue; + } + $declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($param->variableName); + if ($declaration_name !== $param_name) { + continue; + } + $token = $param->byRefToken ?? $param->dotDotDotToken ?? $param->variableName; + $token_start_index = $token->start; + $file_edit = new FileEdit($token_start_index, $token_start_index, "$param_type "); + return new FileEditSet([$file_edit]); + } + return null; + } + + private static function findFunctionLikeDeclaration( + FileCacheEntry $contents, + int $line, + string $name + ): ?FunctionLike { + $candidates = []; + foreach ($contents->getNodesAtLine($line) as $node) { + if ($node instanceof FunctionDeclaration || $node instanceof MethodDeclaration) { + $name_node = $node->name; + if (!$name_node) { + continue; + } + $declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($name_node); + if ($declaration_name === $name) { + $candidates[] = $node; + } + } elseif ($node instanceof AnonymousFunctionCreationExpression) { + if ($name === '{closure}') { + $candidates[] = $node; + } + } + } + if (\count($candidates) === 1) { + return $candidates[0]; + } + return null; + } +} diff --git a/bundled-libs/phan/phan/.phan/plugins/PHPUnitAssertionPlugin.php b/bundled-libs/phan/phan/.phan/plugins/PHPUnitAssertionPlugin.php new file mode 100644 index 000000000..3b9cceedb --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PHPUnitAssertionPlugin.php @@ -0,0 +1,241 @@ +hasClassWithFQSEN($assert_class_fqsen)) { + if (!getenv('PHAN_PHPUNIT_ASSERTION_PLUGIN_QUIET')) { + // @phan-suppress-next-line PhanPluginRemoveDebugCall + fwrite(STDERR, "PHPUnitAssertionPlugin failed to find class PHPUnit\Framework\Assert, giving up (set environment variable PHAN_PHPUNIT_ASSERTION_PLUGIN_QUIET=1 to ignore this)\n"); + } + return []; + } + $result = []; + foreach ($code_base->getClassByFQSEN($assert_class_fqsen)->getMethodMap($code_base) as $method) { + $closure = $this->createClosureForMethod($code_base, $method, $method->getName()); + if (!$closure) { + continue; + } + $result[(string)$method->getFQSEN()] = $closure; + } + return $result; + } + + /** + * @return ?Closure(CodeBase, Context, FunctionInterface, array, ?Node):void + * @suppress PhanAccessClassConstantInternal, PhanAccessMethodInternal + */ + private function createClosureForMethod(CodeBase $code_base, Method $method, string $name): ?Closure + { + // TODO: Add a helper method which will convert a doc comment and a stub php function source code to a closure for a param index (or indices) + switch (\strtolower($name)) { + case 'asserttrue': + case 'assertnotfalse': + return $method->createClosureForAssertion( + $code_base, + new Assertion(UnionType::empty(), 'unusedParamName', Assertion::IS_TRUE), + 0 + ); + case 'assertfalse': + case 'assertnottrue': + return $method->createClosureForAssertion( + $code_base, + new Assertion(UnionType::empty(), 'unusedParamName', Assertion::IS_FALSE), + 0 + ); + // TODO: Rest of https://github.com/sebastianbergmann/phpunit/issues/3368 + case 'assertisstring': + // TODO: Could convert to real types? + return $method->createClosureForAssertion( + $code_base, + new Assertion(UnionType::fromFullyQualifiedPHPDocString('string'), 'unusedParamName', Assertion::IS_OF_TYPE), + 0 + ); + case 'assertnull': + return $method->createClosureForAssertion( + $code_base, + new Assertion(UnionType::fromFullyQualifiedPHPDocString('null'), 'unusedParamName', Assertion::IS_OF_TYPE), + 0 + ); + case 'assertnotnull': + return $method->createClosureForAssertion( + $code_base, + new Assertion(UnionType::fromFullyQualifiedPHPDocString('null'), 'unusedParamName', Assertion::IS_NOT_OF_TYPE), + 0 + ); + case 'assertsame': + // Sets the type of $actual to $expected + // + // This is equivalent to the side effects of the below doc comment. + // Note that the doc comment would make phan emit warnings about invalid classes, etc. + // TODO: Reuse the code for templates here + // + // (at)template T + // (at)param T $expected + // (at)param mixed $actual + // (at)phan-assert T $actual + return $method->createClosureForUnionTypeExtractorAndAssertionType( + /** + * @param list $args + */ + static function (CodeBase $code_base, Context $context, array $args): UnionType { + if (\count($args) < 2) { + return UnionType::empty(); + } + return UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[0]); + }, + Assertion::IS_OF_TYPE, + 1 + ); + case 'assertinternaltype': + return $method->createClosureForUnionTypeExtractorAndAssertionType( + /** + * @param list $args + */ + function (CodeBase $code_base, Context $context, array $args): UnionType { + if (\count($args) < 2) { + return UnionType::empty(); + } + $string = $args[0]; + if ($string instanceof ast\Node) { + $string = (UnionTypeVisitor::unionTypeFromNode($code_base, $context, $string))->asSingleScalarValueOrNull(); + } + if (!is_string($string)) { + return UnionType::empty(); + } + $original_type = (UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[1])); + switch ($string) { + case 'numeric': + return UnionType::fromFullyQualifiedPHPDocString('int|float|string'); + case 'integer': + case 'int': + return UnionType::fromFullyQualifiedPHPDocString('int'); + + case 'double': + case 'float': + case 'real': + return UnionType::fromFullyQualifiedPHPDocString('float'); + + case 'string': + return UnionType::fromFullyQualifiedPHPDocString('string'); + + case 'boolean': + case 'bool': + return UnionType::fromFullyQualifiedPHPDocString('bool'); + + case 'null': + return UnionType::fromFullyQualifiedPHPDocString('null'); + + case 'array': + $result = $original_type->arrayTypes(); + if ($result->isEmpty()) { + return UnionType::fromFullyQualifiedPHPDocString('array'); + } + return $result; + case 'object': + $result = $original_type->objectTypes(); + if ($result->isEmpty()) { + return UnionType::fromFullyQualifiedPHPDocString('object'); + } + return $result; + case 'resource': + return UnionType::fromFullyQualifiedPHPDocString('resource'); + case 'scalar': + $result = $original_type->scalarTypes(); + if ($result->isEmpty()) { + return UnionType::fromFullyQualifiedPHPDocString('int|string|float|bool'); + } + return $result; + + case 'callable': + $result = $original_type->callableTypes(); + if ($result->isEmpty()) { + return UnionType::fromFullyQualifiedPHPDocString('callable'); + } + return $result; + } + // Warn about possibly invalid assertion + // NOTE: This is only emitted for variables + $this->emitPluginIssue( + $code_base, + $context, + 'PhanPluginPHPUnitAssertionInvalidInternalType', + 'Unknown type {STRING_LITERAL} in call to assertInternalType', + [$string] + ); + + return UnionType::empty(); + }, + Assertion::IS_OF_TYPE, + 1 + ); + case 'assertinstanceof': + // This is equivalent to the side effects of the below doc comment. + // Note that the doc comment would make phan emit warnings about invalid classes, etc. + // TODO: Reuse the code for class-string here. + // + // (at)template T + // (at)param class-string $expected + // (at)param mixed $actual + // (at)phan-assert T $actual + return $method->createClosureForUnionTypeExtractorAndAssertionType( + /** + * @param list $args + */ + static function (CodeBase $code_base, Context $context, array $args): UnionType { + if (\count($args) < 2) { + return UnionType::empty(); + } + $string = (UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[0]))->asSingleScalarValueOrNull(); + if (!is_string($string)) { + return UnionType::empty(); + } + try { + return FullyQualifiedClassName::fromFullyQualifiedString($string)->asType()->asPHPDocUnionType(); + } catch (\Exception $_) { + return UnionType::empty(); + } + }, + Assertion::IS_OF_TYPE, + 1 + ); + } + return null; + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new PHPUnitAssertionPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PHPUnitNotDeadCodePlugin.php b/bundled-libs/phan/phan/.phan/plugins/PHPUnitNotDeadCodePlugin.php new file mode 100644 index 000000000..a7858fa6b --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PHPUnitNotDeadCodePlugin.php @@ -0,0 +1,154 @@ +code_base contains all class definitions + * @override + * @unused-param $node + */ + public function visitClass(Node $node): void + { + if (!Config::get_track_references()) { + return; + } + $code_base = $this->code_base; + if (!$code_base->hasClassWithFQSEN(self::$phpunit_test_case_fqsen)) { + if (!self::$did_warn_missing_class) { + // @phan-suppress-next-line PhanPluginRemoveDebugCall + fprintf(STDERR, "Using plugin %s but could not find PHPUnit\Framework\TestCase\n", self::class); + self::$did_warn_missing_class = true; + } + return; + } + // This assumes PreOrderAnalysisVisitor->visitClass is called first. + $context = $this->context; + $class = $context->getClassInScope($code_base); + if (!$class->getFQSEN()->asType()->asExpandedTypes($code_base)->hasType(self::$phpunit_test_case_type)) { + // This isn't a phpunit test case. + return; + } + + // Mark subclasses of TestCase as referenced + $class->addReference($context); + // Mark all test cases as referenced + foreach ($class->getMethodMap($code_base) as $method) { + if (static::isTestCase($method)) { + // TODO: Parse @dataProvider methodName, check for method existence, + // then mark method for dataProvider as referenced. + $method->addReference($context); + $this->markDataProvidersAsReferenced($class, $method); + } + } + // https://phpunit.de/manual/current/en/fixtures.html (PHPUnit framework checks for this override) + if ($class->hasPropertyWithName($code_base, 'backupStaticAttributesBlacklist')) { + $property = $class->getPropertyByName($code_base, 'backupStaticAttributesBlacklist'); + $property->addReference($context); + $property->setHasReadReference(); + } + } + + /** + * This regex contains a single pattern, which matches a valid PHP identifier. + * (e.g. for variable names, magic property names, etc. + * This does not allow backslashes. + */ + private const WORD_REGEX = '([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)'; + + /** + * Marks all data provider methods as being referenced + * + * @param Method $method the Method representing a unit test in a test case subclass + */ + private function markDataProvidersAsReferenced(Clazz $class, Method $method): void + { + if (preg_match('/@dataProvider\s+' . self::WORD_REGEX . '/', $method->getNode()->children['docComment'] ?? '', $match)) { + $data_provider_name = $match[1]; + if ($class->hasMethodWithName($this->code_base, $data_provider_name, true)) { + $class->getMethodByName($this->code_base, $data_provider_name)->addReference($this->context); + } + } + } + + /** + * @return bool true if $method is a PHPUnit test case + */ + protected static function isTestCase(Method $method): bool + { + if (!$method->isPublic()) { + return false; + } + if (preg_match('@^test@i', $method->getName())) { + return true; + } + if (preg_match('/@test\b/', $method->getNode()->children['docComment'] ?? '')) { + return true; + } + return false; + } + + /** + * Static initializer for this plugin - Gets called below before any methods can be used + * @suppress PhanThrowTypeAbsentForCall this FQSEN is valid + */ + public static function init(): void + { + $fqsen = FullyQualifiedClassName::make('\\PHPUnit\Framework', 'TestCase'); + self::$phpunit_test_case_fqsen = $fqsen; + self::$phpunit_test_case_type = $fqsen->asType(); + } +} +PHPUnitNotDeadPluginVisitor::init(); + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new PHPUnitNotDeadCodePlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PhanSelfCheckPlugin.php b/bundled-libs/phan/phan/.phan/plugins/PhanSelfCheckPlugin.php new file mode 100644 index 000000000..27bf534da --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PhanSelfCheckPlugin.php @@ -0,0 +1,271 @@ +):void + */ + $make_array_issue_callback = static function (int $fmt_index, int $arg_index): Closure { + /** + * @param list $args the nodes for the arguments to the invocation + */ + return static function ( + CodeBase $code_base, + Context $context, + FunctionInterface $unused_function, + array $args + ) use ( + $fmt_index, + $arg_index +): void { + if (\count($args) <= $fmt_index) { + return; + } + // TODO: Check for AST_UNPACK + $issue_message_template = $args[$fmt_index]; + if ($issue_message_template instanceof Node) { + $issue_message_template = (new ContextNode($code_base, $context, $issue_message_template))->getEquivalentPHPScalarValue(); + } + if (!is_string($issue_message_template)) { + return; + } + $issue_message_arg_count = self::computeArraySize($code_base, $context, $args[$arg_index] ?? null); + if ($issue_message_arg_count === null) { + return; + } + self::checkIssueTemplateUsage($code_base, $context, $issue_message_template, $issue_message_arg_count); + }; + }; + /** + * @param int $type_index the index of a parameter expecting an issue type (e.g. PhanParamTooMany) + * @param int $arg_index the index of an array parameter expecting sequential arguments. This is >= $type_index. + * @return Closure(CodeBase, Context, FunctionInterface, list):void + */ + $make_type_and_parameters_callback = static function (int $type_index, int $arg_index): Closure { + /** + * @param list $args the nodes for the arguments to the invocation + */ + return static function ( + CodeBase $code_base, + Context $context, + FunctionInterface $function, + array $args + ) use ( + $type_index, + $arg_index +): void { + if (\count($args) <= $type_index) { + return; + } + // TODO: Check for AST_UNPACK + $issue_type = $args[$type_index]; + if ($issue_type instanceof Node) { + $issue_type = (new ContextNode($code_base, $context, $issue_type))->getEquivalentPHPScalarValue(); + } + if (!is_string($issue_type)) { + return; + } + $issue = self::getIssueOrWarn($code_base, $context, $function, $issue_type); + if (!$issue) { + return; + } + $issue_message_arg_count = self::computeArraySize($code_base, $context, $args[$arg_index] ?? null); + if ($issue_message_arg_count === null) { + return; + } + self::checkIssueTemplateUsage($code_base, $context, $issue->getTemplate(), $issue_message_arg_count); + }; + }; + /** + * @param int $type_index the index of a parameter expecting an issue type (e.g. PhanParamTooMany) + * @param int $arg_index the index of an array parameter expecting variable arguments. This is >= $type_index. + * @return Closure(CodeBase, Context, FunctionInterface, list):void + */ + $make_type_and_varargs_callback = static function (int $type_index, int $arg_index): Closure { + /** + * @param list $args the nodes for the arguments to the invocation + */ + return static function ( + CodeBase $code_base, + Context $context, + FunctionInterface $function, + array $args + ) use ( + $type_index, + $arg_index +): void { + if (\count($args) <= $type_index) { + return; + } + // TODO: Check for AST_UNPACK + $issue_type = $args[$type_index]; + if ($issue_type instanceof Node) { + $issue_type = (new ContextNode($code_base, $context, $issue_type))->getEquivalentPHPScalarValue(); + } + if (!is_string($issue_type)) { + return; + } + $issue = self::getIssueOrWarn($code_base, $context, $function, $issue_type); + if (!$issue) { + return; + } + if ((\end($args)->kind ?? null) === \ast\AST_UNPACK) { + // give up + return; + } + // number of args passed to varargs. >= 0 if valid. + $issue_message_arg_count = count($args) - $arg_index; + if ($issue_message_arg_count < 0) { + // should already emit PhanParamTooFew + return; + } + self::checkIssueTemplateUsage($code_base, $context, $issue->getTemplate(), $issue_message_arg_count); + }; + }; + /** + * Analyzes a call to plugin->emitIssue($code_base, $context, $issue_type, $issue_message_fmt, $args) + */ + $short_emit_issue_callback = $make_type_and_varargs_callback(0, 2); + + $results = [ + '\Phan\AST\ContextNode::emitIssue' => $short_emit_issue_callback, + '\Phan\Issue::emit' => $make_type_and_varargs_callback(0, 3), + '\Phan\Issue::emitWithParameters' => $make_type_and_parameters_callback(0, 3), + '\Phan\Issue::maybeEmit' => $make_type_and_varargs_callback(2, 4), + '\Phan\Issue::maybeEmitWithParameters' => $make_type_and_parameters_callback(2, 4), + '\Phan\Analysis\BinaryOperatorFlagVisitor::emitIssue' => $short_emit_issue_callback, + '\Phan\Language\Element\Comment\Builder::emitIssue' => $make_type_and_parameters_callback(0, 2), + ]; + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + $emit_plugin_issue_fqsen = FullyQualifiedMethodName::fromFullyQualifiedString('\Phan\PluginV3\IssueEmitter::emitPluginIssue'); + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + $analysis_visitor_fqsen = FullyQualifiedMethodName::fromFullyQualifiedString('\Phan\AST\AnalysisVisitor::emitIssue'); + + $emit_plugin_issue_callback = $make_array_issue_callback(3, 4); + foreach ($code_base->getMethodSet() as $method) { + $real_fqsen = $method->getRealDefiningFQSEN(); + if ($real_fqsen === $emit_plugin_issue_fqsen) { + $results[(string)$method->getFQSEN()] = $emit_plugin_issue_callback; + } elseif ($real_fqsen === $analysis_visitor_fqsen) { + $results[(string)$method->getFQSEN()] = $short_emit_issue_callback; + } + } + return $results; + } + + private static function getIssueOrWarn(CodeBase $code_base, Context $context, FunctionInterface $function, string $issue_type): ?Issue + { + // Calling Issue::fromType() would print a backtrace to stderr + $issue = Issue::issueMap()[$issue_type] ?? null; + if (!$issue) { + self::emitIssue( + $code_base, + $context, + self::UnknownIssueType, + 'Unknown issue type {STRING_LITERAL} in a call to {METHOD}(). (may be a false positive - check if the version of Phan running PhanSelfCheckPlugin is the same version that the analyzed codebase is using)', + [$issue_type, $function->getFQSEN()] + ); + return null; + } + return $issue; + } + + private static function checkIssueTemplateUsage(CodeBase $code_base, Context $context, string $issue_message_template, int $issue_message_arg_count): void + { + $issue_message_format_string = Issue::templateToFormatString($issue_message_template); + $expected_arg_count = ConversionSpec::computeExpectedArgumentCount($issue_message_format_string); + if ($expected_arg_count === $issue_message_arg_count) { + return; + } + if ($issue_message_arg_count > $expected_arg_count) { + self::emitIssue( + $code_base, + $context, + self::TooManyArgumentsForIssue, + 'Too many arguments for issue {STRING_LITERAL}: expected {COUNT}, got {COUNT}', + [StringUtil::jsonEncode($issue_message_template), $expected_arg_count, $issue_message_arg_count], + Issue::SEVERITY_NORMAL + ); + } else { + self::emitIssue( + $code_base, + $context, + self::TooFewArgumentsForIssue, + 'Too few arguments for issue {STRING_LITERAL}: expected {COUNT}, got {COUNT}', + [StringUtil::jsonEncode($issue_message_template), $expected_arg_count, $issue_message_arg_count], + Issue::SEVERITY_CRITICAL + ); + } + } + + /** + * @param Node|mixed $arg + */ + private static function computeArraySize(CodeBase $code_base, Context $context, $arg): ?int + { + if ($arg === null) { + return 0; + } + $union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $arg); + if ($union_type->typeCount() !== 1) { + return null; + } + $types = $union_type->getTypeSet(); + $array_shape_type = \reset($types); + if (!$array_shape_type instanceof ArrayShapeType) { + return null; + } + $field_types = $array_shape_type->getFieldTypes(); + foreach ($field_types as $field_type) { + if ($field_type->isPossiblyUndefined()) { + return null; + } + } + return count($field_types); + } +} + +return new PhanSelfCheckPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PossiblyStaticMethodPlugin.php b/bundled-libs/phan/phan/.phan/plugins/PossiblyStaticMethodPlugin.php new file mode 100644 index 000000000..b433a5796 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PossiblyStaticMethodPlugin.php @@ -0,0 +1,302 @@ + a list of functions and methods where checks were postponed + */ + private $methods_for_postponed_analysis = []; + + /** + * @param CodeBase $code_base + * The code base in which the method exists + * + * @param FunctionInterface $method + * A function or method being analyzed + */ + private static function analyzePostponedMethod( + CodeBase $code_base, + FunctionInterface $method + ): void { + if ($method instanceof Method) { + if ($method->isOverride()) { + // This method can't be static unless its parent is also static. + return; + } + if ($method->isOverriddenByAnother()) { + // Changing this method causes a fatal error. + return; + } + } + + $stmts_list = self::getStatementListToAnalyze($method); + if ($stmts_list === null) { + // check for abstract methods, etc. + return; + } + if (self::nodeCanBeStatic($code_base, $method, $stmts_list)) { + if ($method instanceof Method) { + $visibility_upper = ucfirst($method->getVisibilityName()); + self::emitIssue( + $code_base, + $method->getContext(), + "PhanPluginPossiblyStatic${visibility_upper}Method", + "$visibility_upper method {METHOD} can be static", + [$method->getRepresentationForIssue()] + ); + } else { + self::emitIssue( + $code_base, + $method->getContext(), + "PhanPluginPossiblyStaticClosure", + "{FUNCTION} can be static", + [$method->getRepresentationForIssue()] + ); + } + } + } + + /** + * @param FunctionInterface $method + * @return ?Node - returns null if there's no statement list to analyze + */ + private static function getStatementListToAnalyze(FunctionInterface $method): ?Node + { + $node = $method->getNode(); + if (!$node) { + return null; + } + return $node->children['stmts']; + } + + /** + * @param CodeBase $code_base + * The code base in which the method exists + * + * @param Node|int|string|float|null $node + * @return bool - returns true if the node allows its method to be static + */ + private static function nodeCanBeStatic(CodeBase $code_base, FunctionInterface $method, $node): bool + { + if (!($node instanceof Node)) { + if (is_array($node)) { + foreach ($node as $child_node) { + if (!self::nodeCanBeStatic($code_base, $method, $child_node)) { + return false; + } + } + } + return true; + } + switch ($node->kind) { + case ast\AST_VAR: + if ($node->children['name'] === 'this') { + return false; + } + // Handle edge cases such as `${$this->varName}` + break; + case ast\AST_CLASS: + case ast\AST_FUNC_DECL: + return true; + case ast\AST_STATIC_CALL: + if (self::isSelfOrParentCallUsingObject($code_base, $method, $node)) { + return false; + } + // Check code such as `static::someMethod($this->prop)` + break; + case ast\AST_CLOSURE: + case ast\AST_ARROW_FUNC: + if ($node->flags & \ast\flags\MODIFIER_STATIC) { + return true; + } + break; + } + foreach ($node->children as $child_node) { + if (!self::nodeCanBeStatic($code_base, $method, $child_node)) { + return false; + } + } + return true; + } + + /** + * @param CodeBase $code_base + * The code base in which the calling instance method exists + * + * @param Node $node a node of kind ast\AST_STATIC_CALL + * (e.g. SELF::someMethod(), parent::someMethod(), SomeClass::staticMethod()) + * + * @return bool true if the AST_STATIC_CALL node is really calling an instance method + */ + private static function isSelfOrParentCallUsingObject(CodeBase $code_base, FunctionInterface $method, Node $node): bool + { + $class_node = $node->children['class']; + if (!($class_node instanceof Node && $class_node->kind === ast\AST_NAME)) { + return false; + } + $class_name = $class_node->children['name']; + if (!is_string($class_name)) { + return false; + } + if (!in_array(strtolower($class_name), ['self', 'parent'], true)) { + return false; + } + $method_name = $node->children['method']; + if (!is_string($method_name)) { + // This is uninferable + return true; + } + if (!$method instanceof AddressableElement) { + // should be impossible + return true; + } + try { + $method = (new ContextNode($code_base, new ElementContext($method), $node))->getMethod($method_name, true, false); + } catch (Exception $_) { + // This might be an instance method if we don't know what it is + return true; + } + return !$method->isStatic(); + } + + /** + * @param CodeBase $code_base @unused-param + * The code base in which the method exists + * + * @param Method $method + * A method being analyzed + * + * @override + */ + public function analyzeMethod( + CodeBase $code_base, + Method $method + ): void { + // 1. Perform any checks that can be done immediately to rule out being able + // to convert this to a static method + if ($method->isStatic()) { + // This is what we want. + return; + } + if ($method->isMagic()) { + // Magic methods can't be static. + return; + } + if ($method->getFQSEN() !== $method->getRealDefiningFQSEN()) { + // Only warn once for the original definition of this method. + // Don't warn about subclasses inheriting this method. + return; + } + $method_filter = Config::getValue('plugin_config')['possibly_static_method_ignore_regex'] ?? null; + if (is_string($method_filter)) { + $fqsen_string = ltrim((string)$method->getFQSEN(), '\\'); + if (preg_match($method_filter, $fqsen_string) > 0) { + return; + } + } + if (!$method->hasNode()) { + // There's no body to check - This is abstract or can't be checked + return; + } + $fqsen = $method->getFQSEN(); + + // 2. Defer remaining checks until we have all the necessary information + // (is this method overridden/an override, is parent::foo() referring to a static or an instance method, etc.) + $this->methods_for_postponed_analysis[(string) $fqsen] = $method; + } + + /** + * @param CodeBase $code_base @unused-param + * The code base in which the function exists + * + * @param Func $function + * A function being analyzed + * @override + */ + public function analyzeFunction( + CodeBase $code_base, + Func $function + ): void { + if (!$function->isClosure()) { + return; + } + if ($function->isStatic()) { + return; + } + if (!$function->hasNode()) { + // There's no body to check - This is abstract or can't be checked + return; + } + // NOTE: The possibly_static_method_ignore_regex isn't used because there's no way to apply it to closures + $fqsen = $function->getFQSEN(); + + // 2. Defer remaining checks until we have all the necessary information + // (is this method overridden/an override, is parent::foo() referring to a static or an instance method, etc.) + $this->methods_for_postponed_analysis[(string) $fqsen] = $function; + } + + /** + * @param CodeBase $code_base + * The code base being analyzed + * + * @override + */ + public function finalizeProcess(CodeBase $code_base): void + { + foreach ($this->methods_for_postponed_analysis as $method) { + self::analyzePostponedMethod($code_base, $method); + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new PossiblyStaticMethodPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PreferNamespaceUsePlugin.php b/bundled-libs/phan/phan/.phan/plugins/PreferNamespaceUsePlugin.php new file mode 100644 index 000000000..8e8d706e9 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PreferNamespaceUsePlugin.php @@ -0,0 +1,183 @@ +isMagic() || $method->isPHPInternal()) { + return; + } + if ($method->getFQSEN() !== $method->getDefiningFQSEN()) { + return; + } + self::analyzeFunctionLike($code_base, $method); + } + + private static function analyzeFunctionLike(CodeBase $code_base, FunctionInterface $method): void + { + $node = $method->getNode(); + if (!$node) { + return; + } + $return_type = $node->children['returnType']; + if ($return_type instanceof Node) { + self::analyzeFunctionLikeReturn($code_base, $method, $return_type); + } + foreach ($node->children['params']->children ?? [] as $param_node) { + if (!($param_node instanceof Node)) { + // impossible? + continue; + } + self::analyzeFunctionLikeParam($code_base, $method, $param_node); + } + } + + private static function analyzeFunctionLikeReturn(CodeBase $code_base, FunctionInterface $method, Node $return_type): void + { + $is_nullable = false; + if ($return_type->kind === ast\AST_NULLABLE_TYPE) { + $return_type = $return_type->children['type']; + if (!($return_type instanceof Node)) { + // should not happen + return; + } + $is_nullable = true; + } + $shorter_return_type = self::determineShorterType($method->getContext(), $return_type); + if (is_string($shorter_return_type)) { + $prefix = $is_nullable ? '?' : ''; + self::emitIssue( + $code_base, + $method->getContext(), + self::PreferNamespaceUseReturnType, + 'Could write return type of {FUNCTION} as {TYPE} instead of {TYPE}', + [$method->getName(), $prefix . $shorter_return_type, $prefix . '\\' . $return_type->children['name']] + ); + } + } + + private static function analyzeFunctionLikeParam(CodeBase $code_base, FunctionInterface $method, Node $param_node): void + { + $param_type = $param_node->children['type']; + if (!$param_type instanceof Node) { + return; + } + $is_nullable = false; + if ($param_type->kind === ast\AST_NULLABLE_TYPE) { + $param_type = $param_type->children['type']; + if (!($param_type instanceof Node)) { + // should not happen + return; + } + $is_nullable = true; + } + $shorter_param_type = self::determineShorterType($method->getContext(), $param_type); + if (is_string($shorter_param_type)) { + $param_name = $param_node->children['name']; + if (!is_string($param_name)) { + // should be impossible + return; + } + + $prefix = $is_nullable ? '?' : ''; + self::emitIssue( + $code_base, + $method->getContext(), + self::PreferNamespaceUseParamType, + 'Could write param type of ${PARAMETER} of {FUNCTION} as {TYPE} instead of {TYPE}', + [$param_name, $method->getName(), $prefix . $shorter_param_type, $prefix . '\\' . $param_type->children['name']] + ); + } + } + + /** + * Given a node with a parameter or return type, return a string with a shorter represented of the type (if possible), or return null if this is not possible. + * + * This does not try all possibilities, and only affects fully qualified types. + */ + private static function determineShorterType(Context $context, Node $type_node): ?string + { + if ($type_node->kind !== ast\AST_NAME) { + return null; + } + + if ($type_node->flags !== ast\flags\NAME_FQ) { + return null; + } + $name = $type_node->children['name']; + if (!is_string($name)) { + return null; + } + $parts = explode('\\', $name); + $name_end = (string)array_pop($parts); + $namespace = implode('\\', $parts); + + if ($context->hasNamespaceMapFor(ast\flags\USE_NORMAL, $name_end)) { + $fqsen = $context->getNamespaceMapFor(ast\flags\USE_NORMAL, $name_end); + if ($fqsen->getName() === $name_end && strcasecmp(ltrim($fqsen->getNamespace(), '\\'), $namespace) === 0) { + // found `use Bar\Something` when looking for `\Bar\Something`, so suggest `Something` + return $name_end; + } + // TODO: Could look for `use \Foo\Bar as FB;` + } elseif (strcasecmp($namespace, ltrim($context->getNamespace(), "\\")) === 0) { + // Foo\Bar\Baz in Foo\Bar is Baz unless there is another namespace use shadowing it. + return $name_end; + } + return null; + } + + /** + * @return array + */ + public function getAutomaticFixers(): array + { + require_once __DIR__ . '/PreferNamespaceUsePlugin/Fixers.php'; + return [ + self::PreferNamespaceUseReturnType => Closure::fromCallable([Fixers::class, 'fixReturnType']), + self::PreferNamespaceUseParamType => Closure::fromCallable([Fixers::class, 'fixParamType']), + //self::RedundantClosureComment => $function_like_fixer, + ]; + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new PreferNamespaceUsePlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PreferNamespaceUsePlugin/Fixers.php b/bundled-libs/phan/phan/.phan/plugins/PreferNamespaceUsePlugin/Fixers.php new file mode 100644 index 000000000..04f1a98a8 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PreferNamespaceUsePlugin/Fixers.php @@ -0,0 +1,149 @@ +getTemplateParameters(); + $shorter_return_type = \ltrim((string)$params[1], '?'); + $method_name = $params[0]; + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $method_name); + if (!$declaration) { + return null; + } + return self::computeEditsForReturnTypeDeclaration($declaration, $shorter_return_type); + } + + /** + * Generate an edit to replace a fully qualified param type with a shorter equivalent representation. + * @unused-param $code_base + */ + public static function fixParamType( + CodeBase $code_base, + FileCacheEntry $contents, + IssueInstance $instance + ): ?FileEditSet { + $params = $instance->getTemplateParameters(); + $shorter_return_type = \ltrim((string)$params[2], '?'); + $method_name = (string)$params[1]; + $param_name = (string)$params[0]; + $declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $method_name); + if (!$declaration) { + return null; + } + return self::computeEditsForParamTypeDeclaration($contents, $declaration, $param_name, $shorter_return_type); + } + + /** + * @suppress PhanThrowTypeAbsentForCall + */ + private static function computeEditsForReturnTypeDeclaration( + FunctionLike $declaration, + string $shorter_return_type + ): ?FileEditSet { + // @phan-suppress-next-line PhanUndeclaredProperty + $return_type_node = $declaration->returnType; + if (!$return_type_node instanceof PhpParser\Node) { + return null; + } + // Generate an edit to replace the long return type with the shorter return type + // Long return types are always Nodes instead of Tokens. + $file_edit = new FileEdit( + $return_type_node->getStart(), + $return_type_node->getEndPosition(), + $shorter_return_type + ); + return new FileEditSet([$file_edit]); + } + + private static function computeEditsForParamTypeDeclaration( + FileCacheEntry $contents, + FunctionLike $declaration, + string $param_name, + string $shorter_param_type + ): ?FileEditSet { + // @phan-suppress-next-line PhanUndeclaredProperty + $return_type_node = $declaration->returnType; + if (!$return_type_node) { + return null; + } + // @phan-suppress-next-line PhanUndeclaredProperty + $parameter_node_list = $declaration->parameters->children ?? []; + foreach ($parameter_node_list as $param) { + if (!$param instanceof PhpParser\Node\Parameter) { + continue; + } + $declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($param->variableName); + if ($declaration_name !== $param_name) { + continue; + } + $token = $param->typeDeclaration; + if (!$token) { + return null; + } + // @phan-suppress-next-line PhanThrowTypeAbsentForCall php-parser is not expected to throw here + $start = $token instanceof Token ? $token->start : $token->getStart(); + $file_edit = new FileEdit($start, $token->getEndPosition(), $shorter_param_type); + return new FileEditSet([$file_edit]); + } + return null; + } + + // TODO: Move this into a reusable function + private static function findFunctionLikeDeclaration( + FileCacheEntry $contents, + int $line, + string $name + ): ?FunctionLike { + $candidates = []; + foreach ($contents->getNodesAtLine($line) as $node) { + if ($node instanceof FunctionDeclaration || $node instanceof MethodDeclaration) { + $name_node = $node->name; + if (!$name_node) { + continue; + } + $declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($name_node); + if ($declaration_name === $name) { + $candidates[] = $node; + } + } elseif ($node instanceof AnonymousFunctionCreationExpression) { + if ($name === '{closure}') { + $candidates[] = $node; + } + } + } + if (\count($candidates) === 1) { + return $candidates[0]; + } + return null; + } +} diff --git a/bundled-libs/phan/phan/.phan/plugins/PregRegexCheckerPlugin.php b/bundled-libs/phan/phan/.phan/plugins/PregRegexCheckerPlugin.php new file mode 100644 index 000000000..24a6cff1f --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PregRegexCheckerPlugin.php @@ -0,0 +1,381 @@ + + */ + $err = with_disabled_phan_error_handler(static function () use ($pattern): ?array { + $old_error_reporting = error_reporting(); + \error_reporting(0); + \ob_start(); + \error_clear_last(); + try { + // Annoyingly, preg_match would not warn about the `/e` modifier, removed in php 7. + // Use `preg_replace` instead (The eval body is empty and phan requires 7.0+ to run) + $result = @\preg_replace($pattern, '', ''); + if (!\is_string($result)) { + return \error_get_last() ?? []; + } + return null; + } finally { + \ob_end_clean(); + \error_reporting($old_error_reporting); + } + }); + if ($err !== null) { + // TODO: scan for 'at offset %d$' and print the corresponding section of the regex. Note: Have to remove delimiters and unescape characters within the delimiters. + self::emitIssue( + $code_base, + $context, + 'PhanPluginInvalidPregRegex', + 'Call to {FUNCTION} was passed an invalid regex {STRING_LITERAL}: {DETAILS}', + [(string)$function->getFQSEN(), StringUtil::encodeValue($pattern), \preg_replace('@^preg_replace\(\): @', '', $err['message'] ?? 'unknown error')] + ); + return; + } + if (strpos($pattern, '$') !== false && (Config::getValue('plugin_config')['regex_warn_if_newline_allowed_at_end'] ?? false)) { + foreach (self::checkForSuspiciousRegexPatterns($pattern) as [$issue_type, $issue_template]) { + self::emitIssue( + $code_base, + $context, + $issue_type, + $issue_template, + [$function->getFQSEN(), StringUtil::encodeValue($pattern)] + ); + } + } + } + + /** + * @return Generator + */ + private static function checkForSuspiciousRegexPatterns(string $pattern): Generator + { + $pattern = \trim($pattern); + + $start_chr = $pattern[0] ?? '/'; + // @phan-suppress-next-line PhanParamSuspiciousOrder this is deliberate + $i = \strpos('({[', $start_chr); + if ($i !== false) { + $end_chr = ')}]'[$i]; + } else { + $end_chr = $start_chr; + } + // TODO: Reject characters that preg_match would reject + $end_pos = \strrpos($pattern, $end_chr); + if ($end_pos === false) { + return; + } + + $inner = (string)\substr($pattern, 1, $end_pos - 1); + if ($i !== false) { + // Unescape '/x\/y/' as 'x/y' + $inner = \str_replace('\\' . $start_chr, $start_chr, $inner); + } + foreach (self::tokenizeRegexParts($inner) as $part) { + // If special handling of newlines is given, don't warn. + // If PCRE_EXTENDED is given, this was likely a false positive (E.g. # can be a comment) + if ($part === '$' && !preg_match('/[mDx]/', (string) substr($pattern, $end_pos + 1))) { + yield ['PhanPluginPregRegexDollarAllowsNewline', 'Call to {FUNCTION} used \'$\' in {STRING_LITERAL}, which allows a newline character \'\n\' before the end of the string. Add D to qualifiers to forbid the newline, m to match any newline, or suppress this issue if this is deliberate']; + } + } + } + + /** + * Tokenize the regex, using imperfect heuristics to split up the parts of a regular expression. + */ + private static function tokenizeRegexParts(string $inner): Generator + { + $inner_len = strlen($inner); + for ($j = 0; $j < $inner_len;) { + switch ($c = $inner[$j]) { + case '\\': + // TODO: https://www.php.net/manual/en/regexp.reference.escape.php for alphanumeric characters + yield substr($inner, $j, $j + 2); + $j += 2; + break; + case '[': + // TODO: Handle escaped ]. This is a heuristic that is usually good enough. + $end = strpos($inner, ']', $j + 1); + if ($end === false) { + yield substr($inner, $j); + return; + } + yield substr($inner, $j, $end); + $j = $end; + break; + case '{': + $end = strpos($inner, '}', $j + 1); + if ($end === false) { + yield substr($inner, $j); + return; + } + yield substr($inner, $j, $end); + $j = $end; + break; + // case '(': + // case '}': + // case ')': + // case ']': + default: + yield $c; + $j++; + break; + } + } + } + + /** + * @param CodeBase $code_base + * @param Context $context + * @param Node|string|int|float $pattern + * @return array + */ + private static function extractStringsFromStringOrArray( + CodeBase $code_base, + Context $context, + $pattern + ): array { + if (\is_string($pattern)) { + return [$pattern => $pattern]; + } + $pattern_union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $pattern); + $result = []; + foreach ($pattern_union_type->getTypeSet() as $type) { + if ($type instanceof LiteralStringType) { + $value = $type->getValue(); + $result[$value] = $value; + } elseif ($type instanceof IterableType) { + $iterable_type = $type->iterableValueUnionType($code_base); + foreach ($iterable_type ? $iterable_type->getTypeSet() : [] as $element_type) { + if ($element_type instanceof LiteralStringType) { + $value = $element_type->getValue(); + $result[$value] = $value; + } + } + } + } + return $result; + } + + /** + * @param non-empty-list $patterns 1 or more regex patterns + * @return array the set of keys in the pattern + * @throws InvalidArgumentException if any regex could not be parsed by the heuristics + */ + private static function computePatternKeys(array $patterns): array + { + $result = []; + foreach ($patterns as $regex) { + $result += RegexKeyExtractor::getKeys($regex); + } + return $result; + } + + /** + * @return array references to indices in the pattern + */ + private static function extractTemplateKeys(string $template): array + { + $result = []; + // > replacement may contain references of the form \\n or $n, + // ... + // > n can be from 0 to 99, and \\0 or $0 refers to the text matched by the whole pattern. + preg_match_all('/[$\\\\]([0-9]{1,2}|[^0-9{]|(?<=\$)\{[0-9]{1,2}\})/', $template, $all_matches, PREG_SET_ORDER); + foreach ($all_matches as $match) { + $key = $match[1]; + if ($key[0] === '{') { + $key = (string)\substr($key, 1, -1); + } + if ($key[0] >= '0' && $key[0] <= '9') { + // Edge case: Convert '09' to 9 + $result[(int)$key] = $match[0]; + } + } + return $result; + } + + /** + * @param string[] $patterns 1 or more regex patterns + * @param Node|string|int|float $replacement_node + */ + private static function analyzeReplacementTemplate(CodeBase $code_base, Context $context, array $patterns, $replacement_node): void + { + $replacement_templates = self::extractStringsFromStringOrArray($code_base, $context, $replacement_node); + $pattern_keys = null; + + // https://secure.php.net/manual/en/function.preg-replace.php#refsect1-function.preg-replace-parameters + // > $replacement may contain references of the form \\n or $n, with the latter form being the preferred one. + try { + foreach ($replacement_templates as $replacement_template) { + $pattern_keys = $pattern_keys ?? self::computePatternKeys($patterns); + $regex_group_keys = self::extractTemplateKeys($replacement_template); + foreach ($regex_group_keys as $key => $reference_string) { + if (!isset($pattern_keys[$key])) { + usort($patterns, 'strcmp'); + self::emitIssue( + $code_base, + $context, + 'PhanPluginInvalidPregRegexReplacement', + 'Call to {FUNCTION} was passed an invalid replacement reference {STRING_LITERAL} to pattern {STRING_LITERAL}', + ['\preg_replace', StringUtil::encodeValue($reference_string), StringUtil::encodeValueList(' or ', $patterns)] + ); + } + } + } + } catch (InvalidArgumentException $_) { + // TODO: Is this warned about elsewhere? + return; + } + } + + /** + * @param CodeBase $code_base @phan-unused-param + * @return array + */ + public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array + { + /** + * @param list $args the nodes for the arguments to the invocation + */ + $preg_pattern_callback = static function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ): void { + if (count($args) < 1) { + return; + } + $pattern = $args[0]; + if ($pattern instanceof Node) { + $pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue(); + } + if (\is_string($pattern)) { + self::analyzePattern($code_base, $context, $function, $pattern); + } + }; + + /** + * @param list $args + */ + $preg_pattern_or_array_callback = static function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ): void { + if (count($args) < 1) { + return; + } + $pattern_node = $args[0]; + foreach (self::extractStringsFromStringOrArray($code_base, $context, $pattern_node) as $pattern) { + self::analyzePattern($code_base, $context, $function, $pattern); + } + }; + + /** + * @param list $args + */ + $preg_pattern_and_replacement_callback = static function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ): void { + if (count($args) < 1) { + return; + } + $pattern_node = $args[0]; + $patterns = self::extractStringsFromStringOrArray($code_base, $context, $pattern_node); + if (count($patterns) === 0) { + return; + } + foreach ($patterns as $pattern) { + self::analyzePattern($code_base, $context, $function, $pattern); + } + if (count($args) < 2) { + return; + } + self::analyzeReplacementTemplate($code_base, $context, $patterns, $args[1]); + }; + + /** + * @param list $args the nodes for the arguments to the invocation + */ + $preg_replace_callback_array_callback = static function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ): void { + if (count($args) < 1) { + return; + } + // TODO: Resolve global constants and class constants? + $pattern = $args[0]; + if ($pattern instanceof Node) { + $pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPValue(self::RESOLVE_REGEX_KEY_FLAGS); + } + if (\is_array($pattern)) { + foreach ($pattern as $child_pattern => $_) { + self::analyzePattern($code_base, $context, $function, (string)$child_pattern); + } + return; + } + }; + + // TODO: Check that the callbacks have the right signatures in another PR? + return [ + // call + 'preg_filter' => $preg_pattern_or_array_callback, + 'preg_grep' => $preg_pattern_callback, + 'preg_match' => $preg_pattern_callback, + 'preg_match_all' => $preg_pattern_callback, + 'preg_replace_callback_array' => $preg_replace_callback_array_callback, + 'preg_replace_callback' => $preg_pattern_or_array_callback, + 'preg_replace' => $preg_pattern_and_replacement_callback, + 'preg_split' => $preg_pattern_callback, + ]; + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new PregRegexCheckerPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/PrintfCheckerPlugin.php b/bundled-libs/phan/phan/.phan/plugins/PrintfCheckerPlugin.php new file mode 100644 index 000000000..556d186b6 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/PrintfCheckerPlugin.php @@ -0,0 +1,786 @@ + 'Bonjour'] for $fmt_str == 'Hello') + */ + protected static function gettextForAllLocales(string $fmt_str): array + { + return []; + } + + /** + * Convert an expression(a list of tokens) to a primitive. + * People who have custom such as methods or functions to fetch translations + * may subclass this plugin and override this method to add checks for AST_CALL (foo()), AST_METHOD_CALL(MyClass::getTranslation($id), etc.) + * + * @param CodeBase $code_base + * @param Context $context + * @param bool|int|string|float|Node|array|null $ast_node + */ + protected function astNodeToPrimitive(CodeBase $code_base, Context $context, $ast_node): ?PrimitiveValue + { + // Base case: convert primitive tokens such as numbers and strings. + if (!($ast_node instanceof Node)) { + return new PrimitiveValue($ast_node); + } + switch ($ast_node->kind) { + // TODO: Resolve class constant access when those are format strings. Same for PregRegexCheckerPlugin. + case \ast\AST_CALL: + $name_node = $ast_node->children['expr']; + if ($name_node instanceof Node && $name_node->kind === \ast\AST_NAME) { + // TODO: Use Phan's function resolution? + // TODO: ngettext? + $name = $name_node->children['name']; + if (!\is_string($name)) { + break; + } + if ($name === '_' || strcasecmp($name, 'gettext') === 0) { + $child_arg = $ast_node->children['args']->children[0] ?? null; + if ($child_arg === null) { + break; + } + $prim = self::astNodeToPrimitive($code_base, $context, $child_arg); + if ($prim === null) { + break; + } + return new PrimitiveValue($prim->value, true); + } + } + break; + case \ast\AST_BINARY_OP: + if ($ast_node->flags !== ast\flags\BINARY_CONCAT) { + break; + } + $left = $this->astNodeToPrimitive($code_base, $context, $ast_node->children['left']); + if ($left === null) { + break; + } + $right = $this->astNodeToPrimitive($code_base, $context, $ast_node->children['right']); + if ($right === null) { + break; + } + $result = self::concatenateToPrimitive($left, $right); + if ($result) { + return $result; + } + break; + } + $union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $ast_node); + $result = $union_type->asSingleScalarValueOrNullOrSelf(); + + if (!is_object($result)) { + return new PrimitiveValue($result); + } + $scalar_union_types = $union_type->asScalarValues(); + if (!$scalar_union_types) { + // We don't know how to convert this to a primitive, give up. + // (Subclasses may add their own logic first, then call self::astNodeToPrimitive) + return null; + } + $known_specs = null; + $first_str = null; + foreach ($union_type->getTypeSet() as $type) { + if (!$type instanceof LiteralStringType || $type->isNullable()) { + return null; + } + $str = $type->getValue(); + $new_specs = ConversionSpec::extractAll($str); + if (\is_array($known_specs)) { + if ($known_specs != $new_specs) { + // We have different specs, e.g. %s and %d, %1$s and %2$s, etc. + // TODO: Could allow differences in padding or alignment + return null; + } + } else { + $known_specs = $new_specs; + $first_str = $str; + } + } + return new PrimitiveValue($first_str); + } + + /** + * Convert a primitive and a sequence of tokens to a primitive formed by + * concatenating strings. + * + * @param PrimitiveValue $left the value on the left. + * @param PrimitiveValue $right the value on the right. + */ + protected static function concatenateToPrimitive(PrimitiveValue $left, PrimitiveValue $right): ?PrimitiveValue + { + // Combining untranslated strings with anything will cause problems. + if ($left->is_translated) { + return null; + } + if ($right->is_translated) { + return null; + } + $str = $left->value . $right->value; + return new PrimitiveValue($str); + } + + /** + * @unused-param $code_base + */ + public function getReturnTypeOverrides(CodeBase $code_base): array + { + $string_union_type = StringType::instance(false)->asPHPDocUnionType(); + /** + * @param list $args the nodes for the arguments to the invocation + */ + $sprintf_handler = static function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ) use ($string_union_type): UnionType { + if (count($args) < 1) { + return FalseType::instance(false)->asRealUnionType(); + } + $union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[0]); + $format_strings = []; + foreach ($union_type->getTypeSet() as $type) { + if (!$type instanceof LiteralStringType) { + return $string_union_type; + } + $format_strings[] = $type->getValue(); + } + if (count($format_strings) === 0) { + return $string_union_type; + } + $result_union_type = UnionType::empty(); + foreach ($format_strings as $format_string) { + $min_width = 0; + foreach (ConversionSpec::extractAll($format_string) as $spec_group) { + foreach ($spec_group as $spec) { + $min_width += ($spec->width ?: 0); + } + } + if (!LiteralStringType::canRepresentStringOfLength($min_width)) { + return $string_union_type; + } + $sprintf_args = []; + for ($i = 1; $i < count($args); $i++) { + $arg = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[$i])->asSingleScalarValueOrNullOrSelf(); + if (is_object($arg)) { + return $string_union_type; + } + $sprintf_args[] = $arg; + } + try { + $result = \with_disabled_phan_error_handler( + /** @return string|false */ + static function () use ($format_string, $sprintf_args) { + // @phan-suppress-next-line PhanPluginPrintfVariableFormatString + return @\vsprintf($format_string, $sprintf_args); + } + ); + } catch (Throwable $e) { + // PHP 8 throws ValueError for too few arguments to vsprintf + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeErrorInInternalCall, + $args[0]->lineno ?? $context->getLineNumberStart(), + $function->getName(), + $e->getMessage() + ); + // TODO: When PHP 8.0 stable is out, replace this with string? + $result = false; + } + $result_union_type = $result_union_type->withType(Type::fromObject($result)); + } + return $result_union_type; + }; + return [ + 'sprintf' => $sprintf_handler, + ]; + } + + /** + * @param CodeBase $code_base @phan-unused-param + * @return \Closure[] + */ + public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array + { + /** + * Analyzes a printf-like function with a format directive in the first position. + * @param list $args the nodes for the arguments to the invocation + */ + $printf_callback = function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ): void { + // TODO: Resolve global constants and class constants? + // TODO: Check for AST_UNPACK + $pattern = $args[0] ?? null; + if ($pattern === null) { + return; + } + if ($pattern instanceof Node) { + $pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue(); + } + $remaining_args = \array_slice($args, 1); + $this->analyzePrintfPattern($code_base, $context, $function, $pattern, $remaining_args); + }; + /** + * Analyzes a printf-like function with a format directive in the first position. + * @param list $args the nodes for the arguments to the invocation + */ + $fprintf_callback = function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ): void { + if (\count($args) < 2) { + return; + } + // TODO: Resolve global constants and class constants? + // TODO: Check for AST_UNPACK + $pattern = $args[1]; + if ($pattern instanceof Node) { + $pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue(); + } + $remaining_args = \array_slice($args, 2); + $this->analyzePrintfPattern($code_base, $context, $function, $pattern, $remaining_args); + }; + /** + * Analyzes a printf-like function with a format directive in the first position. + * @param list $args + */ + $vprintf_callback = function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ): void { + if (\count($args) < 2) { + return; + } + // TODO: Resolve global constants and class constants? + // TODO: Check for AST_UNPACK + $pattern = $args[0]; + if ($pattern instanceof Node) { + $pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue(); + } + $format_args_node = $args[1]; + $format_args = (new ContextNode($code_base, $context, $format_args_node))->getEquivalentPHPValue(); + $this->analyzePrintfPattern($code_base, $context, $function, $pattern, \is_array($format_args) ? $format_args : null); + }; + /** + * Analyzes a printf-like function with a format directive in the first position. + * @param list $args the nodes for the arguments to the invocation + */ + $vfprintf_callback = function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ): void { + if (\count($args) < 3) { + return; + } + // TODO: Resolve global constants and class constants? + // TODO: Check for AST_UNPACK + $pattern = $args[1]; + if ($pattern instanceof Node) { + $pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue(); + } + $format_args_node = $args[2]; + $format_args = (new ContextNode($code_base, $context, $format_args_node))->getEquivalentPHPValue(); + $this->analyzePrintfPattern($code_base, $context, $function, $pattern, \is_array($format_args) ? $format_args : null); + }; + return [ + // call + 'printf' => $printf_callback, + 'sprintf' => $printf_callback, + 'fprintf' => $fprintf_callback, + 'vprintf' => $vprintf_callback, + 'vsprintf' => $vprintf_callback, + 'vfprintf' => $vfprintf_callback, + ]; + } + + protected static function encodeString(string $str): string + { + $result = \json_encode($str, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); + if ($result !== false) { + return $result; + } + return var_export($str, true); + } + + /** + * Analyzes a printf pattern, emitting issues if necessary + * @param CodeBase $code_base + * @param Context $context + * @param FunctionInterface $function + * @param Node|array|string|float|int|bool|resource|null $pattern_node + * @param ?(Node|string|int|float)[] $arg_nodes arguments following the format string. Null if the arguments could not be determined. + * @suppress PhanPartialTypeMismatchArgument TODO: refactor into smaller functions + */ + protected function analyzePrintfPattern(CodeBase $code_base, Context $context, FunctionInterface $function, $pattern_node, $arg_nodes): void + { + // Given a node, extract the printf directive and whether or not it could be translated + $primitive_for_fmtstr = $this->astNodeToPrimitive($code_base, $context, $pattern_node); + /** + * @param string $issue_type + * A name for the type of issue such as 'PhanPluginMyIssue' + * + * @param string $issue_message_format + * The complete issue message format string to emit such as + * 'class with fqsen {CLASS} is broken in some fashion' (preferred) + * or 'class with fqsen %s is broken in some fashion' + * The list of placeholders for between braces can be found + * in \Phan\Issue::uncolored_format_string_for_template. + * + * @param list $issue_message_args + * The arguments for this issue format. + * If this array is empty, $issue_message_args is kept in place + * + * @param int $severity + * A value from the set {Issue::SEVERITY_LOW, + * Issue::SEVERITY_NORMAL, Issue::SEVERITY_HIGH}. + * + * @param int $issue_type_id An issue id for pylint + */ + $emit_issue = static function (string $issue_type, string $issue_message_format, array $issue_message_args, int $severity, int $issue_type_id) use ($code_base, $context): void { + self::emitIssue( + $code_base, + $context, + $issue_type, + $issue_message_format, + $issue_message_args, + $severity, + Issue::REMEDIATION_B, + $issue_type_id + ); + }; + if ($primitive_for_fmtstr === null) { + $emit_issue( + 'PhanPluginPrintfVariableFormatString', + 'Code {CODE} has a dynamic format string that could not be inferred by Phan', + [ASTReverter::toShortString($pattern_node)], + Issue::SEVERITY_LOW, + self::ERR_UNTRANSLATED_UNKNOWN_FORMAT_STRING + ); + if (\is_array($arg_nodes) && count($arg_nodes) === 0) { + $replacement_function_name = \in_array($function->getName(), ['vprintf', 'fprintf', 'vfprintf'], true) ? 'fwrite' : 'echo'; + $emit_issue( + "PhanPluginPrintfNoArguments", + "No format string arguments are given for {STRING_LITERAL}, consider using {FUNCTION} instead", + ['(unknown)', $replacement_function_name], + Issue::SEVERITY_LOW, + self::ERR_UNTRANSLATED_USE_ECHO + ); + return; + } + // TODO: Add a verbose option + return; + } + // Make sure that the untranslated format string is being used correctly. + // If the format string will be translated, also check the translations. + + $fmt_str = $primitive_for_fmtstr->value; + $is_translated = $primitive_for_fmtstr->is_translated; + $specs = is_string($fmt_str) ? ConversionSpec::extractAll($fmt_str) : []; + $fmt_str = (string)$fmt_str; + + // Check for extra or missing arguments + if (\is_array($arg_nodes) && \count($arg_nodes) === 0) { + if (count($specs) > 0) { + $largest_positional = \max(\array_keys($specs)); + $examples = []; + foreach ($specs[$largest_positional] as $example_spec) { + $examples[] = self::encodeString($example_spec->directive); + } + // emit issues with 1-based offsets + $emit_issue( + 'PhanPluginPrintfNonexistentArgument', + 'Format string {STRING_LITERAL} refers to nonexistent argument #{INDEX} in {STRING_LITERAL}. This will be an ArgumentCountError in PHP 8.', + [self::encodeString($fmt_str), $largest_positional, \implode(',', $examples)], + Issue::SEVERITY_CRITICAL, + self::ERR_UNTRANSLATED_NONEXISTENT + ); + } + $replacement_function_name = \in_array($function->getName(), ['vprintf', 'fprintf', 'vfprintf'], true) ? 'fwrite' : 'echo'; + $emit_issue( + "PhanPluginPrintfNoArguments", + "No format string arguments are given for {STRING_LITERAL}, consider using {FUNCTION} instead", + [self::encodeString($fmt_str), $replacement_function_name], + Issue::SEVERITY_LOW, + self::ERR_UNTRANSLATED_USE_ECHO + ); + return; + } + if (count($specs) === 0) { + $emit_issue( + 'PhanPluginPrintfNoSpecifiers', + 'None of the formatting arguments passed alongside format string {STRING_LITERAL} are used', + [self::encodeString($fmt_str)], + Issue::SEVERITY_LOW, + self::ERR_UNTRANSLATED_NONE_USED + ); + return; + } + + if (\is_array($arg_nodes)) { + $largest_positional = \max(\array_keys($specs)); + if ($largest_positional > \count($arg_nodes)) { + $examples = []; + foreach ($specs[$largest_positional] as $example_spec) { + $examples[] = self::encodeString($example_spec->directive); + } + // emit issues with 1-based offsets + $emit_issue( + 'PhanPluginPrintfNonexistentArgument', + 'Format string {STRING_LITERAL} refers to nonexistent argument #{INDEX} in {STRING_LITERAL}. This will be an ArgumentCountError in PHP 8.', + [self::encodeString($fmt_str), $largest_positional, \implode(',', $examples)], + Issue::SEVERITY_CRITICAL, + self::ERR_UNTRANSLATED_NONEXISTENT + ); + } elseif ($largest_positional < count($arg_nodes)) { + $emit_issue( + 'PhanPluginPrintfUnusedArgument', + 'Format string {STRING_LITERAL} does not use provided argument #{INDEX}', + [self::encodeString($fmt_str), $largest_positional + 1], + Issue::SEVERITY_NORMAL, + self::ERR_UNTRANSLATED_UNUSED + ); + } + } + + /** @var string[][] maps argument position to a list of possible canonical strings (e.g. '%1$d') for that argument */ + $types_of_arg = []; + + // Check format string alone for common signs of problems. + // E.g. "% s", "%1$d %1$s" + foreach ($specs as $i => $spec_group) { + $types = []; + foreach ($spec_group as $spec) { + $canonical = $spec->toCanonicalString(); + $types[$canonical] = true; + if ((\strlen($spec->padding_char) > 0 || \strlen($spec->alignment)) && ($spec->width === '' || !$spec->position)) { + // Warn about "100% dollars" but not about "100%1$ 2dollars" (If both position and width were parsed, assume the padding was intentional) + $emit_issue( + 'PhanPluginPrintfNotPercent', + // phpcs:ignore Generic.Files.LineLength.MaxExceeded + "Format string {STRING_LITERAL} contains something that is not a percent sign, it will be treated as a format string '{STRING_LITERAL}' with padding of \"{STRING_LITERAL}\" and alignment of '{STRING_LITERAL}' but no width. Use {DETAILS} for a literal percent sign, or '{STRING_LITERAL}' to be less ambiguous", + [self::encodeString($fmt_str), $spec->directive, $spec->padding_char, $spec->alignment, '%%', $canonical], + Issue::SEVERITY_NORMAL, + self::ERR_UNTRANSLATED_NOT_PERCENT + ); + } + if ($is_translated && $spec->width && + ($spec->padding_char === '' || $spec->padding_char === ' ') + ) { + $intended_string = $spec->toCanonicalStringWithWidthAsPosition(); + $emit_issue( + 'PhanPluginPrintfWidthNotPosition', + "Format string {STRING_LITERAL} is specifying a width({STRING_LITERAL}) instead of a position({STRING_LITERAL})", + [self::encodeString($fmt_str), self::encodeString($canonical), self::encodeString($intended_string)], + Issue::SEVERITY_NORMAL, + self::ERR_UNTRANSLATED_WIDTH_INSTEAD_OF_POSITION + ); + } + } + + $types_of_arg[$i] = $types; + if (count($types) > 1) { + // May be an off by one error in the format string. + $emit_issue( + 'PhanPluginPrintfIncompatibleSpecifier', + 'Format string {STRING_LITERAL} refers to argument #{INDEX} in different ways: {DETAILS}', + [self::encodeString($fmt_str), $i, implode(',', \array_keys($types))], + Issue::SEVERITY_LOW, + self::ERR_UNTRANSLATED_INCOMPATIBLE_SPECIFIER + ); + } + } + + if (\is_array($arg_nodes)) { + foreach ($specs as $i => $spec_group) { + // $arg_nodes is a 0-based array, $spec_group is 1-based. + $arg_node = $arg_nodes[$i - 1] ?? null; + if (!isset($arg_node)) { + continue; + } + $actual_union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $arg_node); + if ($actual_union_type->isEmpty()) { + // Nothing to check. + continue; + } + + $expected_set = []; + foreach ($spec_group as $spec) { + $type_name = $spec->getExpectedUnionTypeName(); + $expected_set[$type_name] = true; + } + $expected_union_type = UnionType::empty(); + foreach ($expected_set as $type_name => $_) { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall getExpectedUnionTypeName should only return valid union types + $expected_union_type = $expected_union_type->withType(Type::fromFullyQualifiedString($type_name)); + } + if ($actual_union_type->canCastToUnionType($expected_union_type)) { + continue; + } + if (isset($expected_set['string'])) { + $can_cast_to_string = false; + // Allow passing objects with __toString() to printf whether or not strict types are used in the caller. + // TODO: Move into a common helper method? + try { + foreach ($actual_union_type->asExpandedTypes($code_base)->asClassList($code_base, $context) as $clazz) { + if ($clazz->hasMethodWithName($code_base, '__toString', true)) { + $can_cast_to_string = true; + break; + } + } + } catch (CodeBaseException $_) { + // Swallow "Cannot find class", go on to emit issue. + } + if ($can_cast_to_string) { + continue; + } + } + + $expected_union_type_string = (string)$expected_union_type; + if (self::canWeakCast($actual_union_type, $expected_set)) { + // This can be resolved by casting the arg to (string) manually in printf. + $emit_issue( + 'PhanPluginPrintfIncompatibleArgumentTypeWeak', + // phpcs:ignore Generic.Files.LineLength.MaxExceeded + 'Format string {STRING_LITERAL} refers to argument #{INDEX} as {DETAILS}, so type {TYPE} is expected. However, {FUNCTION} was passed the type {TYPE} (which is weaker than {TYPE})', + [ + self::encodeString($fmt_str), + $i, + self::getSpecStringsRepresentation($spec_group), + $expected_union_type_string, + $function->getName(), + (string)$actual_union_type, + $expected_union_type_string, + ], + Issue::SEVERITY_LOW, + self::ERR_UNTRANSLATED_INCOMPATIBLE_ARGUMENT_WEAK + ); + } else { + // This can be resolved by casting the arg to (int) manually in printf. + $emit_issue( + 'PhanPluginPrintfIncompatibleArgumentType', + 'Format string {STRING_LITERAL} refers to argument #{INDEX} as {DETAILS}, so type {TYPE} is expected, but {FUNCTION} was passed incompatible type {TYPE}', + [ + self::encodeString($fmt_str), + $i, + self::getSpecStringsRepresentation($spec_group), + $expected_union_type_string, + $function->getName(), + (string)$actual_union_type, + ], + Issue::SEVERITY_LOW, + self::ERR_UNTRANSLATED_INCOMPATIBLE_ARGUMENT + ); + } + } + } + + // Make sure the translations are compatible with this format string. + // In order to take advantage of the ability to analyze translations, override gettextForAllLocales + if ($is_translated) { + $this->validateTranslations($code_base, $context, $fmt_str, $types_of_arg); + } + } + + /** + * @param ConversionSpec[] $specs + */ + private static function getSpecStringsRepresentation(array $specs): string + { + return \implode(',', \array_unique(\array_map(static function (ConversionSpec $spec): string { + return $spec->directive; + }, $specs))); + } + + /** + * @param array $expected_set the types being checked for the ability to weakly cast to + */ + private static function canWeakCast(UnionType $actual_union_type, array $expected_set): bool + { + if (isset($expected_set['string'])) { + static $string_weak_types; + if ($string_weak_types === null) { + $string_weak_types = UnionType::fromFullyQualifiedPHPDocString('int|string|float'); + } + return $actual_union_type->canCastToUnionType($string_weak_types); + } + // We already allow int->float conversion + return false; + } + + /** + * TODO: Finish testing this. + * + * By default, this is a no-op, unless gettextForAllLocales is overridden in a subclass + * + * Check that the translations of the format string $fmt_str + * are compatible with the untranslated format string. + * + * In virtually all cases, the conversions specifiers should be + * identical to the conversion specifier (apart from whether or not + * position is explicitly stated) + * + * Emits issues. + * + * @param CodeBase $code_base + * @param Context $context + * @param string $fmt_str + * @param ConversionSpec[][] $types_of_arg contains array of ConversionSpec for + * each position in the untranslated format string. + */ + protected static function validateTranslations(CodeBase $code_base, Context $context, string $fmt_str, array $types_of_arg): void + { + $translations = static::gettextForAllLocales($fmt_str); + foreach ($translations as $locale => $translated_fmt_str) { + // Skip untranslated or equal strings. + if ($translated_fmt_str === $fmt_str) { + continue; + } + // Compare the translated specs for a given position to the existing spec. + $translated_specs = ConversionSpec::extractAll($translated_fmt_str); + foreach ($translated_specs as $i => $spec_group) { + $expected = $types_of_arg[$i] ?? []; + foreach ($spec_group as $spec) { + $canonical = $spec->toCanonicalString(); + if (!isset($expected[$canonical])) { + $expected_types = $expected ? implode(',', \array_keys($expected)) + : 'unused'; + + if ($expected_types !== 'unused') { + $severity = Issue::SEVERITY_NORMAL; + $issue_type_id = self::ERR_TRANSLATED_INCOMPATIBLE; + $issue_type = 'PhanPluginPrintfTranslatedIncompatible'; + } else { + $severity = Issue::SEVERITY_NORMAL; + $issue_type_id = self::ERR_TRANSLATED_HAS_MORE_ARGS; + $issue_type = 'PhanPluginPrintfTranslatedHasMoreArgs'; + } + self::emitIssue( + $code_base, + $context, + $issue_type, + // phpcs:ignore Generic.Files.LineLength.MaxExceeded + 'Translated string {STRING_LITERAL} has local {DETAILS} which refers to argument #{INDEX} as {STRING_LITERAL}, but the original format string treats it as {DETAILS} (ORIGINAL: {STRING_LITERAL}, TRANSLATION: {STRING_LITERAL})', + [ + self::encodeString($fmt_str), + $locale, + $i, + $canonical, + $expected_types, + self::encodeString($fmt_str), + self::encodeString($translated_fmt_str), + ], + $severity, + Issue::REMEDIATION_B, + $issue_type_id + ); + } + } + } + } + } +} + +/** + * Represents the information we have about the result of evaluating an expression. + * Currently, used only for printf arguments. + */ +class PrimitiveValue +{ + /** @var array|int|string|float|bool|null The primitive value of the expression if it could be determined. */ + public $value; + /** @var bool Whether or not the expression value was translated. */ + public $is_translated; + + /** + * @param array|int|string|float|bool|null $value + */ + public function __construct($value, bool $is_translated = false) + { + $this->value = $value; + $this->is_translated = $is_translated; + } +} + +return new PrintfCheckerPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/README.md b/bundled-libs/phan/phan/.phan/plugins/README.md new file mode 100644 index 000000000..95b991497 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/README.md @@ -0,0 +1,629 @@ +Plugins +======= + +The plugins in this folder can be used to add additional capabilities to phan. +Add their relative path (.phan/plugins/...) to the `plugins` entry of .phan/config.php. + +Plugin Documentation +-------------------- + +[Wiki Article: Writing Plugins For Phan](https://github.com/phan/phan/wiki/Writing-Plugins-for-Phan) + +Plugin List +----------- + +This section contains short descriptions of plugin files, and lists the issue types which they emit. + +They are grouped into the following sections: + +1. Plugins Affecting Phan Analysis +2. General-Use Plugins +3. Plugins Specific to Code Styles +4. Demo Plugins (Plugin authors should base new plugins off of these, if they don't see a similar plugin) + +### 1. Plugins Affecting Phan Analysis + +(More plugins will be added later, e.g. if they add new methods, add types to Phan's analysis of a return type, etc) + +#### UnusedSuppressionPlugin.php + +Warns if an `@suppress` annotation is no longer needed to suppress issue types on a function, method, closure, or class. +(Suppressions may stop being needed if Phan's analysis improves/changes in a release, +or if the relevant parts of the codebase fixed the bug/added annotations) +**This must be run with exactly one worker process** + +- **UnusedSuppression**: `Element {FUNCTIONLIKE} suppresses issue {ISSUETYPE} but does not use it` +- **UnusedPluginSuppression**: `Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} on this line but this suppression is unused or suppressed elsewhere` +- **UnusedPluginFileSuppression**: `Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} in this file but this suppression is unused or suppressed elsewhere` + +The following settings can be used in `.phan/config.php`: + - `'plugin_config' => ['unused_suppression_ignore_list' => ['FlakyPluginIssueName']]` will make this plugin avoid emitting `Unused*Suppression` for a list of issue names. + - `'plugin_config' => ['unused_suppression_whitelisted_only' => true]` will make this plugin report unused suppressions only for issues in `whitelist_issue_types`. + +#### FFIAnalysisPlugin.php + +This is only necessary if you are using [PHP 7.4's FFI (Foreign Function Interface) support](https://wiki.php.net/rfc/ffi) + +This makes Phan infer that assignments to variables that originally contained CData will continue to be CData. + +### 2. General-Use Plugins + +These plugins are useful across a wide variety of code styles, and should give low false positives. +Also see [DollarDollarPlugin.php](#dollardollarpluginphp) for a meaningful real-world example. + +#### AlwaysReturnPlugin.php + +Checks if a function or method with a non-void return type will **unconditionally** return or throw. +This is stricter than Phan's default checks (Phan accepts a function or method that **may** return something, or functions that unconditionally throw). + +#### DuplicateArrayKeyPlugin.php + +Warns about common errors in php array keys and switch statements. Has the following checks (This is able to resolve global and class constants to their scalar values). + +- **PhanPluginDuplicateArrayKey**: a duplicate or equivalent array key literal. + + (E.g `[2 => "value", "other" => "s", "2" => "value2"]` duplicates the key `2`) +- **PhanPluginDuplicateArrayKeyExpression**: `Duplicate/Equivalent dynamic array key expression ({CODE}) detected in array - the earlier entry will be ignored if the expression had the same value.` + (E.g. `[$x => 'value', $y => "s", $y => "value2"]`) +- **PhanPluginDuplicateSwitchCase**: a duplicate or equivalent case statement. + + (E.g `switch ($x) { case 2: echo "A\n"; break; case 2: echo "B\n"; break;}` duplicates the key `2`. The later case statements are ignored.) +- **PhanPluginDuplicateSwitchCaseLooseEquality**: a case statement that is loosely equivalent to an earlier case statement. + + (E.g `switch ('foo') { case 0: echo "0\n"; break; case 'foo': echo "foo\n"; break;}` has `0 == 'foo'`, and echoes `0` because of that) +- **PhanPluginMixedKeyNoKey**: mixing array entries of the form [key => value,] with entries of the form [value,]. + + (E.g. `['key' => 'value', 'othervalue']` is often found in code because the key for `'othervalue'` was forgotten) + +#### PregRegexCheckerPlugin + +This plugin checks for invalid regexes. +This plugin is able to resolve literals, global constants, and class constants as regexes. + +- **PhanPluginInvalidPregRegex**: The provided regex is invalid, according to PHP. +- **PhanPluginInvalidPregRegexReplacement**: The replacement string template of `preg_replace` refers to a match group that doesn't exist. (e.g. `preg_replace('/x(a)/', 'y$2', $strVal)`) +- **PhanPluginRegexDollarAllowsNewline**: `Call to {FUNCTION} used \'$\' in {STRING_LITERAL}, which allows a newline character \'\n\' before the end of the string. Add D to qualifiers to forbid the newline, m to match any newline, or suppress this issue if this is deliberate` + (This issue type is specific to coding style, and only checked for when configuration includes `['plugin_config' => ['regex_warn_if_newline_allowed_at_end' => true]]`) + +#### PrintfCheckerPlugin + +Checks for invalid format strings, incorrect argument counts, and unused arguments in printf calls. +Additionally, warns about incompatible union types (E.g. passing `string` for the argument corresponding to `%d`) +This plugin is able to resolve literals, global constants, and class constants as format strings. + + +- **PhanPluginPrintfNonexistentArgument**: `Format string {STRING_LITERAL} refers to nonexistent argument #{INDEX} in {STRING_LITERAL}` +- **PhanPluginPrintfNoArguments**: `No format string arguments are given for {STRING_LITERAL}, consider using {FUNCTION} instead` +- **PhanPluginPrintfNoSpecifiers**: `None of the formatting arguments passed alongside format string {STRING_LITERAL} are used` +- **PhanPluginPrintfUnusedArgument**: `Format string {STRING_LITERAL} does not use provided argument #{INDEX}` +- **PhanPluginPrintfNotPercent**: `Format string {STRING_LITERAL} contains something that is not a percent sign, it will be treated as a format string '{STRING_LITERAL}' with padding. Use %% for a literal percent sign, or '{STRING_LITERAL}' to be less ambiguous` + (Usually a typo, e.g. `printf("%s is 20% done", $taskName)` treats `% d` as a second argument) +- **PhanPluginPrintfWidthNotPosition**: `Format string {STRING_LITERAL} is specifying a width({STRING_LITERAL}) instead of a position({STRING_LITERAL})` +- **PhanPluginPrintfIncompatibleSpecifier**: `Format string {STRING_LITERAL} refers to argument #{INDEX} in different ways: {DETAILS}` (e.g. `"%1$s of #%1$d"`. May be an off by one error.) +- **PhanPluginPrintfIncompatibleArgumentTypeWeak**: `Format string {STRING_LITERAL} refers to argument #{INDEX} as {DETAILS}, so type {TYPE} is expected. However, {FUNCTION} was passed the type {TYPE} (which is weaker than {TYPE})` +- **PhanPluginPrintfIncompatibleArgumentType**: `Format string {STRING_LITERAL} refers to argument #{INDEX} as {DETAILS}, so type {TYPE} is expected, but {FUNCTION} was passed incompatible type {TYPE}` +- **PhanPluginPrintfVariableFormatString**: `Code {CODE} has a dynamic format string that could not be inferred by Phan` + +Note (for projects using `gettext`): +Subclassing this plugin (and overriding `gettextForAllLocales`) will allow you to analyze translations of a project for compatibility. +This will require extra work to set up. +See [PrintfCheckerPlugin's source](./PrintfCheckerPlugin.php) for details. + +#### UnreachableCodePlugin.php + +Checks for syntactically unreachable statements in the global scope or function bodies. +(E.g. function calls after unconditional `continue`/`break`/`throw`/`return`/`exit()` statements) + +- **PhanPluginUnreachableCode**: `Unreachable statement detected` + +#### Unused variable detection + +This is now built into Phan itself, and can be enabled via `--unused-variable-detection`. + +#### InvokePHPNativeSyntaxCheckPlugin.php + +This invokes `php --no-php-ini --syntax-check $analyzed_file_path` for you. (See +This is useful for cases Phan doesn't cover (e.g. [Issue #449](https://github.com/phan/phan/issues/449) or [Issue #277](https://github.com/phan/phan/issues/277)). + +Note: This may double the time Phan takes to analyze a project. This plugin can be safely used along with `--processes N`. + +This does not run on files that are parsed but not analyzed. + +Configuration settings can be added to `.phan/config.php`: + +```php + 'plugin_config' => [ + // A list of 1 or more PHP binaries (Absolute path or program name found in $PATH) + // to use to analyze your files with PHP's native `--syntax-check`. + // + // This can be used to simultaneously run PHP's syntax checks with multiple PHP versions. + // e.g. `'plugin_config' => ['php_native_syntax_check_binaries' => ['php72', 'php70', 'php56']]` + // if all of those programs can be found in $PATH + + // 'php_native_syntax_check_binaries' => [PHP_BINARY], + + // The maximum number of `php --syntax-check` processes to run at any point in time + // (Minimum: 1. Default: 1). + // This may be temporarily higher if php_native_syntax_check_binaries + // has more elements than this process count. + 'php_native_syntax_check_max_processes' => 4, + ], +``` + +If you wish to make sure that analyzed files would be accepted by those PHP versions +(Requires that php72, php70, and php56 be locatable with the `$PATH` environment variable) + +As of Phan 2.7.2, it is also possible to locally configure the PHP binary (or binaries) to run syntax checks with. +e.g. `phan --native-syntax-check php --native-syntax-check /usr/bin/php7.4` would run checks both with `php` (resolved with `$PATH`) +and the absolute path `/usr/bin/php7.4`. (see `phan --extended-help`) + +#### UseReturnValuePlugin.php + +This plugin warns when code fails to use the return value of internal functions/methods such as `sprintf` or `array_merge` or `Exception->getCode()`. +(functions/methods where the return value should almost always be used) + +- **PhanPluginUseReturnValueInternalKnown**: `Expected to use the return value of the internal function/method {FUNCTION}`, + +`'plugin_config' => ['infer_pure_method' => true]` will make this plugin automatically infer which methods are pure, recursively. +This is a best-effort heuristic. +This is done only for the functions and methods that are not excluded from analysis, +and it isn't done for methods that override or are overridden by other methods. + +Note that functions such as `fopen()` are not pure due to side effects. +UseReturnValuePlugin also warns about those because their results should be used. + +* This setting is ignored in the language server or daemon mode, + due to being extremely slow and memory intensive. + +Automatic inference of function purity is done recursively. + +This plugin also has a dynamic mode(disabled by default and slow) where it will warn if a function or method's return value is unused. +This checks if the function/method's return value is used 98% or more of the time, then warns about the remaining places where the return value was unused. +Note that this prevents the hardcoded checks from working. + +- **PhanPluginUseReturnValue**: `Expected to use the return value of the user-defined function/method {FUNCTION} - {SCALAR}%% of calls use it in the rest of the codebase`, +- **PhanPluginUseReturnValueInternal**: `Expected to use the return value of the internal function/method {FUNCTION} - {SCALAR}%% of calls use it in the rest of the codebase`, +- **PhanPluginUseReturnValueGenerator**: `Expected to use the return value of the function/method {FUNCTION} returning a generator of type {TYPE}`, + +See [UseReturnValuePlugin.php](./UseReturnValuePlugin.php) for configuration options. + +#### PHPUnitAssertionPlugin.php + +This plugin will make Phan infer side effects from calls to some of the helper methods that PHPUnit provides in test cases. + +- Infer that a condition is truthy from `assertTrue()` and `assertNotFalse()` (e.g. `assertTrue($x instanceof MyClass)`) +- Infer that a condition is null/not null from `assertNull()` and `assertNotNull()` +- Infer class types from `assertInstanceOf(MyClass::class, $actual)` +- Infer types from `assertInternalType($expected, $actual)` +- Infer that $actual has the exact type of $expected after calling `assertSame($expected, $actual)` +- Other methods aren't supported yet. + +#### EmptyStatementListPlugin.php + +This file checks for empty statement lists in loops/branches. +Due to Phan's AST rewriting for easier analysis, this may miss some edge cases for if/elseif. + +By default, this plugin won't warn if it can find a TODO/FIXME/"Deliberately empty" comment around the empty statement list (case insensitive). +(This may miss some TODOs due to `php-ast` not providing the end line numbers) +The setting `'plugin_config' => ['empty_statement_list_ignore_todos' => true]` can be used to make it unconditionally warn about empty statement lists. + +- **PhanPluginEmptyStatementDoWhileLoop** `Empty statement list statement detected for the do-while loop` +- **PhanPluginEmptyStatementForLoop** `Empty statement list statement detected for the for loop` +- **PhanPluginEmptyStatementForeachLoop** `Empty statement list statement detected for the foreach loop` +- **PhanPluginEmptyStatementIf**: `Empty statement list statement detected for the last if/elseif statement` +- **PhanPluginEmptyStatementSwitch** `No side effects seen for any cases of this switch statement` +- **PhanPluginEmptyStatementTryBody** `Empty statement list statement detected for the try statement's body` +- **PhanPluginEmptyStatementTryFinally** `Empty statement list statement detected for the try's finally body` +- **PhanPluginEmptyStatementWhileLoop** `Empty statement list statement detected for the while loop` + +### LoopVariableReusePlugin.php + +This plugin detects reuse of loop variables. + +- **PhanPluginLoopVariableReuse** `Variable ${VARIABLE} used in loop was also used in an outer loop on line {LINE}` + +### RedundantAssignmentPlugin.php + +This plugin checks for assignments where the variable already +has the given value. +(E.g. `$result = false; if (cond()) { $result = false; }`) + +- **PhanPluginRedundantAssignment** `Assigning {TYPE} to variable ${VARIABLE} which already has that value` +- **PhanPluginRedundantAssignmentInLoop** `Assigning {TYPE} to variable ${VARIABLE} which already has that value` +- **PhanPluginRedundantAssignmentInGlobalScope** `Assigning {TYPE} to variable ${VARIABLE} which already has that value` + +### UnknownClassElementAccessPlugin.php + +This plugin checks for accesses to unknown class elements that can't be type checked (which may hide potential runtime errors such as having too few parameters). +To reduce false positives, this will suppress warnings if at least one recursive analysis could infer class/interface types for the object. + +- **PhanPluginUnknownObjectMethodCall**: `Phan could not infer any class/interface types for the object of the method call {CODE} - inferred a type of {TYPE}` + +This works best when there is only one analysis process (the default, i.e. `--processes 1`). +`--analyze-twice` will reduce the number of issues this emits. + +### MoreSpecificElementTypePlugin.php + +This plugin checks for return types that can be made more specific. +**This has a large number of false positives - it can be used manually to point out comments that should be made more specific, but is not recommended as part of a build.** + +- **PhanPluginMoreSpecificActualReturnType**: `Phan inferred that {FUNCTION} documented to have return type {TYPE} returns the more specific type {TYPE}` +- **PhanPluginMoreSpecificActualReturnTypeContainsFQSEN**: `Phan inferred that {FUNCTION} documented to have return type {TYPE} (without an FQSEN) returns the more specific type {TYPE} (with an FQSEN)` + +It's strongly recommended to use this with a single analysis process (the default, i.e. `--processes 1`). + +This uses the following heuristics to reduce the number of false positives. + +- Avoids warning about methods that are overrides or are overridden. +- Avoids checking generators. +- Flattens array shapes and literals before comparing types +- Avoids warning when the actual return type contains multiple types and the declared return type is a single FQSEN + (e.g. don't warn about `Subclass1|Subclass2` being more specific than `BaseClass`) + +#### UnsafeCodePlugin.php + +This warns about code constructs that may be unsafe and prone to being used incorrectly in general. + +- **PhanPluginUnsafeEval**: `eval() is often unsafe and may have better alternatives such as closures and is unanalyzable. Suppress this issue if you are confident that input is properly escaped for this use case and there is no better way to do this.` +- **PhanPluginUnsafeShellExec**: `This syntax for shell_exec() ({CODE}) is easily confused for a string and does not allow proper exit code/stderr handling. Consider proc_open() instead.` +- **PhanPluginUnsafeShellExecDynamic**: `This syntax for shell_exec() ({CODE}) is easily confused for a string and does not allow proper exit code/stderr handling, and is used with a non-constant. Consider proc_open() instead.` + +### 3. Plugins Specific to Code Styles + +These plugins may be useful to enforce certain code styles, +but may cause false positives in large projects with different code styles. + +#### NonBool + +##### NonBoolBranchPlugin.php + +- **PhanPluginNonBoolBranch** Warns if an expression which has types other than `bool` is used in an if/else if. + + (E.g. warns about `if ($x)`, where $x is an integer. Fix by checking `if ($x != 0)`, etc.) + +##### NonBoolInLogicalArithPlugin.php + +- **PhanPluginNonBoolInLogicalArith** Warns if an expression where the left/right-hand side has types other than `bool` is used in a binary operation. + + (E.g. warns about `if ($x && $x->fn())`, where $x is an object. Fix by checking `if (($x instanceof MyClass) && $x->fn())`) + +#### HasPHPDocPlugin.php + +Checks if an element (class or property) has a PHPDoc comment, +and that Phan can extract a plaintext summary/description from that comment. + +- **PhanPluginNoCommentOnClass**: `Class {CLASS} has no doc comment` +- **PhanPluginDescriptionlessCommentOnClass**: `Class {CLASS} has no readable description: {STRING_LITERAL}` +- **PhanPluginNoCommentOnFunction**: `Function {FUNCTION} has no doc comment` +- **PhanPluginDescriptionlessCommentOnFunction**: `Function {FUNCTION} has no readable description: {STRING_LITERAL}` +- **PhanPluginNoCommentOnPublicProperty**: `Public property {PROPERTY} has no doc comment` (Also exists for Private and Protected) +- **PhanPluginDescriptionlessCommentOnPublicProperty**: `Public property {PROPERTY} has no readable description: {STRING_LITERAL}` (Also exists for Private and Protected) + +Warnings about method verbosity also exist, many categories may need to be completely disabled due to the large number of method declarations in a typical codebase: + +- Warnings are not emitted for `@internal` methods. +- Warnings are not emitted for methods that override methods in the parent class. +- Warnings can be suppressed based on the method FQSEN with `plugin_config => [..., 'has_phpdoc_method_ignore_regex' => (a PCRE regex)]` + + (e.g. to suppress issues about tests, or about missing documentation about getters and setters, etc.) +- This can be used to warn about duplicate method/property descriptions with `plugin_config => [..., 'has_phpdoc_check_duplicates' => true]` + (this skips checking method overrides, magic methods, and deprecated methods/properties) + +The warning types for methods are below: + +- **PhanPluginNoCommentOnPublicMethod**: `Public method {METHOD} has no doc comment` (Also exists for Private and Protected) +- **PhanPluginDescriptionlessCommentOnPublicMethod**: `Public method {METHOD} has no readable description: {STRING_LITERAL}` (Also exists for Private and Protected) +- **PhanPluginDuplicatePropertyDescription**: `Property {PROPERTY} has the same description as the property {PROPERTY} on line {LINE}: {COMMENT}` +- **PhanPluginDuplicateMethodDescription**: `Method {METHOD} has the same description as the method {METHOD} on line {LINE}: {COMMENT}` + +#### PHPDocInWrongCommentPlugin + +This plugin warns about using phpdoc annotations such as `@param` in block comments(`/*`) instead of phpdoc comments(`/**`). +This also warns about using `#` instead of `//` for line comments, because `#[` is used for php 8.0 attributes and will cause confusion. + +- **PhanPluginPHPDocInWrongComment**: `Saw possible phpdoc annotation in ordinary block comment {COMMENT}. PHPDoc comments should start with "/**", not "/*"` +- **PhanPluginPHPDocHashComment**: `Saw comment starting with # in {COMMENT} - consider using // instead to avoid confusion with php 8.0 #[ attributes` + +#### InvalidVariableIssetPlugin.php + +Warns about invalid uses of `isset`. This README documentation may be inaccurate for this plugin. + +- **PhanPluginInvalidVariableIsset** : Forces all uses of `isset` to be on arrays or variables. + + E.g. it will warn about `isset(foo()['key'])`, because foo() is not a variable or an array access. +- **PhanUndeclaredVariable**: Warns if `$array` is undeclared in `isset($array[$key])` + +#### NoAssertPlugin.php + +Discourages the usage of assert() in the analyzed project. +See https://secure.php.net/assert + +- **PhanPluginNoAssert**: `assert() is discouraged. Although phan supports using assert() for type annotations, PHP's documentation recommends assertions only for debugging, and assert() has surprising behaviors.` + +#### NotFullyQualifiedUsagePlugin.php + +Encourages the usage of fully qualified global functions and constants (slightly faster, especially for functions such as `strlen`, `count`, etc.) + +- **PhanPluginNotFullyQualifiedFunctionCall**: `Expected function call to {FUNCTION}() to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}` +- **PhanPluginNotFullyQualifiedOptimizableFunctionCall**: `Expected function call to {FUNCTION}() to be fully qualified or have a use statement but none were found in namespace {NAMESPACE} (opcache can optimize fully qualified calls to this function in recent php versions)` +- **PhanPluginNotFullyQualifiedGlobalConstant**: `Expected usage of {CONST} to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}` + +#### NumericalComparisonPlugin.php + +Enforces that loose equality is used for numeric operands (e.g. `2 == 2.0`), and that strict equality is used for non-numeric operands (e.g. `"2" === "2e0"` is false). + +- **PhanPluginNumericalComparison**: `nonnumerical values compared by the operators '==' or '!=='; numerical values compared by the operators '===' or '!=='` + +#### StrictLiteralComparisonPlugin.php + +Enforces that strict equality is used for comparisons to constant/literal integers or strings. +This is used to avoid surprising behaviors such as `0 == 'a'`, `"10" == "1e1"`, etc. +*Following the advice of this plugin may subtly break existing code (e.g. break implicit null/false checks, or code relying on these unexpected behaviors).* + +- **PhanPluginComparisonNotStrictForScalar**: `Expected strict equality check when comparing {TYPE} to {TYPE} in {CODE}` + +Also see [`StrictComparisonPlugin`](#StrictComparisonPlugin.php) and [`NumericalComparisonPlugin`](#NumericalComparisonPlugin.php). + +#### PHPUnitNotDeadCodePlugin.php + +Marks unit tests and dataProviders of subclasses of PHPUnit\Framework\TestCase as referenced. +Avoids false positives when `--dead-code-detection` is enabled. + +(Does not emit any issue types) + +#### SleepCheckerPlugin.php + +Warn about returning non-arrays in [`__sleep`](https://secure.php.net/__sleep), +as well as about returning array values with invalid property names in `__sleep`. + +- **SleepCheckerInvalidReturnStatement`**: `__sleep must return an array of strings. This is definitely not an array.` +- **SleepCheckerInvalidReturnType**: `__sleep is returning {TYPE}, expected string[]` +- **SleepCheckerInvalidPropNameType**: `__sleep is returning an array with a value of type {TYPE}, expected string` +- **SleepCheckerInvalidPropName**: `__sleep is returning an array that includes {PROPERTY}, which cannot be found` +- **SleepCheckerMagicPropName**: `__sleep is returning an array that includes {PROPERTY}, which is a magic property` +- **SleepCheckerDynamicPropName**: `__sleep is returning an array that includes {PROPERTY}, which is a dynamically added property (but not a declared property)` +- **SleepCheckerPropertyMissingTransient**: `Property {PROPERTY} that is not serialized by __sleep should be annotated with @transient or @phan-transient`, + +#### UnknownElementTypePlugin.php + +Warns about elements containing unknown types (function/method/closure return types, parameter types) + +- **PhanPluginUnknownMethodReturnType**: `Method {METHOD} has no declared or inferred return type` +- **PhanPluginUnknownMethodParamType**: `Method {METHOD} has no declared or inferred parameter type for ${PARAMETER}` +- **PhanPluginUnknownFunctionReturnType**: `Function {FUNCTION} has no declared or inferred return type` +- **PhanPluginUnknownFunctionParamType**: `Function {FUNCTION} has no declared or inferred return type for ${PARAMETER}` +- **PhanPluginUnknownClosureReturnType**: `Closure {FUNCTION} has no declared or inferred return type` +- **PhanPluginUnknownClosureParamType**: `Closure {FUNCTION} has no declared or inferred return type for ${PARAMETER}` +- **PhanPluginUnknownPropertyType**: `Property {PROPERTY} has an initial type that cannot be inferred` + +#### DuplicateExpressionPlugin.php + +This plugin checks for duplicate expressions in a statement +that are likely to be a bug. (e.g. `expr1 == expr`) + +This will significantly increase the memory used by Phan, but that's rarely an issue in small projects. + +- **PhanPluginDuplicateExpressionAssignment**: `Both sides of the assignment {OPERATOR} are the same: {CODE}` +- **PhanPluginDuplicateExpressionBinaryOp**: `Both sides of the binary operator {OPERATOR} are the same: {CODE}` +- **PhanPluginDuplicateConditionalTernaryDuplication**: `"X ? X : Y" can usually be simplified to "X ?: Y". The duplicated expression X was {CODE}` +- **PhanPluginDuplicateConditionalNullCoalescing**: `"isset(X) ? X : Y" can usually be simplified to "X ?? Y" in PHP 7. The duplicated expression X was {CODE}` +- **PhanPluginBothLiteralsBinaryOp**: `Suspicious usage of a binary operator where both operands are literals. Expression: {CODE} {OPERATOR} {CODE} (result is {CODE})` (e.g. warns about `null == 'a literal` in `$x ?? null == 'a literal'`) +- **PhanPluginDuplicateConditionalUnnecessary**: `"X ? Y : Y" results in the same expression Y no matter what X evaluates to. Y was {CODE}` +- **PhanPluginDuplicateCatchStatementBody**: `The implementation of catch({CODE}) and catch({CODE}) are identical, and can be combined if the application only needs to supports php 7.1 and newer` +- **PhanPluginDuplicateAdjacentStatement**: `Statement {CODE} is a duplicate of the statement on the above line. Suppress this issue instance if there's a good reason for this.` + + Note that equivalent catch statements may be deliberate or a coding style choice, and this plugin does not check for TODOs. + +#### WhitespacePlugin.php + +This plugin checks for unexpected whitespace in PHP files. + +- **PhanPluginWhitespaceCarriageReturn**: `The first occurrence of a carriage return ("\r") was seen here. Running "dos2unix" can fix that.` +- **PhanPluginWhitespaceTab**: `The first occurrence of a tab was seen here. Running "expand" can fix that.` +- **PhanPluginWhitespaceTrailing**: `The first occurrence of trailing whitespace was seen here.` + +#### InlineHTMLPlugin.php + +This plugin checks for unexpected inline HTML. + +This can be limited to a subset of files with an `inline_html_whitelist_regex` - e.g. `@^(src/|lib/)@`. + +Files can be excluded with `inline_html_blacklist_regex`, e.g. `@(^src/templates/)|(\.html$)@` + +- **PhanPluginInlineHTML**: `Saw inline HTML between the first and last token: {STRING_LITERAL}` +- **PhanPluginInlineHTMLLeading**: `Saw inline HTML at the start of the file: {STRING_LITERAL}` +- **PhanPluginInlineHTMLTrailing**: `Saw inline HTML at the end of the file: {STRING_LITERAL}` + +#### SuspiciousParamOrderPlugin.php + +This plugin guesses if arguments to a function call are out of order, based on heuristics on the name in the expression (e.g. variable name). +This will only warn if the argument types are compatible with the alternate parameters being suggested. +This may be useful when analyzing methods with long parameter lists. + +E.g. warns about invoking `function example($first, $second, $third)` as `example($mySecond, $myThird, $myFirst)` + +- **PhanPluginSuspiciousParamOrder**: `Suspicious order for arguments named {DETAILS} - These are being passed to parameters {DETAILS} of {FUNCTION} defined at {FILE}:{LINE}` +- **PhanPluginSuspiciousParamOrderInternal**: `Suspicious order for arguments named {DETAILS} - These are being passed to parameters {DETAILS}` + +#### PossiblyStaticMethodPlugin.php + +Checks if a method can be made static without causing any errors. + +- **PhanPluginPossiblyStaticPublicMethod**: `Public method {METHOD} can be static` (Also exists for Private and Protected) +- **PhanPluginPossiblyStaticClosure**: `{FUNCTION} can be static` + +Warnings may need to be completely disabled due to the large number of method declarations in a typical codebase: + +- Warnings are not emitted for methods that override methods in the parent class. +- Warnings are not emitted for methods that are overridden in child classes. +- Warnings can be suppressed based on the method FQSEN with `plugin_config => [..., 'possibly_static_method_ignore_regex' => (a PCRE regex)]` + +#### PHPDocToRealTypesPlugin.php + +This plugin suggests real types that can be used instead of phpdoc types. +Currently, this just checks param and return types. +Some of the suggestions made by this plugin will cause inheritance errors. + +This doesn't suggest changes if classes have subclasses (but this check doesn't work when inheritance involves traits). +`PHPDOC_TO_REAL_TYPES_IGNORE_INHERITANCE=1` can be used to force this to check **all** methods and emit issues. + +This also supports `--automatic-fix` to add the types to the real type signatures. + +- **PhanPluginCanUseReturnType**: `Can use {TYPE} as a return type of {METHOD}` +- **PhanPluginCanUseNullableReturnType**: `Can use {TYPE} as a return type of {METHOD}` (useful if there is a minimum php version of 7.1) +- **PhanPluginCanUsePHP71Void**: `Can use php 7.1's void as a return type of {METHOD}` (useful if there is a minimum php version of 7.1) + +This supports `--automatic-fix`. +- `PHPDocRedundantPlugin` will be useful for cleaning up redundant phpdoc after real types were added. +- `PreferNamespaceUsePlugin` can be used to convert types from fully qualified types back to unqualified types () + +#### PHPDocRedundantPlugin.php + +This plugin warns about function/method/closure phpdoc that does nothing but repeat the information in the type signature. +E.g. this will warn about `/** @return void */ function () : void {}` and `/** */`, but not `/** @return void description of what it does or other annotations */` + +This supports `--automatic-fix` + +- **PhanPluginRedundantFunctionComment**: `Redundant doc comment on function {FUNCTION}(). Either add a description or remove the comment: {COMMENT}` +- **PhanPluginRedundantMethodComment**: `Redundant doc comment on method {METHOD}(). Either add a description or remove the comment: {COMMENT}` +- **PhanPluginRedundantClosureComment**: `Redundant doc comment on closure {FUNCTION}. Either add a description or remove the comment: {COMMENT}` +- **PhanPluginRedundantReturnComment**: `Redundant @return {TYPE} on function {FUNCTION}. Either add a description or remove the @return annotation: {COMMENT}` + +#### PreferNamespaceUsePlugin.php + +This plugin suggests using `ClassName` instead of `\My\Ns\ClassName` when there is a `use My\Ns\ClassName` annotation (or for uses in namespace `\My\Ns`) +Currently, this only checks **real** (not phpdoc) param/return annotations. + +- **PhanPluginPreferNamespaceUseParamType**: `Could write param type of ${PARAMETER} of {FUNCTION} as {TYPE} instead of {TYPE}` +- **PhanPluginPreferNamespaceUseReturnType**: `Could write return type of {FUNCTION} as {TYPE} instead of {TYPE}` + +##### StrictComparisonPlugin.php + +This plugin warns about non-strict comparisons. It warns about the following issue types: + +1. Using `in_array` and `array_search` without explicitly passing true or false to `$strict`. +2. Using equality or comparison operators when both sides are possible objects. + +- **PhanPluginComparisonNotStrictInCall**: `Expected {FUNCTION} to be called with a third argument for {PARAMETER} (either true or false)` +- **PhanPluginComparisonObjectEqualityNotStrict**: `Saw a weak equality check on possible object types {TYPE} and {TYPE} in {CODE}` +- **PhanPluginComparisonObjectOrdering**: `Saw a weak equality check on possible object types {TYPE} and {TYPE} in {CODE}` + +##### EmptyMethodAndFunctionPlugin.php + +This plugin looks for empty methods/functions. +Note that this is not emitted for empty statement lists in functions or methods that are overrides, are overridden, or are deprecated. + +- **PhanEmptyClosure**: `Empty closure` +- **PhanEmptyFunction**: `Empty function {FUNCTION}` +- **PhanEmptyPrivateMethod**: `Empty private method {METHOD}` +- **PhanEmptyProtectedMethod**: `Empty protected method {METHOD}` +- **PhanEmptyPublicMethod**: `Empty public method {METHOD}` + +#### DollarDollarPlugin.php + +Checks for complex variable access expressions `$$x`, which may be hard to read, and make the variable accesses hard/impossible to analyze. + +- **PhanPluginDollarDollar**: Warns about the use of $$x, ${(expr)}, etc. + +### DeprecateAliasPlugin.php + +Makes Phan analyze aliases of global functions (e.g. `join()`, `sizeof()`) as if they were deprecated. +Supports `--automatic-fix`. + +#### PHP53CompatibilityPlugin.php + +Catches common incompatibilities from PHP 5.3 to 5.6. +**This plugin does not aim to be comprehensive - read the guides on https://www.php.net/manual/en/appendices.php if you need to migrate from php versions older than 5.6** + +`InvokePHPNativeSyntaxCheckPlugin` with `'php_native_syntax_check_binaries' => [PHP_BINARY, '/path/to/php53']` in the `'plugin_config'` is a better but slower way to check that syntax used does not cause errors in PHP 5.3. + +`backward_compatibility_checks` should also be enabled if migrating a project from php 5 to php 7. + +Emitted issue types: + +- **PhanPluginCompatibilityShortArray**: `Short arrays ({CODE}) require support for php 5.4+` +- **PhanPluginCompatibilityArgumentUnpacking**: `Argument unpacking ({CODE}) requires support for php 5.6+` +- **PhanPluginCompatibilityVariadicParam**: `Variadic functions ({CODE}) require support for php 5.6+` + +#### DuplicateConstantPlugin.php + +Checks for duplicate constant names for calls to `define()` or `const X =` within the same statement list. + +- **PhanPluginDuplicateConstant**: `Constant {CONST} was previously declared at line {LINE} - the previous declaration will be used instead` + +#### AvoidableGetterPlugin.php + +This plugin checks for uses of getters on `$this` that can be avoided inside of a class. +(E.g. calling `$this->getFoo()` when the property `$this->foo` is accessible, and there are no known overrides of the getter) + +- **PhanPluginAvoidableGetter**: `Can replace {METHOD} with {PROPERTY}` +- **PhanPluginAvoidableGetterInTrait**: `Can replace {METHOD} with {PROPERTY}` + +Note that switching to properties makes the code slightly faster, +but may break code outside of the library that overrides those getters, +or hurt the readability of code. + +This will also remove runtime type checks that were enforced by the getter's return type. + +#### ConstantVariablePlugin.php + +This plugin warns about using variables when they probably have only one possible scalar value (or the only inferred type is `null`). +This may catch some logic errors such as `echo($result === null ? json_encode($result) : 'default')`, or indicate places where it may or may not be clearer to use the constant itself. +Most of the reported issues will likely not be worth fixing, or be false positives due to references/loops. + +- **PhanPluginConstantVariableBool**: `Variable ${VARIABLE} is probably constant with a value of {TYPE}` +- **PhanPluginConstantVariableNull**: `Variable ${VARIABLE} is probably constant with a value of {TYPE}` +- **PhanPluginConstantVariableScalar**: `Variable ${VARIABLE} is probably constant with a value of {TYPE}` + +#### ShortArrayPlugin.php + +This suggests using shorter array syntaxes if supported by the `minimum_target_php_version`. + +- **PhanPluginLongArray**: `Should use [] instead of array()` +- **PhanPluginLongArrayList**: `Should use [] instead of list()` + +#### RemoveDebugStatementPlugin.php + +This suggests removing debugging output statements such as `echo`, `print`, `printf`, fwrite(STDERR)`, `var_export()`, inline html, etc. +This is only useful in applications or libraries that print output in only a few places, as a sanity check that debugging statements are not accidentally left in code. + +- **PhanPluginRemoveDebugEcho**: `Saw output expression/statement in {CODE}` +- **PhanPluginRemoveDebugCall**: `Saw call to {FUNCTION} for debugging` + +Suppression comments can use the issue name `PhanPluginRemoveDebugAny` to suppress all issue types emitted by this plugin. + +### 4. Demo plugins: + +These files demonstrate plugins for Phan. + +#### DemoPlugin.php + +Look at this class's documentation if you want an example to base your plugin off of. +Generates the following issue types under the types: + +- **DemoPluginClassName**: a declared class isn't called 'Class' +- **DemoPluginFunctionName**: a declared function isn't called `function` +- **DemoPluginMethodName**: a declared method isn't called `function` + PHP's default checks(`php -l` would catch the class/function name types.) +- **DemoPluginInstanceof**: codebase contains `(expr) instanceof object` (usually invalid, and `is_object()` should be used instead. That would actually be a check for `class object`). + +### 5. Third party plugins + +- https://github.com/Drenso/PhanExtensions is a third party project with several plugins to do the following: + + - Analyze Symfony doc comment annotations. + - Mark elements in inline doc comments (which Phan doesn't parse) as referencing types from `use statements` as not dead code. + +- https://github.com/TysonAndre/PhanTypoCheck checks all tokens of PHP files for typos, including within string literals. + It is also able to analyze calls to `gettext()`. + +### 6. Self-analysis plugins: + +#### PhanSelfCheckPlugin.php + +This plugin checks for invalid calls to `PluginV2::emitIssue`, `Issue::maybeEmit()`, etc. +This is useful for developing Phan and Phan plugins. + +- **PhanPluginTooFewArgumentsForIssue**: `Too few arguments for issue {STRING_LITERAL}: expected {COUNT}, got {COUNT}` +- **PhanPluginTooManyArgumentsForIssue**: `Too many arguments for issue {STRING_LITERAL}: expected {COUNT}, got {COUNT}` +- **PhanPluginUnknownIssueType**: `Unknown issue type {STRING_LITERAL} in a call to {METHOD}(). (may be a false positive - check if the version of Phan running PhanSelfCheckPlugin is the same version that the analyzed codebase is using)` diff --git a/bundled-libs/phan/phan/.phan/plugins/RedundantAssignmentPlugin.php b/bundled-libs/phan/phan/.phan/plugins/RedundantAssignmentPlugin.php new file mode 100644 index 000000000..ba8390e75 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/RedundantAssignmentPlugin.php @@ -0,0 +1,159 @@ +children['var']; + if (!$var instanceof Node) { + return; + } + if ($var->kind !== ast\AST_VAR) { + return; + } + $var_name = $var->children['name']; + if (!is_string($var_name)) { + return; + } + $variable = $this->context->getScope()->getVariableByNameOrNull($var_name); + if (!$variable || $variable instanceof PassByReferenceVariable) { + return; + } + $variable_type = $variable->getUnionType(); + if ($variable_type->isPossiblyUndefined() || count($variable_type->getRealTypeSet()) !== 1) { + return; + } + $old_value = $variable_type->getRealUnionType()->asValueOrNullOrSelf(); + if (is_object($old_value)) { + return; + } + $expr = $node->children['expr']; + if (!ParseVisitor::isConstExpr($expr)) { + return; + } + try { + $expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr, false); + } catch (Exception $_) { + return; + } + if (count($expr_type->getRealTypeSet()) !== 1) { + return; + } + $expr_value = $expr_type->getRealUnionType()->asValueOrNullOrSelf(); + if ($expr_value !== $old_value) { + return; + } + if ($this->context->hasSuppressIssue($this->code_base, 'PhanPluginRedundantAssignment')) { + // Suppressing this suppresses the more specific issues. + return; + } + if ($this->context->isInGlobalScope()) { + if ($variable->getFileRef()->getFile() !== $this->context->getFile()) { + // Don't warn if this variable was set by a different file + return; + } + if (Config::getValue('__analyze_twice') && $variable->getFileRef()->getLineNumberStart() === $this->context->getLineNumberStart()) { + // Don't warn if this variable was set by a different file + return; + } + $issue_name = 'PhanPluginRedundantAssignmentInGlobalScope'; + } elseif ($this->context->isInLoop()) { + $issue_name = 'PhanPluginRedundantAssignmentInLoop'; + } else { + $issue_name = 'PhanPluginRedundantAssignment'; + } + if ($this->context->isInLoop()) { + $this->context->deferCheckToOutermostLoop(function (Context $context_after_loop) use ($issue_name, $var_name, $variable_type): void { + $new_variable = $context_after_loop->getScope()->getVariableByNameOrNull($var_name); + if (!$new_variable) { + return; + } + $new_variable_type = $new_variable->getUnionType(); + if ($new_variable_type->isPossiblyUndefined()) { + return; + } + if ($new_variable_type->getRealTypeSet() !== $variable_type->getRealTypeSet()) { + return; + } + $this->emitPluginIssue( + $this->code_base, + $this->context, + $issue_name, + 'Assigning {TYPE} to variable ${VARIABLE} which already has that value', + [$variable_type, $var_name] + ); + }); + return; + } + $this->emitPluginIssue( + $this->code_base, + $this->context, + $issue_name, + 'Assigning {TYPE} to variable ${VARIABLE} which already has that value', + [$expr_type, $var_name] + ); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. + +return new RedundantAssignmentPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/RemoveDebugStatementPlugin.php b/bundled-libs/phan/phan/.phan/plugins/RemoveDebugStatementPlugin.php new file mode 100644 index 000000000..cd89ca7b5 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/RemoveDebugStatementPlugin.php @@ -0,0 +1,167 @@ + + */ + public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array + { + $warn_remove_debug_call = static function (CodeBase $code_base, Context $context, FunctionInterface $function): void { + self::emitIssue( + $code_base, + $context, + 'PhanPluginRemoveDebugCall', + 'Saw call to {FUNCTION} for debugging', + [(string)$function->getFQSEN()] + ); + }; + /** + * @param list $unused_args the nodes for the arguments to the invocation + */ + $always_debug_callback = static function ( + CodeBase $code_base, + Context $context, + Func $function, + array $unused_args + ) use ($warn_remove_debug_call): void { + if (self::shouldSuppressDebugIssues($code_base, $context)) { + return; + } + $warn_remove_debug_call($code_base, $context, $function); + }; + /** + * @param list $args the nodes for the arguments to the invocation + * Based on DependentReturnTypeOverridePlugin check + */ + $var_export_callback = static function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ) use ($warn_remove_debug_call): void { + if (self::shouldSuppressDebugIssues($code_base, $context)) { + return; + } + + if (count($args) >= 2) { + $result = (new ContextNode($code_base, $context, $args[1]))->getEquivalentPHPScalarValue(); + // @phan-suppress-next-line PhanSuspiciousTruthyString + if (is_object($result) || $result) { + return; + } + } + $warn_remove_debug_call($code_base, $context, $function); + }; + + /** + * @param list $args the nodes for the arguments to the invocation + */ + $fwrite_callback = static function ( + CodeBase $code_base, + Context $context, + Func $function, + array $args + ) use ($warn_remove_debug_call): void { + $file = $args[0] ?? null; + // @phan-suppress-next-line PhanPossiblyUndeclaredProperty + if (!$file instanceof Node || $file->kind !== ast\AST_CONST || !in_array($file->children['name']->children['name'], ['STDOUT', 'STDERR'], true)) { + // Could resolve the constant, but low priority + return; + } + if (self::shouldSuppressDebugIssues($code_base, $context)) { + return; + } + + $warn_remove_debug_call($code_base, $context, $function); + }; + + return [ + 'var_dump' => $always_debug_callback, + 'printf' => $always_debug_callback, + 'debug_print_backtrace' => $always_debug_callback, + 'debug_zval_dump' => $always_debug_callback, + // Warn for these functions unless the second argument is false + 'var_export' => $var_export_callback, + 'print_r' => $var_export_callback, + + // check for STDOUT/STDERR + 'fwrite' => $fwrite_callback, + 'fprintf' => $fwrite_callback, + ]; + } + + /** + * Returns true if any debug issue should be suppressed + */ + public static function shouldSuppressDebugIssues(CodeBase $code_base, Context $context): bool + { + return Issue::shouldSuppressIssue($code_base, $context, RemoveDebugStatementPlugin::ISSUE_GROUP, $context->getLineNumberStart(), []); + } +} + +/** + * Analyzes node kinds that are associated with debugging + */ +class RemoveDebugStatementVisitor extends PluginAwarePostAnalysisVisitor +{ + /** + * @param Node $node a node of kind ast\AST_ECHO + */ + public function visitPrint(Node $node): void + { + $this->visitEcho($node); + } + + /** + * @param Node $node a node which echoes or prints + */ + public function visitEcho(Node $node): void + { + if (RemoveDebugStatementPlugin::shouldSuppressDebugIssues($this->code_base, $this->context)) { + return; + } + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginRemoveDebugEcho', + "Saw output expression/statement in {CODE}", + [ASTReverter::toShortString($node)] + ); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new RemoveDebugStatementPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/ShortArrayPlugin.php b/bundled-libs/phan/phan/.phan/plugins/ShortArrayPlugin.php new file mode 100644 index 000000000..55c37128a --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/ShortArrayPlugin.php @@ -0,0 +1,69 @@ +flags) { + case \ast\flags\ARRAY_SYNTAX_LONG: + $this->emit( + 'PhanPluginShortArray', + 'Should use [] instead of array()', + [], + Issue::SEVERITY_LOW, + Issue::REMEDIATION_A + ); + return; + case \ast\flags\ARRAY_SYNTAX_LIST: + if (Config::get_closest_minimum_target_php_version_id() >= 70100) { + $this->emit( + 'PhanPluginShortArrayList', + 'Should use [] instead of list()', + [], + Issue::SEVERITY_LOW, + Issue::REMEDIATION_A + ); + } + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new ShortArrayPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/SimplifyExpressionPlugin.php b/bundled-libs/phan/phan/.phan/plugins/SimplifyExpressionPlugin.php new file mode 100644 index 000000000..70b70d830 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/SimplifyExpressionPlugin.php @@ -0,0 +1,187 @@ + 0 ? true : false` can be simplified to `$x > 0` + * + * Note that in PHP 7, many functions did not yet have real return types + * + * This file demonstrates plugins for Phan. Plugins hook into various events. + * DuplicateExpressionPlugin hooks into one event: + * + * - getPostAnalyzeNodeVisitorClassName + * This method returns a visitor that is called on every AST node from every + * file being analyzed in post-order + * + * A plugin file must + * + * - Contain a class that inherits from \Phan\PluginV3 + * + * - End by returning an instance of that class. + * + * It is assumed without being checked that plugins aren't + * mangling state within the passed code base or context. + * + * Note: When adding new plugins, + * add them to the corresponding section of README.md + */ +class SimplifyExpressionPlugin extends PluginV3 implements + PostAnalyzeNodeCapability +{ + + /** + * @return class-string - name of PluginAwarePostAnalysisVisitor subclass + * @override + */ + public static function getPostAnalyzeNodeVisitorClassName(): string + { + return SimplifyExpressionVisitor::class; + } +} + +/** + * This visitor analyzes node kinds that can be the root of expressions + * that can be simplified, and is called on nodes in post-order. + */ +class SimplifyExpressionVisitor extends PluginAwarePostAnalysisVisitor +{ + /** + * Returns true if all types are strictly subtypes of `bool` + */ + protected static function isDefinitelyBool(UnionType $union_type): bool + { + $real_type_set = $union_type->getRealTypeSet(); + if (!$real_type_set) { + return false; + } + foreach ($real_type_set as $type) { + if (!$type->isInBoolFamily() || $type->isNullable()) { + return false; + } + if (count($real_type_set) === 1) { + // If the expression is `true` or `false`, assume that ExtendedDependentReturnPlugin or some other plugin + // inferred a literal value instead of the expression being guaranteed to be a boolean. + // (e.g. `strpos(SOME_CONST, 'val') === false`) + // + // TODO: Could check if the expression is a call and what the getRealReturnType is for that function. + return $type instanceof BoolType; + } + } + return true; + } + + /** + * @param Node|string|int|float|null $node + * @return ?bool if this is the name of a boolean, the value. Otherwise, returns null. + */ + private static function getBoolConst($node): ?bool + { + if (!$node instanceof Node) { + return null; + } + if ($node->kind !== ast\AST_CONST) { + return null; + } + // @phan-suppress-next-line PhanPossiblyUndeclaredProperty, PhanPartialTypeMismatchArgumentInternal + switch (strtolower($node->children['name']->children['name'])) { + case 'false': + return false; + case 'true': + return true; + } + return null; + } + + /** + * @param Node $node + * A ternary operation node of kind ast\AST_CONDITIONAL to analyze + * @override + */ + public function visitConditional(Node $node): void + { + // Detect conditions such as`$bool ?: null` or `$bool ? true : false` + $true_node = $node->children['true']; + $value_if_true = $true_node !== null ? self::getBoolConst($true_node) : true; + if (!is_bool($value_if_true)) { + return; + } + $value_if_false = self::getBoolConst($node->children['false']); + if ($value_if_false !== !$value_if_true) { + return; + } + $this->suggestBoolSimplification($node, $node->children['cond'], !$value_if_true); + } + + /** + * @param Node|string|int|float $inner_expr + */ + private function suggestBoolSimplification(Node $node, $inner_expr, bool $negate): void + { + if (!self::isDefinitelyBool(UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $inner_expr))) { + return; + } + // TODO: Use redundant condition detection helper methods to handle loops + $new_inner_repr = ASTReverter::toShortString($inner_expr); + if ($negate) { + $new_inner_repr = "!($new_inner_repr)"; + } + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginSimplifyExpressionBool', + '{CODE} can probably be simplified to {CODE}', + [ + ASTReverter::toShortString($node), + $new_inner_repr, + ] + ); + } + + /** + * @param Node $node + * A binary op node of kind ast\AST_BINARY_OP to analyze + * @override + */ + public function visitBinaryOp(Node $node): void + { + $is_negated_assertion = false; + switch ($node->flags) { + case flags\BINARY_IS_NOT_IDENTICAL: + case flags\BINARY_IS_NOT_EQUAL: + case flags\BINARY_BOOL_XOR: + $is_negated_assertion = true; + case flags\BINARY_IS_EQUAL: + case flags\BINARY_IS_IDENTICAL: + ['left' => $left_node, 'right' => $right_node] = $node->children; + $left_const = self::getBoolConst($left_node); + if (is_bool($left_const)) { + // E.g. `$x === true` can be simplified to `$x` + $this->suggestBoolSimplification($node, $right_node, $left_const === $is_negated_assertion); + return; + } + $right_const = self::getBoolConst($right_node); + if (is_bool($right_const)) { + $this->suggestBoolSimplification($node, $left_node, $right_const === $is_negated_assertion); + } + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. + +return new SimplifyExpressionPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/SleepCheckerPlugin.php b/bundled-libs/phan/phan/.phan/plugins/SleepCheckerPlugin.php new file mode 100644 index 000000000..9cfbec3ea --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/SleepCheckerPlugin.php @@ -0,0 +1,243 @@ +children['name']) !== 0) { + return; + } + $sleep_properties = []; + $this->analyzeStatementsOfSleep($node, $sleep_properties); + $this->warnAboutTransientSleepProperties($sleep_properties); + } + + /** + * Warn about instance properties that aren't mentioned in __sleep() + * and don't have (at)transient or (at)phan-transient + * + * @param array $sleep_properties + */ + private function warnAboutTransientSleepProperties(array $sleep_properties): void + { + if (count($sleep_properties) === 0) { + // Give up, failed to extract property names + return; + } + $class = $this->context->getClassInScope($this->code_base); + $class_fqsen = $class->getFQSEN(); + foreach ($class->getPropertyMap($this->code_base) as $property_name => $property) { + if ($property->isStatic()) { + continue; + } + if ($property->isFromPHPDoc()) { + continue; + } + if ($property->isDynamicProperty()) { + continue; + } + if (isset($sleep_properties[$property_name])) { + continue; + } + if ($property->getRealDefiningFQSEN()->getFullyQualifiedClassName() !== $class_fqsen) { + continue; + } + $doc_comment = $property->getDocComment() ?? ''; + $has_transient = preg_match('/@(phan-)?transient\b/', $doc_comment) > 0; + if (!$has_transient) { + $regex = Config::getValue('plugin_config')['sleep_transient_warning_blacklist_regex'] ?? null; + if (is_string($regex) && preg_match($regex, $property_name)) { + continue; + } + $this->emitPluginIssue( + $this->code_base, + $property->getContext(), + 'SleepCheckerPropertyMissingTransient', + 'Property {PROPERTY} that is not serialized by __sleep should be annotated with @transient or @phan-transient', + [$property->__toString()] + ); + } + } + } + + /** + * @param Node|int|string|float|null $node + * @param array $sleep_properties + */ + private function analyzeStatementsOfSleep($node, array &$sleep_properties = []): void + { + if (!($node instanceof Node)) { + if (is_array($node)) { + foreach ($node as $child_node) { + $this->analyzeStatementsOfSleep($child_node, $sleep_properties); + } + } + return; + } + switch ($node->kind) { + case ast\AST_RETURN: + $this->analyzeReturnValue($node->children['expr'], $node->lineno, $sleep_properties); + return; + case ast\AST_CLASS: + case ast\AST_CLOSURE: + case ast\AST_FUNC_DECL: + return; + default: + foreach ($node->children as $child_node) { + $this->analyzeStatementsOfSleep($child_node, $sleep_properties); + } + } + } + + private const RESOLVE_SETTINGS = + ContextNode::RESOLVE_ARRAYS | + ContextNode::RESOLVE_ARRAY_VALUES | + ContextNode::RESOLVE_CONSTANTS; + + /** + * @param Node|string|int|float|null $expr_node + * @param int $lineno + * @param array $sleep_properties + */ + private function analyzeReturnValue($expr_node, int $lineno, array &$sleep_properties): void + { + $context = clone($this->context)->withLineNumberStart($lineno); + if (!($expr_node instanceof Node)) { + $this->emitPluginIssue( + $this->code_base, + $context, + 'SleepCheckerInvalidReturnStatement', + '__sleep must return an array of strings. This is definitely not an array.' + ); + return; + } + $code_base = $this->code_base; + + $union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr_node); + if (!$union_type->hasArray()) { + $this->emitPluginIssue( + $this->code_base, + $context, + 'SleepCheckerInvalidReturnType', + '__sleep is returning {TYPE}, expected {TYPE}', + [(string)$union_type, 'string[]'] + ); + return; + } + if (!$context->isInClassScope()) { + return; + } + + $kind = $expr_node->kind; + if (!\in_array($kind, [ast\AST_CONST, ast\AST_ARRAY, ast\AST_CLASS_CONST], true)) { + return; + } + + $value = (new ContextNode($code_base, $context, $expr_node))->getEquivalentPHPValue(self::RESOLVE_SETTINGS); + if (!is_array($value)) { + return; + } + $class = $context->getClassInScope($code_base); + + foreach ($value as $prop_name) { + if (!is_string($prop_name)) { + $prop_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $prop_name); + if (!$prop_type->isType(StringType::instance(false))) { + $this->emitPluginIssue( + $this->code_base, + $context, + 'SleepCheckerInvalidPropNameType', + '__sleep is returning an array with a value of type {TYPE}, expected {TYPE}', + [(string)$prop_type, 'string'] + ); + } + continue; + } + $sleep_properties[$prop_name] = true; + + if (!$class->hasPropertyWithName($code_base, $prop_name)) { + $this->emitPluginIssue( + $this->code_base, + $context, + 'SleepCheckerInvalidPropName', + '__sleep is returning an array that includes {PROPERTY}, which cannot be found', + [$prop_name] + ); + continue; + } + $prop = $class->getPropertyByName($code_base, $prop_name); + if ($prop->isFromPHPDoc()) { + $this->emitPluginIssue( + $this->code_base, + $context, + 'SleepCheckerMagicPropName', + '__sleep is returning an array that includes {PROPERTY}, which is a magic property', + [$prop_name] + ); + continue; + } + if ($prop->isDynamicProperty()) { + $this->emitPluginIssue( + $this->code_base, + $context, + 'SleepCheckerDynamicPropName', + '__sleep is returning an array that includes {PROPERTY}, which is a dynamically added property (but not a declared property)', + [$prop_name] + ); + continue; + } + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new SleepCheckerPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/StrictComparisonPlugin.php b/bundled-libs/phan/phan/.phan/plugins/StrictComparisonPlugin.php new file mode 100644 index 000000000..39425b716 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/StrictComparisonPlugin.php @@ -0,0 +1,164 @@ + + */ + public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array + { + /** + * @return Closure(CodeBase,Context,Func,array):void + */ + $make_callback = static function (int $index, string $index_name, int $min_args): Closure { + /** + * @param list $args the nodes for the arguments to the invocation + */ + return static function ( + CodeBase $code_base, + Context $context, + Func $func, + array $args + ) use ( + $index, + $index_name, + $min_args +): void { + if (count($args) < $min_args) { + return; + } + $strict_node = $args[$index] ?? null; + if ($strict_node instanceof Node) { + $type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $strict_node)->asSingleScalarValueOrNullOrSelf(); + if ($type === true) { + return; + } elseif ($type === false) { + return; + } + } + self::emitPluginIssue( + $code_base, + $context, + self::ComparisonNotStrictInCall, + "Expected {FUNCTION} to be called with a $index_name argument for {PARAMETER} (either true or false)", + [$func->getName(), '$strict'] + ); + }; + }; + // More functions might be added in the future + $always_warn_third_not_strict = $make_callback(2, 'third', 0); + + return [ + 'in_array' => $always_warn_third_not_strict, + 'array_search' => $always_warn_third_not_strict, + ]; + } + + /** + * @return string - The name of the visitor that will be called (formerly analyzeNode) + * @override + */ + public static function getPostAnalyzeNodeVisitorClassName(): string + { + return StrictComparisonVisitor::class; + } +} + +/** + * Warns about using weak comparison operators when both sides are possibly objects + */ +class StrictComparisonVisitor extends PluginAwarePostAnalysisVisitor +{ + /** + * @param Node $node + * A node of kind ast\AST_BINARY_OP to analyze + * + * @override + */ + public function visitBinaryOp(Node $node): void + { + switch ($node->flags) { + case ast\flags\BINARY_IS_EQUAL: + case ast\flags\BINARY_IS_NOT_EQUAL: + if ($this->bothSidesArePossiblyObjects($node)) { + // TODO: Also check arrays of objects? + $this->emit( + StrictComparisonPlugin::ComparisonObjectEqualityNotStrict, + 'Saw a weak equality check on possible object types {TYPE} and {TYPE} in {CODE}', + [ + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['left']), + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['right']), + ASTReverter::toShortString($node), + ] + ); + } + break; + case ast\flags\BINARY_IS_GREATER_OR_EQUAL: + case ast\flags\BINARY_IS_SMALLER_OR_EQUAL: + case ast\flags\BINARY_IS_GREATER: + case ast\flags\BINARY_IS_SMALLER: + case ast\flags\BINARY_SPACESHIP: + if ($this->bothSidesArePossiblyObjects($node)) { + $this->emit( + StrictComparisonPlugin::ComparisonObjectOrdering, + 'Using comparison operator on possible object types {TYPE} and {TYPE} in {CODE}', + [ + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['left']), + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['right']), + ASTReverter::toShortString($node), + ] + ); + } + break; + } + } + + private function bothSidesArePossiblyObjects(Node $node): bool + { + ['left' => $left, 'right' => $right] = $node->children; + if (!($left instanceof Node) || !($right instanceof Node)) { + return false; + } + return UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left)->hasObjectTypes() && + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $right)->hasObjectTypes(); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new StrictComparisonPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/StrictLiteralComparisonPlugin.php b/bundled-libs/phan/phan/.phan/plugins/StrictLiteralComparisonPlugin.php new file mode 100644 index 000000000..10a2c1659 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/StrictLiteralComparisonPlugin.php @@ -0,0 +1,91 @@ +flags === ast\flags\BINARY_IS_NOT_EQUAL || $node->flags === ast\flags\BINARY_IS_EQUAL) { + $this->analyzeEqualityCheck($node); + } + } + + /** + * @param Node $node + * A node of kind ast\AST_BINARY_OP for `==`/`!=` to analyze + */ + private function analyzeEqualityCheck(Node $node): void + { + ['left' => $left, 'right' => $right] = $node->children; + $left_is_const = ParseVisitor::isConstExpr($left); + $right_is_const = ParseVisitor::isConstExpr($right); + if ($left_is_const === $right_is_const) { + return; + } + $const_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left_is_const ? $left : $right); + if ($const_type->isEmpty()) { + return; + } + foreach ($const_type->getTypeSet() as $type) { + if (!($type instanceof IntType || $type instanceof StringType)) { + return; + } + } + self::emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginComparisonNotStrictForScalar', + "Expected strict equality check when comparing {TYPE} to {TYPE} in {CODE}", + [ + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left), + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $right), + ASTReverter::toShortString($node), + ] + ); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new StrictLiteralComparisonPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/SuspiciousParamOrderPlugin.php b/bundled-libs/phan/phan/.phan/plugins/SuspiciousParamOrderPlugin.php new file mode 100644 index 000000000..57efaaf10 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/SuspiciousParamOrderPlugin.php @@ -0,0 +1,431 @@ +children['args']->children; + if (count($args) < 1) { + // Can't have a suspicious param order/position if there are no params + return; + } + $expression = $node->children['expr']; + try { + $function_list_generator = (new ContextNode( + $this->code_base, + $this->context, + $expression + ))->getFunctionFromNode(); + + foreach ($function_list_generator as $function) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $this->checkCall($function, $args, $node); + } + } catch (CodeBaseException $_) { + } + } + + /** + * @param Node|string|int|float|null $arg_node + */ + private static function extractName($arg_node): ?string + { + if (!$arg_node instanceof Node) { + return null; + } + switch ($arg_node->kind) { + case ast\AST_VAR: + $name = $arg_node->children['name']; + break; + /* + case ast\AST_CONST: + $name = $arg_node->children['name']->children['name']; + break; + */ + case ast\AST_PROP: + case ast\AST_STATIC_PROP: + $name = $arg_node->children['prop']; + break; + case ast\AST_METHOD_CALL: + case ast\AST_STATIC_CALL: + $name = $arg_node->children['method']; + break; + case ast\AST_CALL: + $name = $arg_node->children['expr']; + break; + default: + return null; + } + return is_string($name) ? $name : null; + } + + /** + * Returns a distance in the range 0..1, inclusive. + * + * A distance of 0 means they are similar (e.g. foo and getFoo()), + * and 1 means there are no letters in common (bar and foo) + */ + private static function computeDistance(string $a, string $b): float + { + $la = strlen($a); + $lb = strlen($b); + return (levenshtein($a, $b) - abs($la - $lb)) / max(1, min($la, $lb)); + } + + /** + * @param list $args + */ + private function checkCall(FunctionInterface $function, array $args, Node $node): void + { + $arg_names = []; + foreach ($args as $i => $arg_node) { + $name = self::extractName($arg_node); + if (!is_string($name)) { + continue; + } + $arg_names[$i] = strtolower($name); + } + if (count($arg_names) < 2) { + if (count($arg_names) === 1) { + $this->checkMovedArg($function, $args, $node, $arg_names); + } + return; + } + $parameters = $function->getParameterList(); + $parameter_names = []; + foreach ($arg_names as $i => $_) { + if (!isset($parameters[$i])) { + unset($arg_names[$i]); + continue; + } + $parameter_names[$i] = strtolower($parameters[$i]->getName()); + } + if (count($arg_names) < 2) { + // $arg_names and $parameter_names have the same keys + $this->checkMovedArg($function, $args, $node, $arg_names); + return; + } + $best_destination_map = []; + foreach ($arg_names as $i => $name) { + // To even be considered, the distance metric must be less than 60% (100% would have nothing in common) + $best_distance = min( + 0.6, + self::computeDistance($name, $parameter_names[$i]) + ); + $best_destination = null; + // echo "Distances for $name to $parameter_names[$i] is $best_distance\n"; + + foreach ($parameter_names as $j => $parameter_name_j) { + if ($j === $i) { + continue; + } + $d_swap_j = self::computeDistance($name, $parameter_name_j); + // echo "Distances for $name to $parameter_name_j is $d_swap_j\n"; + if ($d_swap_j < $best_distance) { + $best_destination = $j; + $best_distance = $d_swap_j; + } + } + if ($best_destination !== null) { + $best_destination_map[$i] = $best_destination; + } + } + if (count($best_destination_map) < 2) { + $this->checkMovedArg($function, $args, $node, $arg_names); + return; + } + $places_set = []; + foreach (self::findCycles($best_destination_map) as $cycle) { + // To reduce false positives, don't warn unless we know the parameter $j would be compatible with what was used at $i + foreach ($cycle as $array_index => $i) { + $j = $cycle[($array_index + 1) % count($cycle)]; + $type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $args[$i]); + // echo "Checking if $type can cast to $parameters[$j]\n"; + if (!$type->asExpandedTypes($this->code_base)->canCastToUnionType($parameters[$j]->getUnionType())) { + continue 2; + } + } + foreach ($cycle as $i) { + $places_set[$i] = true; + } + $arg_details = implode(' and ', array_map(static function (int $i) use ($args): string { + return self::extractName($args[$i]) ?? 'unknown'; + }, $cycle)); + $param_details = implode(' and ', array_map(static function (int $i) use ($parameters): string { + $param = $parameters[$i]; + return '#' . ($i + 1) . ' (' . trim($param->getUnionType() . ' $' . $param->getName()) . ')'; + }, $cycle)); + if ($function->isPHPInternal()) { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($node->lineno), + self::SuspiciousParamOrderInternal, + 'Suspicious order for arguments named {DETAILS} - These are being passed to parameters {DETAILS} of {FUNCTION}', + [ + $arg_details, + $param_details, + $function->getRepresentationForIssue(true), + ] + ); + } else { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($node->lineno), + self::SuspiciousParamOrder, + 'Suspicious order for arguments named {DETAILS} - These are being passed to parameters {DETAILS} of {FUNCTION} defined at {FILE}:{LINE}', + [ + $arg_details, + $param_details, + $function->getRepresentationForIssue(true), + $function->getContext()->getFile(), + $function->getContext()->getLineNumberStart(), + ] + ); + } + } + $this->checkMovedArg($function, $args, $node, $arg_names, $places_set); + } + + /** + * @param FunctionInterface $function the function being called + * @param list $args + * @param Node $node + * @param associative-array $arg_names + * @param associative-array $places_set the places that were already warned about being transposed. + */ + private function checkMovedArg(FunctionInterface $function, array $args, Node $node, array $arg_names, array $places_set = []): void + { + $real_parameters = $function->getRealParameterList(); + $parameters = $function->getParameterList(); + /** @var associative-array maps lowercase param names to their unique index, or null */ + $parameter_names = []; + foreach ($real_parameters as $i => $param) { + if (isset($places_set[$i])) { + continue; + } + $name_key = str_replace('_', '', strtolower($param->getName())); + if (array_key_exists($name_key, $parameter_names)) { + $parameter_names[$name_key] = null; + } else { + $parameter_names[$name_key] = $i; + } + } + foreach ($arg_names as $i => $name) { + $other_i = $parameter_names[str_replace('_', '', strtolower($name))] ?? null; + if ($other_i === null || $other_i === $i) { + continue; + } + $real_param = $real_parameters[$other_i]; + if ($real_param->isVariadic()) { + // Skip warning about signatures such as var_dump($var, ...$args) or array_unshift($values, $arg, $arg2) + // + // NOTE: For internal functions, some functions such as implode() have alternate signatures where the real parameter is in a different place, + // which is why this checks both $real_param and $param + // + // For user-defined functions, alternates are not supported. + continue; + } + $param = $parameters[$other_i] ?? null; + if ($param && $param->getName() === $real_param->getName()) { + if ($param->isVariadic()) { + continue; + } + $real_param = $param; + } + $real_param_details = '#' . ($other_i + 1) . ' (' . trim($real_param->getUnionType() . ' $' . $real_param->getName()) . ')'; + $arg_details = self::extractName($args[$i]) ?? 'unknown'; + if ($function->isPHPInternal()) { + $this->emitPluginIssue( + $this->code_base, + (clone($this->context))->withLineNumberStart($args[$i]->lineno ?? $node->lineno), + self::SuspiciousParamPositionInternal, + 'Suspicious order for argument {DETAILS} - This is getting passed to parameter {DETAILS} of {FUNCTION}', + [ + $arg_details, + $real_param_details, + $function->getRepresentationForIssue(true), + ] + ); + } else { + $this->emitPluginIssue( + $this->code_base, + clone($this->context)->withLineNumberStart($args[$i]->lineno ?? $node->lineno), + self::SuspiciousParamPosition, + 'Suspicious order for argument {DETAILS} - This is getting passed to parameter {DETAILS} of {FUNCTION} defined at {FILE}:{LINE}', + [ + $arg_details, + $real_param_details, + $function->getRepresentationForIssue(true), + $function->getContext()->getFile(), + $function->getContext()->getLineNumberStart(), + ] + ); + } + } + } + + /** + * @param list $values + * @return list the same values of the cycle, rearranged to start with the smallest value. + */ + private static function normalizeCycle(array $values, int $next): array + { + $pos = array_search($next, $values, true); + $values = array_slice($values, $pos ?: 0); + $min_pos = 0; + foreach ($values as $i => $value) { + if ($value < $values[$min_pos]) { + $min_pos = $values[$i]; + } + } + return array_merge(array_slice($values, $min_pos), array_slice($values, 0, $min_pos)); + } + + /** + * Given [1 => 2, 2 => 3, 3 => 1, 4 => 5, 5 => 6, 6 => 5]], return [[1,2,3],[5,6]] + * @param array $destination_map + * @return array> + */ + public static function findCycles(array $destination_map): array + { + $result = []; + while (count($destination_map) > 0) { + reset($destination_map); + $key = (int) key($destination_map); + $values = []; + while (count($destination_map) > 0) { + $values[] = $key; + $next = $destination_map[$key]; + unset($destination_map[$key]); + if (in_array($next, $values, true)) { + $values = self::normalizeCycle($values, $next); + if (count($values) >= 2) { + $result[] = $values; + } + $values = []; + break; + } + if (!isset($destination_map[$next])) { + break; + } + $key = $next; + } + } + return $result; + } + + /** + * @param Node $node a node of type AST_NULLSAFE_METHOD_CALL + * @override + */ + public function visitNullsafeMethodCall(Node $node): void + { + $this->visitMethodCall($node); + } + + /** + * @param Node $node a node of type AST_METHOD_CALL + * @override + */ + public function visitMethodCall(Node $node): void + { + $args = $node->children['args']->children; + if (count($args) < 1) { + // Can't have a suspicious param order/position if there are no params + return; + } + + $method_name = $node->children['method']; + + if (!\is_string($method_name)) { + return; + } + try { + $method = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getMethod($method_name, false, true); + } catch (Exception $_) { + return; + } + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $this->checkCall($method, $args, $node); + } + + /** + * @param Node $node a node of type AST_STATIC_CALL + * @override + */ + public function visitStaticCall(Node $node): void + { + $args = $node->children['args']->children; + if (count($args) < 1) { + // Can't have a suspicious param order/position if there are no params + return; + } + + $method_name = $node->children['method']; + + if (!\is_string($method_name)) { + return; + } + try { + $method = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getMethod($method_name, true, true); + } catch (Exception $_) { + return; + } + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $this->checkCall($method, $args, $node); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new SuspiciousParamOrderPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/UnknownClassElementAccessPlugin.php b/bundled-libs/phan/phan/.phan/plugins/UnknownClassElementAccessPlugin.php new file mode 100644 index 000000000..6eae37288 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/UnknownClassElementAccessPlugin.php @@ -0,0 +1,160 @@ +someMethod(null)` + * + * This file demonstrates plugins for Phan. Plugins hook into various events. + * UnknownClassElementAccessPlugin hooks into two events: + * + * - getPostAnalyzeNodeVisitorClassName + * This method returns a visitor that is called on every AST node from every + * file being analyzed in post-order + * - finalizeProcess + * This is called after the other forms of analysis are finished running. + * + * A plugin file must + * + * - Contain a class that inherits from \Phan\PluginV3 + * + * - End by returning an instance of that class. + * + * It is assumed without being checked that plugins aren't + * mangling state within the passed code base or context. + * + * Note: When adding new plugins, + * add them to the corresponding section of README.md + */ +class UnknownClassElementAccessPlugin extends PluginV3 implements + PostAnalyzeNodeCapability, + FinalizeProcessCapability +{ + public const UnknownObjectMethodCall = 'PhanPluginUnknownObjectMethodCall'; + /** + * @var array> + * Map from file name+line+node hash to the union type to a closure to emit the issue + */ + private static $deferred_unknown_method_issues = []; + + /** + * @var array + * Set of file name+line+node hashes where the union type is known. + */ + private static $known_method_set = []; + + /** + * @return class-string - name of PluginAwarePostAnalysisVisitor subclass + */ + public static function getPostAnalyzeNodeVisitorClassName(): string + { + return UnknownClassElementAccessVisitor::class; + } + + private static function generateKey(FileRef $context, int $lineno, string $node_string): string + { + // Sadly, the node can either be from the parse phase or any analysis phase, so we can't use spl_object_id. + return $context->getFile() . ':' . $lineno . ':' . sha1($node_string); + } + + /** + * Emit an issue if the object of the method call isn't found later/earlier + */ + public static function deferEmittingMethodIssue(Context $context, Node $node, UnionType $union_type): void + { + $node_string = ASTReverter::toShortString($node); + $key = self::generateKey($context, $node->lineno, $node_string); + if (isset(self::$known_method_set[$key])) { + return; + } + self::$deferred_unknown_method_issues[$key][] = [(clone $context)->withLineNumberStart($node->lineno), $node_string, $union_type]; + } + + /** + * Prevent this plugin from warning about $node_string at this file and line + */ + public static function blacklistMethodIssue(Context $context, Node $node): void + { + $node_string = ASTReverter::toShortString($node); + $key = self::generateKey($context, $node->lineno, $node_string); + self::$known_method_set[$key] = true; + unset(self::$deferred_unknown_method_issues[$key]); + } + + public function finalizeProcess(CodeBase $code_base): void + { + foreach (self::$deferred_unknown_method_issues as $issues) { + foreach ($issues as [$context, $node_string, $union_type]) { + $this->emitIssue( + $code_base, + $context, + self::UnknownObjectMethodCall, + 'Phan could not infer any class/interface types for the object of the method call {CODE} - inferred a type of {TYPE}', + [ + $node_string, + $union_type->isEmpty() ? '(empty union type)' : $union_type + ] + ); + } + } + } +} + +/** + * This visitor analyzes node kinds that can be the root of expressions + * containing duplicate expressions, and is called on nodes in post-order. + */ +class UnknownClassElementAccessVisitor extends PluginAwarePostAnalysisVisitor +{ + /** + * @param Node $node a node of kind ast\AST_NULLSAFE_METHOD_CALL, representing a call to an instance method + */ + public function visitNullsafeMethodCall(Node $node): void + { + $this->visitMethodCall($node); + } + + /** + * @param Node $node a node of kind ast\AST_METHOD_CALL, representing a call to an instance method + */ + public function visitMethodCall(Node $node): void + { + try { + // Fetch the list of valid classes, and warn about any undefined classes. + // (We have more specific issue types such as PhanNonClassMethodCall below, don't emit PhanTypeExpected*) + $union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']); + } catch (Exception $_) { + // Phan should already throw for this + return; + } + foreach ($union_type->getTypeSet() as $type) { + if ($type->isObjectWithKnownFQSEN()) { + UnknownClassElementAccessPlugin::blacklistMethodIssue($this->context, $node); + return; + } + } + if (Issue::shouldSuppressIssue($this->code_base, $this->context, UnknownClassElementAccessPlugin::UnknownObjectMethodCall, $node->lineno, [])) { + return; + } + UnknownClassElementAccessPlugin::deferEmittingMethodIssue($this->context, $node, $union_type); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new UnknownClassElementAccessPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/UnknownElementTypePlugin.php b/bundled-libs/phan/phan/.phan/plugins/UnknownElementTypePlugin.php new file mode 100644 index 000000000..dc7aa411d --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/UnknownElementTypePlugin.php @@ -0,0 +1,396 @@ + + */ + private $deferred_checks = []; + + /** + * Returns true for array, ?array, and array|null + */ + private static function isRegularArray(UnionType $type): bool + { + return $type->hasTypeMatchingCallback(static function (Type $type): bool { + return get_class($type) === ArrayType::class; + }) && !$type->hasTypeMatchingCallback(static function (Type $type): bool { + return get_class($type) !== ArrayType::class && !($type instanceof NullType); + }); + } + + /** + * @param CodeBase $code_base + * The code base in which the method exists + * + * @param Method $method + * A method being analyzed + * @override + */ + public function analyzeMethod( + CodeBase $code_base, + Method $method + ): void { + if ($method->getFQSEN() !== $method->getRealDefiningFQSEN()) { + return; + } + + $this->performChecks( + $method, + 'PhanPluginUnknownMethodReturnType', + 'Method {METHOD} has no declared or inferred return type', + 'PhanPluginUnknownArrayMethodReturnType', + 'Method {METHOD} has a return type of array, but does not specify any key types or value types' + ); + // NOTE: Placeholders can be found in \Phan\Issue::uncolored_format_string_for_replace + $warning_closures = []; + $inferred_types = []; + foreach ($method->getParameterList() as $i => $parameter) { + if ($parameter->getUnionType()->isEmpty()) { + $warning_closures[$i] = static function () use ($code_base, $parameter, $method, $i, &$inferred_types): void { + $suggestion = self::suggestionFromUnionType($inferred_types[$i] ?? null); + self::emitIssueAndSuggestion( + $code_base, + $parameter->createContext($method), + 'PhanPluginUnknownMethodParamType', + 'Method {METHOD} has no declared or inferred parameter type for ${PARAMETER}', + [(string)$method->getFQSEN(), $parameter->getName()], + $suggestion + ); + }; + } elseif (self::isRegularArray($parameter->getUnionType())) { + $warning_closures[$i] = static function () use ($code_base, $parameter, $method, $i, &$inferred_types): void { + $suggestion = self::suggestionFromUnionTypeNotRegularArray($inferred_types[$i] ?? null); + self::emitIssueAndSuggestion( + $code_base, + $parameter->createContext($method), + 'PhanPluginUnknownArrayMethodParamType', + 'Method {METHOD} has a parameter type of array for ${PARAMETER}, but does not specify any key types or value types', + [(string)$method->getFQSEN(), $parameter->getName()], + $suggestion + ); + }; + } + } + if (!$warning_closures) { + return; + } + $this->deferred_checks[$method->getFQSEN()->__toString()] = static function (CodeBase $_) use ($warning_closures): void { + foreach ($warning_closures as $cb) { + $cb(); + } + }; + $method->addFunctionCallAnalyzer( + /** + * @param list $args + */ + static function (CodeBase $code_base, Context $context, Method $unused_method, array $args, Node $unused_node) use ($warning_closures, &$inferred_types): void { + foreach ($warning_closures as $i => $_) { + $parameter = $args[$i] ?? null; + if ($parameter !== null) { + $parameter_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $parameter); + if ($parameter_type->isEmpty()) { + return; + } + $combined_type = $inferred_types[$i] ?? null; + if ($combined_type instanceof UnionType) { + $combined_type = $combined_type->withUnionType($parameter_type); + } else { + $combined_type = $parameter_type; + } + $inferred_types[$i] = $combined_type; + } + } + } + ); + } + + private static function suggestionFromUnionType(?UnionType $type): ?Suggestion + { + if (!$type || $type->isEmpty()) { + return null; + } + $type = $type->withFlattenedArrayShapeOrLiteralTypeInstances()->asNormalizedTypes(); + return Suggestion::fromString("Types inferred after analysis: $type"); + } + + private static function suggestionFromUnionTypeNotRegularArray(?UnionType $type): ?Suggestion + { + if (!$type || $type->isEmpty()) { + return null; + } + if (self::isRegularArray($type)) { + return null; + } + $type = $type->withFlattenedArrayShapeOrLiteralTypeInstances()->asNormalizedTypes(); + return Suggestion::fromString("Types inferred after analysis: $type"); + } + + private function performChecks( + AddressableElement $element, + string $issue_type_for_empty, + string $message_for_empty, + string $issue_type_for_unknown_array, + string $message_for_unknown_array + ): void { + $union_type = $element->getUnionType(); + if ($union_type->isEmpty()) { + $issue_type = $issue_type_for_empty; + $message = $message_for_empty; + } elseif (self::isRegularArray($union_type)) { + $issue_type = $issue_type_for_unknown_array; + $message = $message_for_unknown_array; + } else { + return; + } + $this->deferred_checks[$issue_type . ':' . $element->getFQSEN()->__toString()] = static function (CodeBase $code_base) use ($element, $issue_type, $message, $issue_type_for_unknown_array): void { + $new_union_type = $element->getUnionType(); + $suggestion = null; + if (!$new_union_type->isEmpty()) { + if ($issue_type !== $issue_type_for_unknown_array || !self::isRegularArray($new_union_type)) { + $suggestion = self::suggestionFromUnionType($new_union_type); + } + } + self::emitIssueAndSuggestion( + $code_base, + $element->getContext(), + $issue_type, + $message, + [$element->getRepresentationForIssue()], + $suggestion + ); + }; + } + + /** + * @param list $args + */ + private static function emitIssueAndSuggestion( + CodeBase $code_base, + Context $context, + string $issue_type, + string $message, + array $args, + ?Suggestion $suggestion + ): void { + self::emitIssue( + $code_base, + $context, + $issue_type, + $message, + $args, + Issue::SEVERITY_NORMAL, + Issue::REMEDIATION_B, + Issue::TYPE_ID_UNKNOWN, + $suggestion + ); + } + + + /** + * @param CodeBase $code_base + * The code base in which the function exists + * + * @param Func $function + * A function being analyzed + * @override + */ + public function analyzeFunction( + CodeBase $code_base, + Func $function + ): void { + // NOTE: Placeholders can be found in \Phan\Issue::uncolored_format_string_for_replace + if ($function->getUnionType()->isEmpty()) { + if ($function->getFQSEN()->isClosure()) { + $issue = 'PhanPluginUnknownClosureReturnType'; + $message = 'Closure {FUNCTION} has no declared or inferred return type'; + } else { + $issue = 'PhanPluginUnknownFunctionReturnType'; + $message = 'Function {FUNCTION} has no declared or inferred return type'; + } + $this->deferred_checks[$issue . ':' . $function->getFQSEN()->__toString()] = static function (CodeBase $code_base) use ($function, $issue, $message): void { + $new_union_type = $function->getUnionType(); + $suggestion = self::suggestionFromUnionType($new_union_type); + self::emitIssue( + $code_base, + $function->getContext(), + $issue, + $message, + [$function->getRepresentationForIssue()], + Issue::SEVERITY_NORMAL, + Issue::REMEDIATION_B, + Issue::TYPE_ID_UNKNOWN, + $suggestion + ); + }; + } elseif (self::isRegularArray($function->getUnionType())) { + if ($function->getFQSEN()->isClosure()) { + $issue = 'PhanPluginUnknownArrayClosureReturnType'; + $message = 'Closure {FUNCTION} has a return type of array, but does not specify key or value types'; + } else { + $issue = 'PhanPluginUnknownArrayFunctionReturnType'; + $message = 'Function {FUNCTION} has a return type of array, but does not specify key or value types'; + } + $this->deferred_checks[$issue . ':' . $function->getFQSEN()->__toString()] = static function (CodeBase $code_base) use ($function, $issue, $message): void { + $new_union_type = $function->getUnionType(); + $suggestion = self::suggestionFromUnionTypeNotRegularArray($new_union_type); + self::emitIssue( + $code_base, + $function->getContext(), + $issue, + $message, + [$function->getRepresentationForIssue()], + Issue::SEVERITY_NORMAL, + Issue::REMEDIATION_B, + Issue::TYPE_ID_UNKNOWN, + $suggestion + ); + }; + } + $warning_closures = []; + $inferred_types = []; + foreach ($function->getParameterList() as $i => $parameter) { + if ($parameter->getUnionType()->isEmpty()) { + if ($function->getFQSEN()->isClosure()) { + $issue = 'PhanPluginUnknownClosureParamType'; + $message = 'Closure {FUNCTION} has no declared or inferred return type for ${PARAMETER}'; + } else { + $issue = 'PhanPluginUnknownFunctionParamType'; + $message = 'Function {FUNCTION} has no declared or inferred return type for ${PARAMETER}'; + } + $warning_closures[$i] = static function () use ($code_base, $issue, $message, $parameter, $function, $i, &$inferred_types): void { + $suggestion = self::suggestionFromUnionType($inferred_types[$i] ?? null); + self::emitIssueAndSuggestion( + $code_base, + $parameter->createContext($function), + $issue, + $message, + [$function->getNameForIssue(), $parameter->getName()], + $suggestion + ); + }; + } elseif (self::isRegularArray($parameter->getUnionType())) { + if ($function->getFQSEN()->isClosure()) { + $issue = 'PhanPluginUnknownArrayClosureParamType'; + $message = 'Closure {FUNCTION} has a parameter type of array for ${PARAMETER}, but does not specify any key types or value types'; + } else { + $issue = 'PhanPluginUnknownArrayFunctionParamType'; + $message = 'Function {FUNCTION} has a parameter type of array for ${PARAMETER}, but does not specify any key types or value types'; + } + $warning_closures[$i] = static function () use ($code_base, $issue, $message, $parameter, $function, $i, &$inferred_types): void { + $suggestion = self::suggestionFromUnionType($inferred_types[$i] ?? null); + self::emitIssueAndSuggestion( + $code_base, + $parameter->createContext($function), + $issue, + $message, + [$function->getNameForIssue(), $parameter->getName()], + $suggestion + ); + }; + } + } + if (!$warning_closures) { + return; + } + $this->deferred_checks[$function->getFQSEN()->__toString()] = static function (CodeBase $_) use ($warning_closures): void { + foreach ($warning_closures as $cb) { + $cb(); + } + }; + $function->addFunctionCallAnalyzer( + /** + * @param list $args + */ + static function (CodeBase $code_base, Context $context, Func $unused_function, array $args, Node $unused_node) use ($warning_closures, &$inferred_types): void { + foreach ($warning_closures as $i => $_) { + $parameter = $args[$i] ?? null; + if ($parameter !== null) { + $parameter_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $parameter); + if ($parameter_type->isEmpty()) { + return; + } + $combined_type = $inferred_types[$i] ?? null; + if ($combined_type instanceof UnionType) { + $combined_type = $combined_type->withUnionType($parameter_type); + } else { + $combined_type = $parameter_type; + } + $inferred_types[$i] = $combined_type; + } + } + } + ); + } + + /** + * @param CodeBase $code_base @unused-param + * The code base in which the property exists + * + * @param Property $property + * A property being analyzed + * @override + */ + public function analyzeProperty( + CodeBase $code_base, + Property $property + ): void { + if ($property->getFQSEN() !== $property->getRealDefiningFQSEN()) { + return; + } + $this->performChecks( + $property, + 'PhanPluginUnknownPropertyType', + 'Property {PROPERTY} has an initial type that cannot be inferred', + 'PhanPluginUnknownArrayPropertyType', + 'Property {PROPERTY} has an array type, but does not specify any key types or value types' + ); + } + + public function finalizeProcess(CodeBase $code_base): void + { + try { + foreach ($this->deferred_checks as $check) { + $check($code_base); + } + } finally { + // There were errors in unit tests if this wasn't cleared. + $this->deferred_checks = []; + } + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new UnknownElementTypePlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/UnreachableCodePlugin.php b/bundled-libs/phan/phan/.phan/plugins/UnreachableCodePlugin.php new file mode 100644 index 000000000..2932f67ab --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/UnreachableCodePlugin.php @@ -0,0 +1,118 @@ + true, + \ast\AST_FUNC_DECL => true, + \ast\AST_CONST => true, + ]; + + /** + * @param Node $node + * A node to analyze + * @override + */ + public function visitStmtList(Node $node): void + { + $child_nodes = $node->children; + + $last_node_index = count($child_nodes) - 1; + foreach ($child_nodes as $i => $node) { + if (!\is_int($i)) { + throw new AssertionError("Expected integer index"); + } + if ($i >= $last_node_index) { + break; + } + if (!($node instanceof Node)) { + continue; + } + if (!BlockExitStatusChecker::willUnconditionallySkipRemainingStatements($node)) { + continue; + } + // Skip over empty statements and scalar statements. + for ($j = $i + 1; array_key_exists($j, $child_nodes); $j++) { + $next_node = $child_nodes[$j]; + if (!($next_node instanceof Node && $next_node->lineno > 0)) { + continue; + } + if (array_key_exists($next_node->kind, self::DECL_KIND_SET)) { + if ($this->context->isInGlobalScope()) { + continue; + } + } + $context = clone($this->context)->withLineNumberStart($next_node->lineno); + if ($this->context->isInFunctionLikeScope()) { + if ($this->context->getFunctionLikeInScope($this->code_base)->checkHasSuppressIssueAndIncrementCount('PhanPluginUnreachableCode')) { + // don't emit the below issue. + break; + } + } + $this->emitPluginIssue( + $this->code_base, + $context, + 'PhanPluginUnreachableCode', + 'Unreachable statement detected', + [] + ); + break; + } + break; + } + } +} +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new UnreachableCodePlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/UnsafeCodePlugin.php b/bundled-libs/phan/phan/.phan/plugins/UnsafeCodePlugin.php new file mode 100644 index 000000000..84f857054 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/UnsafeCodePlugin.php @@ -0,0 +1,106 @@ +flags !== ast\flags\EXEC_EVAL) { + return; + } + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginUnsafeEval', + 'eval() is often unsafe and may have better alternatives such as closures and is unanalyzable. Suppress this issue if you are confident that input is properly escaped for this use case and there is no better way to do this.', + [] + ); + } + + /** + * @param Node $node a + * A node of kind ast\AST_SHELL_EXEC to analyze + * @override + */ + public function visitShellExec(Node $node): void + { + if (!ParseVisitor::isConstExpr($node->children['expr'])) { + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginUnsafeShellExecDynamic', + 'This syntax for shell_exec() ({CODE}) is easily confused for a string and does not allow proper exit code/stderr handling, and is used with a non-constant. Consider proc_open() instead.', + [ASTReverter::toShortString($node)] + ); + return; + } + $this->emitPluginIssue( + $this->code_base, + $this->context, + 'PhanPluginUnsafeShellExec', + 'This syntax for shell_exec() ({CODE}) is easily confused for a string and does not allow proper exit code/stderr handling. Consider proc_open() instead.', + [ASTReverter::toShortString($node)] + ); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new UnsafeCodePlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/UnusedSuppressionPlugin.php b/bundled-libs/phan/phan/.phan/plugins/UnusedSuppressionPlugin.php new file mode 100644 index 000000000..1880ecfea --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/UnusedSuppressionPlugin.php @@ -0,0 +1,312 @@ +analyze*()` were called, + * which is why those methods postpone the check until analysis is finished. + * + * Also, looping over all elements again would be slow. + * + * These are currently unique, even when quick_mode is false. + */ + private $elements_for_postponed_analysis = []; + + /** + * @var string[] a list of files where checks for unused suppressions was postponed + * (Because of non-quick mode, we may emit issues in a file after analysis has run on that file) + */ + private $files_for_postponed_analysis = []; + + /** + * @var array>>> stores the suppressions for active plugins + * maps plugin class to + * file name to + * issue type to + * unique list of line numbers of suppressions + */ + private $plugin_active_suppression_list; + + /** + * @param CodeBase $code_base + * The code base in which the element exists + * + * @param AddressableElement $element + * Any element such as function, method, class + * (which has an FQSEN) + */ + private static function analyzeAddressableElement( + CodeBase $code_base, + AddressableElement $element + ): void { + // Get the set of suppressed issues on the element + $suppress_issue_list = + $element->getSuppressIssueList(); + + if (\array_key_exists('UnusedSuppression', $suppress_issue_list)) { + // The element's doc comment is suppressing everything emitted by this plugin. + return; + } + + // Check to see if any are unused + foreach ($suppress_issue_list as $issue_type => $use_count) { + if (0 !== $use_count) { + continue; + } + if (in_array($issue_type, self::getUnusedSuppressionIgnoreList(), true)) { + continue; + } + self::emitIssue( + $code_base, + $element->getContext(), + 'UnusedSuppression', + "Element {FUNCTIONLIKE} suppresses issue {ISSUETYPE} but does not use it", + [(string)$element->getFQSEN(), $issue_type] + ); + } + } + + private function postponeAnalysisOfElement(AddressableElement $element): void + { + if (count($element->getSuppressIssueList()) === 0) { + // There are no suppressions, so there's no reason to check this + return; + } + $this->elements_for_postponed_analysis[] = $element; + } + + /** + * @param CodeBase $code_base @unused-param + * The code base in which the class exists + * + * @param Clazz $class + * A class being analyzed + * @override + */ + public function analyzeClass( + CodeBase $code_base, + Clazz $class + ): void { + $this->postponeAnalysisOfElement($class); + } + + /** + * @param CodeBase $code_base @unused-param + * The code base in which the method exists + * + * @param Method $method + * A method being analyzed + * @override + */ + public function analyzeMethod( + CodeBase $code_base, + Method $method + ): void { + + // Ignore methods inherited by subclasses + if ($method->getFQSEN() !== $method->getRealDefiningFQSEN()) { + return; + } + + $this->postponeAnalysisOfElement($method); + } + + /** + * @param CodeBase $code_base @unused-param + * The code base in which the function exists + * + * @param Func $function + * A function being analyzed + * @override + */ + public function analyzeFunction( + CodeBase $code_base, + Func $function + ): void { + $this->postponeAnalysisOfElement($function); + } + + /** + * @param CodeBase $code_base @unused-param + * The code base in which the property exists + * + * @param Property $property + * A property being analyzed + * @override + */ + public function analyzeProperty( + CodeBase $code_base, + Property $property + ): void { + if ($property->getFQSEN() !== $property->getRealDefiningFQSEN()) { + return; + } + $this->elements_for_postponed_analysis[] = $property; + } + + /** + * NOTE! This plugin only produces correct results when Phan + * is run on a single processor (via the `-j1` flag). + * Putting this hook in finalizeProcess() just minimizes the incorrect result counts. + * @override + */ + public function finalizeProcess(CodeBase $code_base): void + { + foreach ($this->elements_for_postponed_analysis as $element) { + self::analyzeAddressableElement($code_base, $element); + } + $this->analyzePluginSuppressions($code_base); + } + + private function analyzePluginSuppressions(CodeBase $code_base): void + { + $suppression_plugin_set = ConfigPluginSet::instance()->getSuppressionPluginSet(); + if (count($suppression_plugin_set) === 0) { + return; + } + + foreach ($this->files_for_postponed_analysis as $file_path) { + foreach ($suppression_plugin_set as $plugin) { + $this->analyzePluginSuppressionsForFile($code_base, $plugin, $file_path); + } + } + } + + /** + * @return list + */ + private static function getUnusedSuppressionIgnoreList(): array + { + return Config::getValue('plugin_config')['unused_suppression_ignore_list'] ?? []; + } + + private static function getReportOnlyWhitelisted(): bool + { + return Config::getValue('plugin_config')['unused_suppression_whitelisted_only'] ?? false; + } + + private static function shouldReportUnusedSuppression(string $issue_type): bool + { + $ignore_list = self::getUnusedSuppressionIgnoreList(); + $only_whitelisted = self::getReportOnlyWhitelisted(); + $issue_whitelist = Config::getValue('whitelist_issue_types') ?? []; + + return !in_array($issue_type, $ignore_list, true) && + (!$only_whitelisted || in_array($issue_type, $issue_whitelist, true)); + } + + private function analyzePluginSuppressionsForFile(CodeBase $code_base, SuppressionCapability $plugin, string $relative_file_path): void + { + $absolute_file_path = Config::projectPath($relative_file_path); + $plugin_class = \get_class($plugin); + $name_pos = \strrpos($plugin_class, '\\'); + if ($name_pos !== false) { + $plugin_name = \substr($plugin_class, $name_pos + 1); + } else { + $plugin_name = $plugin_class; + } + $plugin_suppressions = $plugin->getIssueSuppressionList($code_base, $absolute_file_path); + $plugin_successful_suppressions = $this->plugin_active_suppression_list[$plugin_class][$absolute_file_path] ?? null; + + foreach ($plugin_suppressions as $issue_type => $line_list) { + foreach ($line_list as $lineno => $lineno_of_comment) { + if (isset($plugin_successful_suppressions[$issue_type][$lineno])) { + continue; + } + // TODO: finish letting plugins suppress UnusedSuppression on other plugins + $issue_kind = 'UnusedPluginSuppression'; + $message = 'Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} on this line but this suppression is unused or suppressed elsewhere'; + if ($lineno === 0) { + $issue_kind = 'UnusedPluginFileSuppression'; + $message = 'Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} in this file but this suppression is unused or suppressed elsewhere'; + } + if (isset($plugin_suppressions['UnusedSuppression'][$lineno_of_comment])) { + continue; + } + if (isset($plugin_suppressions[$issue_kind][$lineno_of_comment])) { + continue; + } + if (!self::shouldReportUnusedSuppression($issue_type)) { + continue; + } + self::emitIssue( + $code_base, + (new Context())->withFile($relative_file_path)->withLineNumberStart($lineno_of_comment), + $issue_kind, + $message, + [$plugin_name, $issue_type] + ); + } + } + return; + } + + /** + * @unused-param $code_base + * @unused-param $file_contents + * @unused-param $node + */ + public function beforeAnalyzeFile( + CodeBase $code_base, + Context $context, + string $file_contents, + Node $node + ): void { + $file = $context->getFile(); + $this->files_for_postponed_analysis[$file] = $file; + } + + /** + * Record the fact that $plugin caused suppressions in $file_path for issue $issue_type due to an annotation around $line + * @internal + */ + public function recordPluginSuppression( + SuppressionCapability $plugin, + string $file_path, + string $issue_type, + int $line + ): void { + $file_name = Config::projectPath($file_path); + $plugin_class = \get_class($plugin); + $this->plugin_active_suppression_list[$plugin_class][$file_name][$issue_type][$line] = $line; + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new UnusedSuppressionPlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/UseReturnValuePlugin.php b/bundled-libs/phan/phan/.phan/plugins/UseReturnValuePlugin.php new file mode 100644 index 000000000..43cac72ef --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/UseReturnValuePlugin.php @@ -0,0 +1,11 @@ +withLineNumberStart(self::calculateLine($file_contents, $newline_position)), + self::CarriageReturn, + 'The first occurrence of a carriage return ("\r") was seen here. Running "dos2unix" can fix that.' + ); + } + $tab_position = strpos($file_contents, "\t"); + if ($tab_position !== false) { + self::emitIssue( + $code_base, + clone($context)->withLineNumberStart(self::calculateLine($file_contents, $tab_position)), + self::Tab, + 'The first occurrence of a tab was seen here. Running "expand" can fix that.' + ); + } + if (preg_match('/[ \t]\r?$/mS', $file_contents, $match, PREG_OFFSET_CAPTURE)) { + self::emitIssue( + $code_base, + clone($context)->withLineNumberStart(self::calculateLine($file_contents, $match[0][1])), + self::WhitespaceTrailing, + 'The first occurrence of trailing whitespace was seen here.' + ); + } + } + + /** + * @return array + */ + public function getAutomaticFixers(): array + { + return require(__DIR__ . '/WhitespacePlugin/fixers.php'); + } +} + +// Every plugin needs to return an instance of itself at the +// end of the file in which it's defined. +return new WhitespacePlugin(); diff --git a/bundled-libs/phan/phan/.phan/plugins/WhitespacePlugin/fixers.php b/bundled-libs/phan/phan/.phan/plugins/WhitespacePlugin/fixers.php new file mode 100644 index 000000000..e9291b3d8 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/plugins/WhitespacePlugin/fixers.php @@ -0,0 +1,119 @@ + static function (CodeBase $unused_code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet { + $spaces_per_tab = (int)(Config::getValue('plugin_config')['spaces_per_tab'] ?? 4); + if ($spaces_per_tab <= 0) { + $spaces_per_tab = 4; + } + + /** + * @return Generator + */ + $compute_edits = static function (string $line_contents, int $byte_offset) use ($spaces_per_tab): Generator { + preg_match_all('/\t+/', $line_contents, $matches, PREG_OFFSET_CAPTURE); + + $effective_space_count = 0; + $prev_end = 0; // byte offset of previous end of tab sequences + // run the equivalent of unix's 'unexpand' + foreach ($matches[0] as $match) { + $column = $match[1]; // 0-based column + $effective_space_count += $column - $prev_end; + $len = strlen($match[0]); + + $prev_end = $column + $len; + + $replacement_space_count = ($len - 1) * $spaces_per_tab + ($spaces_per_tab - ($effective_space_count % $spaces_per_tab)); + + $start = $byte_offset + $match[1]; + yield new FileEdit($start, $start + $len, str_repeat(' ', $replacement_space_count)); + } + }; + + IssueFixer::debug("Calling tab fixer for {$instance->getFile()}\n"); + $raw_contents = $contents->getContents(); + $byte_offset = 0; + $edits = []; + foreach (explode("\n", $raw_contents) as $line_contents) { + if (strpos($line_contents, "\t") !== false) { + foreach ($compute_edits(rtrim($line_contents), $byte_offset) as $edit) { + $edits[] = $edit; + } + } + $byte_offset += strlen($line_contents) + 1; + } + if (!$edits) { + return null; + } + IssueFixer::debug("Resulting edits for tab fixes: " . json_encode($edits) . "\n"); + //$line = $instance->getLine(); + return new FileEditSet($edits); + }, + /** + * @return ?FileEditSet + */ + WhitespacePlugin::WhitespaceTrailing => static function (CodeBase $unused_code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet { + IssueFixer::debug("Calling trailing whitespace fixer {$instance->getFile()}\n"); + $raw_contents = $contents->getContents(); + $byte_offset = 0; + $edits = []; + foreach (explode("\n", $raw_contents) as $line_contents) { + $new_byte_offset = $byte_offset + strlen($line_contents) + 1; + $line_contents = rtrim($line_contents, "\r"); + if (preg_match('/\s+$/D', $line_contents, $matches)) { + $len = strlen($matches[0]); + $offset = $byte_offset + strlen($line_contents) - $len; + // Remove 1 or more bytes of trailing whitespace from each line + $edits[] = new FileEdit($offset, $offset + $len); + } + $byte_offset = $new_byte_offset; + } + if (!$edits) { + return null; + } + IssueFixer::debug("Resulting edits for trailing whitespace: " . json_encode($edits) . "\n"); + //$line = $instance->getLine(); + return new FileEditSet($edits); + }, + + /** + * @return ?FileEditSet + */ + WhitespacePlugin::CarriageReturn => static function (CodeBase $unused_code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet { + IssueFixer::debug("Calling trailing whitespace fixer {$instance->getFile()}\n"); + $raw_contents = $contents->getContents(); + $byte_offset = 0; + $edits = []; + foreach (explode("\n", $raw_contents) as $line_contents) { + if (substr($line_contents, -1) === "\r") { + $offset = $byte_offset + strlen($line_contents) - 1; + // Remove the byte with the carriage return + $edits[] = new FileEdit($offset, $offset + 1); + } + $byte_offset += strlen($line_contents) + 1; + } + if (!$edits) { + return null; + } + IssueFixer::debug("Resulting edits for trailing whitespace: " . json_encode($edits) . "\n"); + //$line = $instance->getLine(); + return new FileEditSet($edits); + }, +]; diff --git a/bundled-libs/phan/phan/.phan/stubs/README.md b/bundled-libs/phan/phan/.phan/stubs/README.md new file mode 100644 index 000000000..8e7a4c9fc --- /dev/null +++ b/bundled-libs/phan/phan/.phan/stubs/README.md @@ -0,0 +1,2 @@ +Add any stubs to this directory for code that you don't want to parse, but still want +to expose to phan while analyzing the phan codebase diff --git a/bundled-libs/phan/phan/.phan/stubs/mbstring.phan_php b/bundled-libs/phan/phan/.phan/stubs/mbstring.phan_php new file mode 100644 index 000000000..0a3856606 --- /dev/null +++ b/bundled-libs/phan/phan/.phan/stubs/mbstring.phan_php @@ -0,0 +1,89 @@ + +As a maintainer of Phan, I agree to the [Developer Certificate of Origin](https://github.com/phan/phan/blob/38bf1fd15e39bc668084accb8caab21f09ff75ba/DCO.txt). + +**Rasmus Lerdorf**
+As a maintainer of Phan, I agree to the [Developer Certificate of Origin](https://github.com/phan/phan/blob/38bf1fd15e39bc668084accb8caab21f09ff75ba/DCO.txt). + +**Tyson Andre**
+As a maintainer of Phan, I agree to the [Developer Certificate of Origin](https://github.com/phan/phan/blob/38bf1fd15e39bc668084accb8caab21f09ff75ba/DCO.txt). diff --git a/bundled-libs/phan/phan/NEWS.md b/bundled-libs/phan/phan/NEWS.md new file mode 100644 index 000000000..ef0ad300f --- /dev/null +++ b/bundled-libs/phan/phan/NEWS.md @@ -0,0 +1,4015 @@ +Phan NEWS + +Dec 31 2020, Phan 3.2.10 +------------------------ + +Phan 4 is out (requires php-ast 1.0.7+ to run), +and that release line will contain all of Phan's new features, bug fixes, and crash fixes. + +Maintenance: + ++ Recommend using Phan 4 when analyzing code as a line printed to `STDERR`. (#4189) ++ Mention that Phan 4 has been released in `--help`, `--version`, and crash reports. (#4189) + The environment variable `PHAN_SUPPRESS_PHP_UPGRADE_NOTICE=1` can be set to disable this notice. ++ Warn if attempting to execute Phan 3.x with PHP 8.1-dev or newer (A future release of Phan 4+ will fully support 8.1) + PHP 8.1 may contain changes to syntax that are unsupported by Phan 3 or the native/polyfill parsers. + +Bug fixes ++ Fix false positive PhanPossiblyFalseTypeReturn with strict type checking for substr when target php version is 8.0+ (#4335) + +Dec 26 2020, Phan 3.2.9 +----------------------- + +Bug fixes: ++ Fix a few parameter names for issue messages (#4316) ++ Fix bug that could cause Phan not to warn about `SomeClassWithoutConstruct::__construct` + in some edge cases. (#4323) ++ Properly infer `self` is referring to the current object context even when the object context is unknown in namespaces. (#4070) + +Deprecations: ++ Emit a deprecation notice when running this in PHP 7 and php-ast < 1.0.7. (#4189) + This can be suppressed by setting the environment variable `PHAN_SUPPRESS_AST_DEPRECATION=1`. + +Dec 23 2020, Phan 3.2.8 +----------------------- + +Bug fixes: ++ Fix false positive PhanUnusedVariable for variable redefined in loop (#4301) ++ Fix handling of `-z`/`--signature-compatibility` - that option now enables `analyze_signature_compatibility` instead of disabling it. (#4303) ++ Fix possible `PhanCoalescingNeverUndefined` for variable defined in catch block (#4305) ++ Don't emit `PhanCompatibleConstructorPropertyPromotion` when `minimum_target_php_version` is 8.0 or newer. (#4307) ++ Infer that PHP 8.0 constructor property promotion's properties have write references. (#4308) + They are written to by the constructor. ++ Inherit phpdoc parameter types for the property declaration in php 8.0 constructor property promotion (#4311) + +Dec 13 2020, Phan 3.2.7 +----------------------- + +New features (Analysis): ++ Update real parameter names to match php 8.0's parameter names for php's own internal methods (including variadics and those with multiple signatures). (#4263) + Update real parameter names, types, and return types for some PECL extensions. ++ Raise the severity of some php 8.0 incompatibility issues to critical. ++ Fix handling of references after renaming variadic reference parameters of `fscanf`/`scanf`/`mb_convert_variables` ++ Mention if PhanUndeclaredFunction is potentially caused by the target php version being too old. (#4230) ++ Improve real type inference for conditionals on literal types (#4288) ++ Change the way the real type set of array access is inferred for mixes of array shapes and arrays (#4296) ++ Emit `PhanSuspiciousNamedArgumentVariadicInternal` when using named arguments with variadic parameters of internal functions that are + not among the few reflection functions known to support named arguments. (#4284) ++ Don't suggest instance properties as alternatives to undefined variables inside of static methods. + +Bug fixes: ++ Support a `non-null-mixed` type and change the way analysis involving nullability is checked for `mixed` (phpdoc and real). (#4278, #4276) + +Nov 27 2020, Phan 3.2.6 +----------------------- + +New features (Analysis): ++ Update many more real parameter names to match php 8.0's parameter names for php's own internal methods. (#4263) ++ Infer that an instance property exists for PHP 8.0 constructor property promotion. (#3938) ++ Infer types of properties from arguments passed into constructor for PHP 8.0 constructor property promotion. (#3938) ++ Emit `PhanInvalidNode` and `PhanRedefineProperty` when misusing syntax for constructor property promotion. (#3938) ++ Emit `PhanCompatibleConstructorPropertyPromotion` when constructor property promotion is used. (#3938) ++ Emit `PhanSuspiciousMagicConstant` when using `__FUNCTION__` inside of a closure. (#4222) + +Nov 26 2020, Phan 3.2.5 +----------------------- + +New features (Analysis): ++ Convert more internal function signature types from resource to the new object types with `target_php_version` of `8.0`+ (#4245, #4246) ++ Make internal function signature types and counts consistent with PHP 8.0's `.php.stub` files used to generate some reflection information. + +Bug fixes ++ Fix logic error inferring the real key type of lists and arrays + and infer that the real union type of arrays is `array` + when all keys have real type int. (#4251) ++ Fix rendering of processed item count in `--long-progress-bar`. + +Miscellaneous: ++ Rename OCI-Collection and OCI-Lob to OCICollection and OCILob internally to prepare for php 8 support. + (Previously `OCI_Collection` and `OCI_Lob` were used to be valid fqsens internally) + +Nov 12 2020, Phan 3.2.4 +----------------------- + +New features (Analysis): ++ Partially support `self` and `static` in phpdoc types. (#4226) + This support is incomplete and may run into issues with inheritance. + +Bug fixes: ++ Properly infer the literal string value of `__FUNCTION__` for global functions in namespaces (#4231) ++ Fix false positive `PhanPossiblyInfiniteLoop` for `do {} while (false);` that is unchangeably false (#4236) ++ Infer that array_shift and array_pop return null when the passed in array could be empty, not false. (#4239) ++ Handle `PhpToken::getAll()` getting renamed to `PhpToken::tokenize()` in PHP 8.0.0RC4. (#4189) + +Oct 12 2020, Phan 3.2.3 +----------------------- + +New features (CLI, Config): ++ Add `light_high_contrast` support for `--color-scheme`. (#4203) + This may be useful in terminals or CI web pages that use white backgrounds. + +New features (Analysis): ++ Infer that `parent::someMethodReturningStaticType()` is a subtype of the current class, not just the parent class. (#4202) ++ Support phpdoc `@abstract` or `@phan-abstract` on non-abstract class constants, properties, and methods + to indicate that the intent is for non-abstract subclasses to override the definition. (#2278, #2285) + New issue types: `PhanCommentAbstractOnInheritedConstant`, `PhanCommentAbstractOnInheritedProperty`, `PhanCommentOverrideOnNonOverrideProperty` + + For example, code using `static::SOME_CONST` or `static::$SOME_PROPERTY` or `$this->someMethod()` + may declare a placeholder `@abstract` constant/property/method, + and use this annotation to ensure that all non-abstract subclasses override the constant/property/method + (if using real abstract methods is not practical for a use case) ++ Warn about `@override` on properties that do not override an ancestor's property definition. + New issue type: `PhanCommentOverrideOnNonOverrideProperty`. + (Phan already warns for constants and methods) + +Plugins: ++ Emit `PhanPluginUseReturnValueGenerator` for calling a function returning a generator without using the returned Generator. (#4013) + +Bug fixes: ++ Properly analyze the right hand side for `$cond || throw ...;` (e.g. emit `PhanCompatibleThrowException`) (#4199) ++ Don't infer implications of `left || right` on the right hand expression when the right hand side has no side effects. (#4199) ++ Emit `PhanTypeInvalidThrowStatementNonThrowable` for thrown expressions that definitely aren't `\Throwable` + even when `warn_about_undocumented_throw_statements` is disabled or the throw expression is in the top level scope. (#4200) ++ Increase the minimum requirements in composer.json to what Phan actually requires. (#4217) + +Sep 19 2020, Phan 3.2.2 +----------------------- + +New features (Analysis): ++ Improve handling of missing argument info when analyzing calls to functions/methods. + This will result in better issue detection for inherited methods or methods which Phan does not have type info for. + +Bug fixes: ++ Fix false positive `PhanUnusedVariable` in `for (; $loop; ...) {}` (#4191) ++ Don't infer defaults of ancestor class properties when analyzing the implementation of `__construct`. (#4195) + This is only affects projects where the config setting `infer_default_properties_in_construct` is overridden to be enabled. ++ Check `minimum_target_php_version` for more compatibility warnings about parameter types. + +Sep 13 2020, Phan 3.2.1 +----------------------- + +New features (Analysis): ++ Don't compare parameter types against alternate method signatures which have too many required parameters. + (e.g. warn about `max([])` but not `max([], [1])`) ++ Support `/** @unused-param $param_name */` in doc comments as an additional way to support suppressing warnings about individual parameters being unused. ++ Warn about loop conditions that potentially don't change due to the body of the loop. + This check uses heuristics and is prone to false positives. + New issue types: `PhanPossiblyInfiniteLoop` ++ Treat `unset($x);` as shadowing variable definitions during dead code detection. ++ Change the way `$i++`, `--$i`, etc. are analyzed during dead code detection ++ Properly enable `allow_method_param_type_widening` by default when the inferred `minimum_target_php_version` is `'7.2'` or newer. (#4168) ++ Start preparing for switching to AST version 80 in an upcoming Phan 4 release. (#4167)` + +Bug fixes: ++ Fix various crashes in edge cases. ++ Fix crash with adjacent named labels for gotos. ++ Fix false positive unused parameter warning with php 8.0 constructor property promotion. + +Plugins: ++ Warn about `#` comments in `PHPDocInWrongCommentPlugin` if they're not used for the expected `#[` syntax of php 8.0 attributes. + +Maintenance: ++ Update polyfill/fallback parser to properly skip attributes in php 8.0. + The upcoming Phan 4 release will support analyzing attributes, which requires AST version 80. + +Aug 25 2020, Phan 3.2.0 +----------------------- + +New features (CLI, Config): ++ **Add the `minimum_target_php_version` config setting and `--minimum-target-php-version` CLI flag.** (#3939) + Phan will use this instead of `target_php_version` for some backwards compatibility checks + (i.e. to check that the feature in question is supported by the oldest php version the project supports). + + If this is not configured, Phan will attempt to use the composer.json version ranges if they are available. + Otherwise, `target_php_version` will be used. + + Phan will use `target_php_version` instead if `minimum_target_php_version` is greater than `target_php_version`. + + Update various checks to use `minimum_target_php_version` instead of `target_php_version`. ++ Add `--always-exit-successfully-after-analysis` flag. + By default, phan exits with a non-zero exit code if 1 or more unsuppressed issues were reported. + When this CLI flag is set, phan will instead exit with exit code 0 as long as the analysis completed. ++ Include the installed php-ast version and the php version used to run Phan in the output of `phan --version`. (#4147) + +New features (Analysis): ++ Emit `PhanCompatibleArrowFunction` if using arrow functions with a minimum target php version older than php 7.4. ++ Emit `PhanCompatibleMatchExpression` if using match expressions with a minimum target php version older than php 8.0. ++ Emit `PhanNoopRepeatedSilenceOperator` for `@@expr` or `@(@expr)`. + This is less efficient and only makes a difference in extremely rare edge cases. ++ Avoid false positives for bitwise operations on floats such as unsigned 64-bit numbers (#4106) ++ Incomplete support for analyzing calls with php 8.0's named arguments. (#4037) + New issue types: `PhanUndeclaredNamedArgument*`, `PhanDuplicateNamedArgument*`, + `PhanMissingNamedArgument*`, + `PhanDefinitelyDuplicateNamedArgument`, `PhanPositionalArgumentAfterNamedArgument`, and + `PhanArgumentUnpackingUsedWithNamedArgument`, `PhanSuspiciousNamedArgumentForVariadic` ++ Incomplete support for analyzing uses of PHP 8.0's nullsafe operator(`?->`) for property reads and method calls. (#4067) ++ Warn about using `@var` where `@param` should be used (#1366) ++ Treat undefined variables as definitely null/undefined in various places + when they are used outside of loops and the global scope. (#4148) ++ Don't warn about undeclared global constants after `defined()` conditions. (#3337) + Phan will infer a broad range of types for these constants that can't be narrowed. ++ Parse `lowercase-string` and `non-empty-lowercase-string` in phpdoc for compatibility, but treat them like ordinary strings. ++ Emit `PhanCompatibleTrailingCommaParameterList` and `PhanCompatibleTrailingCommaArgumentList` **when the polyfill is used**. (#2269) + Trailing commas in argument lists require a minimum target version of php 7.3+, + and trailing commas in parameters or closure use lists require php 8.0+. + + This is only available in the polyfill because the native `php-ast` parser + exposes the information that php itself tracks internally, + and php deliberately does not track whether any of these node types have trailing commas. + + There are already other ways to detect these backwards compatibility issues, + such as `--native-syntax-check path/to/php7.x`. ++ Warn about variable definitions that are unused due to fallthroughs in switch statements. (#4162) + +Plugins: ++ Add more aliases to `DeprecateAliasPlugin` + +Miscellaneous: ++ Raise the severity of `PhanUndeclaredConstant` and `PhanStaticCallToNonStatic` from normal to critical. + Undeclared constants will become a thrown `Error` at runtime in PHP 8.0+. + +Bug fixes: ++ Suppress `PhanParamNameIndicatingUnused` in files loaded from `autoload_internal_extension_signatures` ++ Improve compatibility of polyfill/fallback parser with php 8.0 ++ Also try to check against the realpath() of the current working directory when converting absolute paths + to relative paths. ++ Generate baseline files with `/` instead of `\` on Windows in `--save-baseline` (#4149) + +Jul 31 2020, Phan 3.1.1 +----------------------- + +New features (CLI, Config): ++ Add `--baseline-summary-type={ordered_by_count,ordered_by_type,none}` to control the generation + of the summary comment generated by `--save-baseline=path/to/baseline.php` (#4044) + (overrides the new `baseline_summary_type` config). + The default comment summary (`ordered_by_count`) is prone to merge conflicts in large projects. + This does not affect analysis. ++ Add `tool/phan_repl_helpers.php`, a prototype tool that adds some functionality to `php -a`. + It can be required by running `require_once 'path/to/phan/tool/phan_repl_helpers.php'` during an interactive session. + + - This replaces the readline code completion and adds autocomplete for `->` on global variables. + This is currently buggy and very limited, and is missing some of the code completion functionality that is available in `php -a`. + (And it's missing a lot of the code completion functionality from the language server) + - This adds a global function `help($element_name_or_object)`. Run `help('help')` for usage and examples. + - Future releases may advantage of Phan's parsing/analysis capabilities in more ways. + - Several alternatives to the php shell already exist, such as [psysh](https://github.com/bobthecow/psysh). + `tool/phan_repl_helpers.php` is an experiment in augmenting the interactive php shell, not an alternative shell. ++ Update progress bar during class analysis phase. (#4099) + +New features (Analysis): ++ Support casting `iterable` to `iterable` (#4089) ++ Change phrasing for `analyze` phase in `--long-progress-bar` with `--analyze-twice` ++ Add `PhanParamNameIndicatingUnused` and `PhanParamNameIndicatingUnusedInClosure` + to indicate that using parameter names(`$unused*`, `$_`) to indicate to Phan that a parameter is unused is no longer recommended. (#4097) + Suppressions or the `@param [Type] $param_name @unused-param` syntax can be used instead. + PHP 8.0 will introduce named argument support. ++ Add a message to `PhanParamSignatureMismatch` indicating the cause of the issue being emitted. (#4103) + Note that `PhanParamSignaturePHPDocMismatch*` and `PhanParamSignatureReal*` have fewer false positives. ++ Warn about invalid types in class constants. (#4104) + Emit `PhanUndeclaredTypeClassConstant` if undeclared types are seen in phpdoc for class constants. + Emit `PhanCommentObjectInClassConstantType` if object types are seen in phpdoc for class constants. ++ Warn about `iterable` containing undeclared classes. (#4104) + +Language Server/Daemon mode: ++ Include PHP keywords such as `__FILE__`, `switch`, `function`, etc. in suggestions for code completions. + +Plugins: ++ Make `DuplicateExpressionPlugin` warn if adjacent statements are identical. (#4074) + New issue types: `PhanPluginDuplicateAdjacentStatement`. ++ Consistently make `PhanPluginPrintfNonexistentArgument` have critical severity. (#4080) + Passing too few format string arguments (e.g. `printf("%s %s", "Hello,")`) will be an `ArgumentCountError` in PHP 8. + +Bug fixes: ++ Fix false positive `PhanParamSignatureMismatch` issues (#4103) ++ Fix false positive `PhanParamSignaturePHPDocMismatchHasParamType` seen for magic method override of a real method with no real signature types. (#4103) + +Jul 16 2020, Phan 3.1.0 +----------------------- + +New features (CLI, Config): ++ Add `--output-mode=verbose` to print the line of code which caused the issue to be emitted after the textual issue output. + This is only emitted if the line is not whitespace, could be read, and does not exceed the config setting `max_verbose_snippet_length`. ++ Add `included_extension_subset` to limit Phan to using the reflection information to a subset of available extensions. (#4015) + This can be used to make Phan warn about using constants/functions/classes that are not in the target environment or dependency list + of a given PHP project/library. + Note that this may cause issues if a class from an extension in this list depends on classes from another extension that is outside of this list. + +New features (Analysis): ++ Don't emit `PhanTypeInvalidLeftOperandOfBitwiseOp` and other binary operation warnings for `mixed` ++ Emit `PhanIncompatibleRealPropertyType` when real property types are incompatible (#4016) ++ Change the way `PhanIncompatibleCompositionProp` is checked for. (#4024) + Only emit it when the property was redeclared in an inherited trait. ++ Emit `PhanProvidingUnusedParameter` when passing an argument to a function with an optional parameter named `$unused*` or `$_`. (#4026) + This can also be suppressed on the functionlike's declaration, and should be suppressed if this does not match the project's parameter naming. + This is limited to functions with no overrides. ++ Emit `PhanParamTooFewInPHPDoc` when a parameter that is marked with `@phan-mandatory-param` is not passed in. (#4026) + This is useful when needing to preserve method signature compatibility in a method override, or when a parameter will become mandatory in a future backwards incompatible release of a project. ++ Emit `PhanTypeMismatchArgumentProbablyReal` instead of `PhanTypeMismatchArgument` when the inferred real type of an argument has nothing in common with the phpdoc type of a user-defined function/method. + This is usually a stronger indicator that the phpdoc parameter type is inaccurate/incomplete or the argument is incorrect. + (Overall, fixing phpdoc errors may help ensure compatibility long-term if the library/framework being used moves to real types (e.g. php 8.0 union types) in the future.) + + **Note that Phan provides many ways to suppress issues (including the `--save-baseline=.phan/baseline.php` and `--load-baseline=.phan/baseline.php` functionality) in case + the switch to `ProbablyReal` introduces too many new issues in your codebase.** + (The new `ProbablyReal` issues are more severe than the original issue types. + When they're suppressed, the original less severe issue types will also be suppressed) ++ Emit `PhanTypeMismatchReturnProbablyReal` instead of `PhanTypeMismatchReturn` when the inferred real return type has nothing in common with the declared phpdoc return type of a user-defined function/method. (#4028) ++ Emit `PhanTypeMismatchPropertyProbablyReal` instead of `PhanTypeMismatchProperty` when the inferred assigned property type has nothing in common with a property's declared phpdoc type. (#4029) ++ Emit `PhanTypeMismatchArgumentInternalProbablyReal` instead of `PhanTypeMismatchArgumentInternal` in a few more cases. ++ Be stricter about checking if callables/closures have anything in common with other types. ++ Preserve more specific phpdoc types when the php 8.0 `mixed` type is part of the real type set. ++ Also emit `PhanPluginUseReturnValueNoopVoid` when a function/method's return type is implicitly void (#4049) ++ Support `@param MyType $name one line description @unused-param` to suppress warnings about individual unused method parameters. + This is a new alias of `@phan-unused-param`. ++ Support analyzing [PHP 8.0's match expression](https://wiki.php.net/rfc/match_expression_v2). (#3970) + +Plugins: ++ Warn and skip checks instead of crashing when running `InlineHTMLPlugin` without the `tokenizer` extension installed. (#3998) ++ Support throwing `\Phan\PluginV3\UnloadablePluginException` instead of returning a plugin object in plugin files. ++ When a plugin registers for a method definition with `AnalyzeFunctionCallCapability`, automatically register the same closure for all classlikes using the same inherited definition of that method. (#4021) ++ Add `UnsafeCodePlugin` to warn about uses of `eval` or the backtick string shorthand for `shell_exec()`. ++ Add `DeprecateAliasPlugin` to mark known aliases such as `sizeof()` or `join()` as deprecated. + Implement support for `--automatic-fix`. ++ Add `PHPDocInWrongCommentPlugin` to warn about using `/*` instead of `/**` with phpdoc annotations supported by Phan. + +Miscellaneous ++ Update more unit tests for php 8.0. ++ Emit a warning and load an extremely limited polyfill for `filter_var` to parse integers/floats if the `filter` extension is not loaded. + +Bug Fixes: ++ Make suppressions on trait methods/properties consistently apply to the inherited definitions from classes/traits using those traits. ++ Fix false positive where Phan would think that union types with real types containing `int` and other types had an impossible condition. + Fix another false positive checking if `?A|?B` can cast to another union type. + +Jul 03 2020, Phan 3.0.5 +----------------------- + +New features(CLI, Configs): ++ Add `-X` as an alias of `--dead-code-detection-prefer-false-positive`. + +New features(Analysis): ++ Emit `PhanTypeInvalidLeftOperandOfBitwiseOp` and `PhanTypeInvalidRightOperandOfBitwiseOp` for argument types to bitwise operations other than `int|string`. + (affects `^`, `|`, `&`, `^=`, `|=`, `&=`) + +Bug fixes: ++ Fix false positives in php 8.0+ type checking against the real `mixed` type. (#3994) ++ Fix unintentionally enabling GC when the `pcntl` extension is not enabled. (#4000) + It should only be enabled when running in daemon mode or as a language server. + +Jul 01 2020, Phan 3.0.4 +----------------------- + +New features(Analysis): ++ Emit `PhanTypeVoidExpression` when using an expression returning `void` in places such as array keys/values. ++ More accurately infer unspecified types when closures are used with `array_map` (#3973) ++ Don't flatten array shapes and literal values passed to closures when analyzing closures. (Continue flattening for methods and global functions) ++ Link to documentation for internal stubs as a suggestion for undeclared class issues when Phan has type information related to the class in its signature files. + See https://github.com/phan/phan/wiki/Frequently-Asked-Questions#undeclared_element ++ Properly render the default values if available(`ReflectionParameter->isDefaultValueAvailable()`) in php 8.0+. ++ Properly set the real union types based on reflection information for functions/methods in more edge cases. ++ Properly infer that union types containing the empty array shape are possibly empty after sorting (#3980) ++ Infer a more accurate real type set from unary ops `~`, `+`, and `-` (#3991) ++ Improve ability to infer assignments within true branch of complex expressions in conditions such as `if (A && complex_expression) { } else { }` (#3992) + +Plugins: ++ Add `ShortArrayPlugin`, to suggest using `[]` instead of `array()` or `list()` ++ In `DuplicateExpressionPlugin`, emit `PhanPluginDuplicateExpressionAssignmentOperation` if `X = X op Y` is seen and it can be converted to `X op= Y` (#3985) + (excluding `??=` for now) ++ Add `SimplifyExpressionPlugin`, to suggest shortening expressions such as `$realBool ? true : false` or `$realBool === false` ++ Add `RemoveDebugStatementPlugin`, to suggest removing debugging output statements such as `echo`, `print`, `printf`, `fwrite(STDERR, ...)`, `var_export(...)`, inline html, etc. + This is only useful in applications or libraries that print output in only a few places, as a sanity check that debugging statements are not accidentally left in code. + +Bug fixes: ++ Treat `@method static foo()` as an instance method returning the union type `static` (#3981) + Previously, Phan treated it like a static method with type `void` based on an earlier phpdoc spec. ++ Fix the way that Phan inferred the `finally` block's exit status affected the `try` block. (#3987) + +Jun 21 2020, Phan 3.0.3 +----------------------- + +New features(Analysis): ++ Include the most generic types when conditions such as `is_string()` to union types containing `mixed` (#3947) ++ More aggressively infer that `while` and `for` loop bodies are executed at least once in functions outside of other loops (#3948) ++ Infer the union type of `!$expr` from the type of `$expr` (#3948) ++ Re-enable `simplify_ast` by default in `.phan/config.php` (#3944, #3945) ++ Avoid false positives in `--constant-variable-detection` for `++`/`--` ++ Make `if (!$nullableValue) { }` remove truthy literal scalar values such as `'value'` and `1` and `1.0` when they're nullable ++ Emit `PhanTypeVoidArgument` when passing a void return value as a function argument (#3961) ++ Correctly merge the possible union types of pass-by-reference variables (#3959) ++ Improve php 8.0-dev shim support. Fix checking for array references and closure use references in php 8.0+. ++ More aggressively check if expression results should be used for conditionals and binary operators. + +Plugins: ++ Add `ConstantVariablePlugin` to point out places where variables are read when they have only one possible scalar value. (#3953) + This may help detect logic errors such as `$x === null ? json_encode($x) : 'default'` or code that could be simplified, + but most issues it emits wouldn't be worth fixing due to hurting readability or being false positives. ++ Add `MergeVariableInfoCapability` for plugins to hook into ContextMergeVisitor and update data for a variable + when merging the outcome of different scopes. (#3956) ++ Make `UseReturnValuePlugin` check if a method is declared as pure before using the dynamic checks based on percentage of + calls where the return value is used, if that option is enabled. ++ In `DuplicateArrayKeyPlugin`, properly check for duplicate non-scalar cases. + +Language Server/Daemon mode: ++ Fix bug where the Phan daemon would crash on the next request after analyzing a file outside of the project being analyzed, + when pcntl was disabled or unavailable (#3954) + +Bug fixes: ++ Fix `PhanDebugAnnotation` output for variables after the first one in `@phan-debug-var $a, $b` (#3943) ++ Use the correct constant to check if closure use variables are references in php 8.0+ + +Miscellaneous: ++ Update function signature stubs for the `memcache` PECL (#3841) + +Jun 07 2020, Phan 3.0.2 +----------------------- + +New features(CLI, Configs): ++ Add `--dead-code-detection-prefer-false-positive` to run dead code detection, + erring on the side of reporting potentially dead code even when it is possibly not dead. + (e.g. when methods of unknown objects are invoked, don't mark all methods with the same name as potentially used) + +New features(Analysis): ++ Fix false positive `PhanAbstractStaticMethodCall` (#3935) + Also, properly emit `PhanAbstractStaticMethodCall` for a variable containing a string class name. + +Plugins: ++ Fix incorrect check and suggestion for `PregRegexCheckerPlugin`'s warning if + `$` allows an optional newline before the end of the string when the configuration includes + `['plugin_config' => ['regex_warn_if_newline_allowed_at_end' => true]]`) (#3938) ++ Add `BeforeLoopBodyAnalysisCapability` for plugins to analyze loop conditions before the body (#3936) ++ Warn about suspicious param order for `str_contains`, `str_ends_with`, and `str_starts_with` in `SuspiciousParamOrderPlugin` (#3934) + +Bug fixes: ++ Don't report unreferenced class properties of internal stub files during dead code detection + (i.e. files in `autoload_internal_extension_signatures`). ++ Don't remove the leading directory separator when attempting to convert a file outside the project to a relative path. + (in cases where the directory is different but has the project's name as a prefix) + +Jun 04 2020, Phan 3.0.1 +----------------------- + +New features(Analysis): ++ Support analysis of php 8.0's `mixed` type (#3899) + New issue types: `PhanCompatibleMixedType`, `PhanCompatibleUseMixed`. ++ Treat `static` and `false` like real types and emit more severe issues in all php versions. ++ Improve type inferences from negated type assertions (#3923) + (analyze more expression kinds, infer real types in more places) ++ Warn about unnecessary use of `expr ?? null`. (#3925) + New issue types: `PhanCoalescingNeverUndefined`. ++ Support PHP 8.0 non-capturing catches (#3907) + New issue types: `PhanCompatibleNonCapturingCatch`. ++ Infer type of `$x->magicProp` from the signature of `__get` ++ Treat functions/methods that are only called by themselves as unreferenced during dead code detection. ++ Warn about `each()` being deprecated when the `target_php_version` is php 7.2+. (#2746) + This is special cased because PHP does not flag the function itself as deprecated in `ReflectionFunction`. + (PHP only emits the deprecation notice for `each()` once at runtime) + +Miscellaneous: ++ Check for keys that are too long when computing levenshtein distances (when Phan suggests alternatives). + +Plugins: ++ Add `AnalyzeLiteralStatementCapability` for plugins to analyze no-op string literals (#3911) ++ In `PregRegexCheckerPlugin`, warn if `$` allows an optional newline before the end of the string + when configuration includes `['plugin_config' => ['regex_warn_if_newline_allowed_at_end' => true]]`) (#3915) ++ In `SuspiciousParamOrderPlugin`, warn if an argument has a near-exact name match for a parameter at a different position (#3929) + E.g. warn about calling `foo($b)` or `foo(true, $this->A)` for `function foo($a = false, $b = false)`. + New issue types: `PhanPluginSuspiciousParamPosition`, `PhanPluginSuspiciousParamPositionInternal` + +Bug fixes: ++ Fix false positive `PhanTypeMismatchPropertyDefault` involving php 7.4 typed properties with no default + and generic comments (#3917) ++ Don't remove leading directory separator when attempting to convert a file outside the project to a relative path. + +May 09 2020, Phan 3.0.0 +----------------------- + +New features(CLI, Config): ++ Support `PHAN_COLOR_PROGRESS_BAR` as an environment variable to set the color of the progress bar. + Ansi color names (e.g. `light_blue`) or color codes (e.g. `94`) can be used. (See src/Phan/Output/Colorizing.php) + +New features(Analysis): ++ Infer that `foreach` keys and values of possibly empty iterables are possibly undefined after the end of a loop. (#3898) ++ Allow using the polyfill parser to parse internal stubs. (#3902) + (To support newer syntax such as union types, trailing commas in parameter lists, etc.) + +May 02 2020, Phan 3.0.0-RC2 +----------------------- + +Fix published GitHub release tag (used `master` instead of `v3`). + +May 02 2020, Phan 3.0.0-RC1 +----------------------- + +Backwards incompatible changes: ++ Drop PHP 7.1 support. PHP 7.1 reached its end of life for security support in December 2019. + Many of Phan's dependencies no longer publish releases supporting php 7.1, + which will likely become a problem running Phan with future 8.x versions + (e.g. in the published phar releases). ++ Drop PluginV2 support (which was deprecated in Phan 2) in favor of PluginV3. ++ Remove deprecated classes and helper methods. + +??? ?? 2020, Phan 2.7.3 (dev) +----------------------- + +Bug fixes: ++ Fix handling of windows path separators in `phan_client` ++ Fix a crash when emitting `PhanCompatibleAnyReturnTypePHP56` or `PhanCompatibleScalarTypePHP56` for methods with no parameters. + +May 02 2020, Phan 2.7.2 +----------------------- + +New features(CLI, Config): ++ Add a `--native-syntax-check=/path/to/php` option to enable `InvokePHPNativeSyntaxCheckPlugin` + and add that php binary to the `php_native_syntax_check_binaries` array of `plugin_config` + (treated here as initially being the empty array if not configured). + + This CLI flag can be repeated to run PHP's native syntax checks with multiple php binaries. + +New features(Analysis): ++ Emit `PhanTypeInvalidThrowStatementNonThrowable` when throwing expressions that can't cast to `\Throwable`. (#3853) ++ Include the relevant expression in more issue messages for type errors. (#3844) ++ Emit `PhanNoopSwitchCases` when a switch statement contains only the default case. ++ Warn about unreferenced private methods of the same name as methods in ancestor classes, in dead code detection. ++ Warn about useless loops. Phan considers loops useless when the following conditions hold: + + 1. Variables defined within the loop aren't used outside of the loop + (requires `unused_variable_detection` to be enabled whether or not there are actually variables) + 2. It's likely that the statements within the loop have no side effects + (this is only inferred for a subset of expressions in code) + + (Enabling the plugin `UseReturnValuePlugin` (and optionally `'plugin_config' => ['infer_pure_methods' = true]`) helps detect if function calls are useless) + 3. The code is in a functionlike scope. + + New issue types: `PhanSideEffectFreeForeachBody`, `PhanSideEffectFreeForBody`, `PhanSideEffectFreeWhileBody`, `PhanSideEffectFreeDoWhileBody` ++ Infer that previous conditions are negated when analyzing the cases of a switch statement (#3866) ++ Support using `throw` as an expression, for PHP 8.0 (#3849) + (e.g. `is_string($arg) || throw new InvalidArgumentException()`) + Emit `PhanCompatibleThrowException` when `throw` is used as an expression instead of a statement. + +Plugins: ++ Emit `PhanPluginDuplicateCatchStatementBody` in `DuplicateExpressionPlugin` when a catch statement has the same body and variable name as an adjacent catch statement. + (This should be suppressed in projects that support php 7.0 or older) ++ Add `PHP53CompatibilityPlugin` as a demo plugin to catch common incompatibilities with PHP 5.3. (#915) + New issue types: `PhanPluginCompatibilityArgumentUnpacking`, `PhanPluginCompatibilityArgumentUnpacking`, `PhanPluginCompatibilityArgumentUnpacking` ++ Add `DuplicateConstantPlugin` to warn about duplicate constant names (`define('X', value)` or `const X = value`) in the same statement list. + This is only recommended in projects with files with too many global constants to track manually. + +Bug Fixes: ++ Fix a bug causing FQSEN names or namespaces to be converted to lowercase even if they were never lowercase in the codebase being analyzed (#3583) + +Miscellaneous: ++ Replace `PhanTypeInvalidPropertyDefaultReal` with `TypeMismatchPropertyDefault` (emitted instead of `TypeMismatchProperty`) + and `TypeMismatchPropertyDefaultReal` (#3068) ++ Speed up `ASTHasher` for floats and integers (affects code such as `DuplicateExpressionPlugin`) ++ Call `uopz_allow_exit(true)` if uopz is enabled when initializing Phan. (#3880) + Running Phan with `uopz` is recommended against (unless debugging Phan itself), because `uopz` causes unpredictable behavior. + Use stubs or internal stubs instead. + +Apr 11 2020, Phan 2.7.1 +----------------------- + +New features(CLI, Configs): ++ Improve the output of `tool/make_stubs`. Use better defaults than `null`. + Render `unknown` for unknown defaults in `tool/make_stubs` and Phan's issue messages. + (`default` is a reserved keyword used in switch statements) + +Bug Fixes: ++ Work around unintentionally using `symfony/polyfill-72` for `spl_object_id` instead of Phan's polyfill. + The version used caused issues on 32-bit php 7.1 installations, and a slight slowdown in php 7.1. + +Plugins: ++ PHP 8.0-dev compatibility fixes for `InvokePHPNativeSyntaxCheckPlugin` on Windows. ++ Infer that some new functions in PHP 8.0-dev should be used in `UseReturnValuePlugin` ++ Emit the line and expression of the duplicated array key or switch case (#3837) + +Apr 01 2020, Phan 2.7.0 +----------------------- + +New features(CLI, Configs): ++ Sort output of `--dump-ctags=basic` by element type before sorting by file name (#3811) + (e.g. make class and global function declarations the first tag type for a tag name) ++ Colorize the output of `phan_client` by default for the default and text output modes. (#3808) + Add `phan --no-color` option to disable colors. ++ Warn about invalid CLI flags in `phan_client` (#3776) ++ Support representing more AST node types in issue messages. (#3783) ++ Make some issue messages easier to read (#3745, #3636) ++ Allow using `--minimum-severity=critical` instead of `--minimum-severity=10` (#3715) ++ Use better placeholders for parameter default types than `null` in issue messages and hover text (#3736) ++ Release `phantasm`, a prototype tool for assembling information about a codebase and aggressively optimizing it. + Currently, the only feature is replacing class constants with their values, when safe to do so. + More features (e.g. inlining methods, aggressively optimizing out getters/setters, etc.) are planned for the future. + See `tool/phantasm --help` for usage. + +New features(Analysis): ++ Improve analysis of php 7.4 typed properties. + Support extracting their real union types from Reflection information. + Infer the existence of properties that are not in `ReflectionClass->getPropertyDefaults()` + due to being uninitialized by default. ++ Emit `PhanAbstractStaticMethodCall*` when calling an abstract static method statically. (#3799) ++ Emit `PhanUndeclaredClassReference` instead of `PhanUndeclaredClassConstant` for `MissingClass::class`. + +Language Server/Daemon mode: ++ Catch exception seen when printing debug info about not being able to parse a file. ++ Warn when Phan's language server dependencies were installed for php 7.2+ + but the language server gets run in php 7.1. (phpdocumentor/reflection-docblock 5.0 requires php 7.2) ++ Immediately return cached hover text when the client repeats an identical hover request. (#3252) + +Miscellaneous: ++ PHP 8.0-dev compatibility fixes, analysis for some new functions of PHP 8.0-dev. ++ Add `symfony/polyfill-php72` dependency so that symfony 5 will work better in php 7.1. + The next Phan major release will drop support for php 7.1. + +Mar 13 2020, Phan 2.6.1 +----------------------- + +New features(CLI, Configs): ++ Add a `--dump-ctags=basic` flag to dump a `tags` file in the project root directory. (https://linux.die.net/man/1/ctags) + This is different from `tool/make_ctags_for_phan_project` - it has no external dependencies. + +New features(Analysis): ++ Infer that the real type set of the key in `foreach ($arrayVar as $key => ...)` is definitely an `int|string` + in places where Phan previously inferred the empty union type, improving redundant condition detection. (#3789) + +Bug fixes: ++ Fix a crash in `phan --dead-code-detection` when a trait defines a real method and phpdoc `@method` of the same name (#3796) + +Miscellaneous: ++ Also allow `netresearch/jsonmapper@^2.0` as a dependency when enforcing the minimum allowed version (#3801) + +Mar 07 2020, Phan 2.6.0 +----------------------- + +New features(CLI, Configs): ++ Show empty union types as `(empty union type)` in issue messages instead of as an empty string. ++ Add a new CLI option `--analyze-twice` to run the analysis phase twice (#3743) + + Phan infers additional type information for properties, return types, etc. while analyzing, + and this will help it detect more potential errors. + (on the first run, it would analyze files before some of those types were inferred) ++ Add a CLI option `--analyze-all-files` to analyze all files, ignoring `exclude_analysis_file_list`. + This is potentially useful if third party dependencies are missing type information (also see `--analyze-twice`). ++ Add `--dump-analyzed-file-list` to dump all files Phan would analyze to stdout. ++ Add `allow_overriding_vague_return_types` to allow Phan to add inferred return types to functions/closures/methods declared with `@return mixed` or `@return object`. + This is disabled by default. + + When this is enabled, it can be disabled for individual methods by adding `@phan-hardcode-return-type` to the comment of the method. + (if the method has any type declarations such as `@return mixed`) + + Previously, Phan would only add inferred return types if there was no return type declaration. + (also see `--analyze-twice`) ++ Also emit the code fragment for the argument in question in the `PhanTypeMismatchArgument` family of issue messages (#3779) ++ Render a few more AST node kinds in code fragments in issue messages. + +New features(Analysis): ++ Support parsing php 8.0 union types (and the static return type) in the polyfill. (#3419, #3634) ++ Emit `PhanCompatibleUnionType` and `PhanCompatibleStaticType` when the target php version is less than 8.0 and union types or static return types are seen. (#3419, #3634) ++ Be more consistent when warning about issues in values of class constants, global constants, and property defaults. ++ Infer key and element types from `iterator_to_array()` ++ Infer that modification of or reading from static properties all use the same property declaration. (#3760) + Previously, Phan would track the static property's type separately for each subclass. + (static properties from traits become different instances, in each class using the trait) ++ Make assignments to properties of the declaring class affect type inference for those properties when accessed on subclasses (#3760) + + Note that Phan is only guaranteed to analyze files once, so if type information is missing, + the only way to ensure it's available is to add it to phpdoc (`UnknownElementTypePlugin` can help) or use `--analyze-twice`. ++ Make internal checks if generic array types are strict subtypes of other types more accurate. + (e.g. `object[]` is not a strict subtype of `stdClass[]`, but `stdClass[]` is a strict subtype of `object[]`) + +Plugins: ++ Add `UnknownClassElementAccessPlugin` to warn about cases where Phan can't infer which class an instance method is being called on. + (To work correctly, this plugin requires that Phan use a single analysis process) ++ Add `MoreSpecificElementTypePlugin` to warn about functions/methods where the phpdoc/actual return type is vaguer than the types that are actually returned by a method. (#3751) + This is a work in progress, and has a lot of false positives. + (To work correctly, this plugin requires that Phan use a single analysis process) ++ Fix crash in `PrintfCheckerPlugin` when analyzing code where `fprintf()` was passed an array instead of a format string. ++ Emit `PhanTypeMissingReturnReal` instead of `PhanTypeMissingReturn` when there is a real return type signature. (#3716) ++ Fix bug running `InvokePHPNativeSyntaxCheckPlugin` on Windows when PHP binary is in a path containing spaces. (#3766) + +Bug fixes: ++ Fix bug causing Phan to fail to properly recursively analyze parameters of inherited methods (#3740) + (i.e. when the methods are called on the subclass) ++ Fix ambiguity in the way `Closure():T[]` and `callable():T[]` are rendered in error messages. (#3731) + Either render it as `(Closure():T)[]` or `Closure():(T[])` ++ Don't include both `.` and `vendor/x/y/` when initializing Phan configs with settings such as `--init --init-analyze-dir=.` (#3699) ++ Be more consistent about resolving `static` in generators and template types. ++ Infer the iterable value type for `Generator`. It was previously only inferred when there were 2 or more template args in phpdoc. ++ Don't let less specific type signatures such as `@param object $x` override the real type signature of `MyClass $x` (#3749) ++ Support PHP 7.4's `??=` null coalescing assignment operator in the polyfill. ++ Fix crash analyzing invalid nodes such as `2 = $x` in `RedundantAssignmentPlugin`. ++ Fix crash inferring type of `isset ? 2 : 3` with `--use-fallback-parser` (#3767) ++ Fix false positive unreferenced method warnings for methods from traits + when the methods were referenced in base classes or interfaces of classes using those traits. + +Language Server/Daemon mode: ++ Various performance improvements for the language server/daemon with or without pcntl (#3758, #3769, #3771) + +Feb 20 2020, Phan 2.5.0 +----------------------- + +New Features(CLI): ++ Support using `directory_suppressions` in Phan baseline files in `--load-baseline`. (#3698) ++ Improve error message for warnings about Phan being unable to read files in the analyzed directory. + +New Features(Analysis): ++ Instead of failing to parse intersection types in phpdoc entirely, parse them as if they were union types. (#1629) + The annotations `@phan-param`, `@phan-return`, `@phan-var`, etc. can be used to override the regular phpdoc in the various cases where this behavior causes problems. + + **Future** Phan releases will likely about unsupported phpdoc (e.g. `int&string`) and have actual support for intersection types. ++ Emit `PhanUndeclaredConstantOfClass` (severity critical) for undeclared class constants instead of `PhanUndeclaredConstant` (severity normal) + This should not be confused with `PhanUndeclaredClassConstant`, which already exists and refers to accessing class constants of classes that don't exist. ++ Emit the expression that's an invalid object with issue types such as `PhanTypeExpectedObject*`, `PhanTypeInvalidInstanceof` (#3717) ++ Emit `PhanCompatibleScalarTypePHP56` and `PhanCompatibleAnyReturnTypePHP56` for function signatures when `target_php_version` is `'5.6'` (#915) + (This also requires that `backward_compatibility_checks` be enabled.) ++ Use more accurate line numbers for warnings about function parameters. ++ When `assume_real_types_for_internal_functions` is on *and* a function has a non-empty return type in Reflection, + make Phan's known real type signatures override the real reflection return type information (useful when Phan infers `list` and Reflection says `array`). + Phan previously used the type from Reflection. ++ Normalize phpdoc parameter and return types when there is a corresponding real type in the signature. (#3725) + (e.g. convert `bool|false|null` to `?bool`) + +Plugins: ++ Add `SubscribeEmitIssueCapability` to detect or suppress issues immediately before they are emitted. (#3719) + +Bug fixes: ++ Don't include issues that weren't emitted in the file generated by `--save-baseline` (#3719) ++ Fix incorrect file location for other definition in `PhanRedefinedClassReference` under some circumstances. ++ Fix incorrect issue name: `PhanCompatibleNullableTypePHP71` should be named `PhanCompatibleObjectTypePHP71` ++ Fix false positive `PhanPartialTypeMismatchProperty` when a php 7.4 typed property has a default expression value (#3725) + +Feb 13 2020, Phan 2.4.9 +----------------------- + +New Features(Analysis): ++ Infer that `class_exists` implies the first argument is a class-string, + and that `method_exists` implies the first argument is a class-string or an object. (#2804, #3058) + + Note that Phan still does not infer that the class or method actually exists. ++ Emit `PhanRedefineClass` on **all** occurrences of a duplicate class, not just the ones after the first occurrence of the class. (#511) ++ Emit `PhanRedefineFunction` on **all** occurrences of a duplicate function/method, not just the ones after the first. ++ Emit `PhanRedefinedClassReference` for many types of uses of user-defined classes that Phan has parsed multiple definitions of. + Phan will not warn about internal classes, because the duplicate definition is probably a polyfill. + (e.g. `new DuplicateClass()`, `DuplicateClass::someMethod()`) + +Bug fixes: ++ Fix false positive `PhanParamSuspiciousOrder` for `preg_replace_callback` (#3680) ++ Fix false positive `PhanUnanalyzableInheritance` for renamed methods from traits. (#3695) ++ Fix false positive `PhanUndeclaredConstant` previously seen for inherited class constants in some parse orders. (#3706) ++ Fix uncaught `TypeError` converting `iterable` to nullable (#3709) + +Jan 25 2020, Phan 2.4.8 +----------------------- + +Bug fixes: ++ Fix bug introduced in 2.4.7 where there were more false positives when `--no-progress-bar` was used. (#3677) + +Jan 22 2020, Phan 2.4.7 +----------------------- + +New features(CLI, Configs): ++ Add an environment variable `PHAN_NO_UTF8=1` to always avoid UTF-8 in progress bars. + This may help with terminals or logs that have issues with UTF-8 output. + Error messages will continue to include UTF-8 when part of the error. ++ Allow `phan --init` to complete even if composer.json has no configured `autoload` directories, + as long as at least one directory or file was configured. ++ Add a setting `error_prone_truthy_condition_detection` that can be enabled to warn about error-prone truthiness/falsiness checks. New issue types: + - `PhanSuspiciousTruthyCondition` (e.g. for `if ($x)` where `$x` is `object|int`) + - `PhanSuspiciousTruthyString` (e.g. for `?string` - `'0'` is also falsey in PHP) ++ Limit calculation of max memory usage to the **running** worker processes with `--processes N` (#3606) ++ Omit options that should almost always be on (e.g. `analyze_signature_compatibility`) from the output of `phan --init` (#3660) ++ Allow `phan --init` to create config file with `target_php_version` of `'7.4'` or `'8.0'` based on composer.json (#3671) + +New Features(Analysis): ++ Infer that merging defined variables with possibly undefined variables is also possibly undefined. (#1942) ++ Add a fallback when some types of conditional check results in a empty union type in a loop: + If all types assigned to the variable in a loop in a function are known, + then try applying the condition to the union of those types. (#3614) + (This approach was chosen because it needs to run only once per function) ++ Infer that assignment operations (e.g. `+=`) create variables if they were undefined. ++ Properly infer that class constants that weren't literal int/float/strings have real type sets in their union types. ++ Normalize union types of generic array elements after fetching `$x[$offset]`. + (e.g. change `bool|false|null` to `?bool`) ++ Normalize union types of result of `??` operator. ++ Fix false positives in redundant condition detection for the real types of array accesses. (#3638, #3645, #3650) ++ Support the `non-empty-string` type in phpdoc comments (neither `''` nor `'0'`). + Warn about redundant/impossible checks of `non-empty-string`. ++ Support the `non-zero-int` type in phpdoc comments. Infer it in real types and warn about redundant checks for zero/truthiness. ++ Support the the `non-empty-mixed` in phpdoc comments and in inferences. ++ Fix false positives possibly undefined variable warnings after conditions + such as `if (X || count($x = []))`, `if (X && preg_match(..., $matches))`, etc. + +Bug fixes: ++ Fix a crash analyzing assignment operations on `$GLOBALS` such as `$GLOBALS['var'] += expr;` (#3615) ++ Fix false positive `Phan[Possibly]UndeclaredGlobalVariable` after conditions such as `assert($var instanceof MyClass` when the variable was not assigned to within the file or previously analyzed files. (#3616) ++ Fix line number of 0 for some nodes when `simplify_ast` is enabled. (#3649) + +Plugins: ++ Make Phan use the real type set of the return value of the function being analyzed when plugins return a union type without a real type set. + +Maintenance: ++ Infer that `explode()` is possibly the empty list when `$limit` is possibly negative. (#3617) ++ Make Phan's code follow more PSR-12 style guidelines + (`` when using `Type::fromFullyQualifiedString()`. ++ Fix warnings about `password_hash()` algorithm constants with php 7.4 (#3560) + `PASSWORD_DEFAULT` became null in php 7.4, and other constants became strings. + + Note that you will need to run Phan with both php 7.4 and a `target_php_version` of 7.4 to fix the errors. ++ Fix uncaught `AssertionError` when parsing `@return \\...` (#3573) + +Nov 24 2019, Phan 2.4.4 +----------------------- + +New features(CLI, Configs): ++ When stderr is redirected a file or to another program, show an append-only progress bar by default. (#3514) + Phan would previously disable the progress bar entirely by default. + + The new `--long-progress-bar` CLI flag can be used to choose this progress bar. + + (The `--no-progress-bar` CLI flag or the environment variable `PHAN_DISABLE_PROGRESS_BAR=1` can be used to disable this) ++ Treat `$var = $x['possibly undefined offset']` as creating a definitely defined variable, + not a possibly undefined variable. (#3534) + + The config setting `convert_possibly_undefined_offset_to_nullable` controls + whether the field type gets cast to the nullable equivalent after removing undefined. + +New features(Analysis): ++ Emit `PhanPossiblyUndefinedArrayOffset` for accesses to array fields that are possibly undefined. (#3534) ++ Warn about returning/not returning in void/non-void functions. + New issue types: `PhanSyntaxReturnValueInVoid`, `PhanSyntaxReturnExpectedValue` ++ Infer that `$var[$offset] = expr;`/`$this->prop[$offset] = expr;` causes that element to be non-null (#3546) ++ Emit `PhanEmptyForeachBody` when iterating over a type that isn't `Traversable` with an empty statement list. ++ Warn about computing `array_values` for an array that is already a list. (#3540) ++ Infer the real type is still an array after assigning to a field of an array. + +Plugins: ++ In `DuplicateExpressionPlugin`, emit `PhanPluginDuplicateIfStatements` + if the body for `else` is identical to the above body for the `if/elseif`. + +Maintenance: ++ Support native parsing of `AST_TYPE_UNION` (union type) nodes for PHP 8.0.0-dev. ++ Reduce memory usage after the polyfill/fallback parser parses ASTs + (when the final AST isn't cached on disk from a previous run) ++ Make the error message for missing `php-ast` contain more detailed instructions on how to install `php-ast`. + +Nov 20 2019, Phan 2.4.3 +----------------------- + +New features(CLI, Configs): ++ Support `NO_COLOR` environment variable. (https://no-color.org/) + When this variable is set, Phan's error message and issue text will not be colorized unless the CLI arg `--color` or `-c` is used. + This overrides the `PHAN_ENABLE_COLOR_OUTPUT` setting. ++ Add `PHAN_DISABLE_PROGRESS_BAR` environment variable to disable progress bar by default unless the CLI arg `--progress-bar` is used. ++ Show an extra decimal digit of precision in the progress bar when the terminal is wide enough. (#3514) + +New features(Analysis): ++ Make inferred real types more accurate for equality/identity/instanceof checks. ++ Combine array shape types into a single union type when merging variable types from multiple branches. (#3506) + Do a better job of invalidating the real union type of fields of array shape types when the field is only checked/set on some code branches. ++ Make issue suggestions (and CLI suggestions) for completions of prefixes case-insensitive. ++ Support `@seal-properties` and `@seal-methods` as aliases of `@phan-forbid-undeclared-magic-properties` and `@phan-forbid-undeclared-magic-methods` ++ More aggressively infer real types of array destructuring(e.g. `[$x] = expr`) and accesses of array dimensions (e.g. `$x = expr[dim]`) (#3481) + + This will result in a few more false positives about potentially real redundant/impossible conditions and real type mismatches. ++ Fix false positives caused by assuming that the default values of properties are the real types of properties. ++ Infer that globals used in functions (`global $myGlobal;`) have unknown real types - don't emit warnings about redundant/impossible conditions. (#3521) + +Plugins: ++ Also start checking if closures (and arrow functions) can be static in `PossiblyStaticMethodPlugin` ++ Add `AvoidableGetterPlugin` to suggest when `$this->prop` can be used instead of `$this->getProp()`. + (This will suggest using the property instead of the getter method if there are no known method overrides of the getter. This is only checked for instance properties of `$this`) ++ Increase severity of `PhanPluginPrintfNonexistentArgument` to critical. It will become an ArgumentCountError in PHP 8. + +Maintenance: ++ Bump minimum version of netresearch/jsonmapper to avoid php notices in the language server in php 7.4 ++ Improve worst-case performance when analyzing code that has many possible combinations of array shapes. + +Bug fixes: ++ Properly emit redundant and impossible condition warnings about uses of class constants defined as literal strings/floats/integers. + (i.e. infer their real union types) ++ Fix false positive inference that `$x[0]` was `string` for `$x` of types such as `list<\MyClass>|string` (reported in #3483) ++ Consistently inherit analysis settings from parent classes recursively, instead of only inheriting them from the direct parent class. (#3472) + (settings include presence of dynamic properties, whether undeclared magic methods are forbidden, etc.) ++ Don't treat methods that were overridden in one class but inherited by a different class as if they had overrides. ++ Fix a crash when running in php 8.0.0-dev due to Union Types being found in Reflection. (#3503) ++ Fix edge case looking up the `extends` class/interface name when the namespace is a `use` alias (#3494) + +Nov 08 2019, Phan 2.4.2 +----------------------- + +New features(Analysis): ++ Emit `PhanTypeInvalidCallExpressionAssignment` when improperly assigning to a function/method's result (or a dimension of that result) (#3455) ++ Fix an edge case parsing `(0)::class` with the polyfill. (#3454) ++ Emit `PhanTypeInvalidDimOffset` for accessing any dimension on an empty string or an empty array. (#3385) ++ Warn about invalid string literal offsets such as `'str'[3]`, `'str'[-4]`, etc. (#3385) ++ Infer that arrays are non-empty and support array access from `isset($x[$offset])` (#3463) ++ Make `array_key_exists` imply argument is a `non-empty-array` (or an `object`). (#3465, #3469) ++ Make `isset($x[$offset])` imply argument is a `non-empty-array`, `object`, or `string` + Make `isset($x['literal string'])` imply argument is a `non-empty-array` or `object`, and not a `string`. ++ Make `isset($x->prop)` imply `$x` is an `object`. ++ Make `isset($this->prop[$x])` imply `$this->prop` is not the empty array shape. (#3467) ++ Improve worst-case time of deduplicating unique types in a union type (#3477, suggested in #3475) + +Maintenance: ++ Update function signature maps for internal signatures. + +Bug fixes: ++ Fix false positive `PhanSuspiciousWeakTypeComparison` for `in_array`/`array_search`/`array_key_exists` with function arguments defaulting to `[]` + +Nov 03 2019, Phan 2.4.1 +----------------------- + +New features(CLI, Configs): ++ Enable the progress bar by default, if `STDERR` is being rendered directly to a terminal. + Add a new option `--no-progress-bar`. ++ Emit warnings about missing files in `file_list`, CLI args, etc. to `STDERR`. (#3434) ++ Clear the progress bar when emitting many types of warnings to STDERR. + +New features(Analysis): ++ Suggest similarly named static methods and static properties for `PhanUndeclaredConstant` issues on class constants. (#3393) ++ Support `@mixin` (and an alias `@phan-mixin`) as a way to load public methods and public instance properties + as magic methods and magic properties from another classlike. (#3237) + + Attempts to parse or analyze mixins can be disabled by setting `read_mixin_annotations` to `false` in your Phan config. ++ Support `@readonly` as an alias of the `@phan-read-only` annotation. ++ Also emit `PhanImpossibleTypeComparison` for `int === float` checks. (#3106) ++ Emit `PhanSuspiciousMagicConstant` when using `__METHOD__` in a function instead of a method. ++ Check return types and parameter types of global functions which Phan has signatures for, + when `ignore_undeclared_functions_with_known_signatures` is `false` and `PhanUndeclaredFunction` is emitted. (#3441) + + Previously, Phan would emit `PhanUndeclaredFunction` without checking param or return types. ++ Emit `PhanImpossibleTypeComparison*` and `PhanSuspiciousWeakTypeComparison*` + when `in_array` or `array_search` is used in a way that will always return false. ++ Emit `PhanImpossibleTypeComparison*` when `array_key_exists` is used in a way that will always return false. + (e.g. checking for a string literal or negative key in a list, an integer in an array with known string keys, or anything in an empty array) ++ Add some missing function analyzers: Infer that `shuffle`, `rsort`, `natsort`, etc. convert arrays to lists. + Same for `arsort`, `krsort`, etc. ++ Convert to `list` or `associative-array` in `sort`/`asort` in more edge cases. ++ Infer that `sort`/`asort` on an array (and other internal functions using references) returns a real `list` or `associative-array`. + Infer that `sort`/`asort` on a non-empty array (and other internal functions using references) returns a real `non-empty-list` or `non-empty-associative-array`. ++ Infer that some array operations (`array_reduce`, `array_filter`, etc.) result in `array` instead of `non-empty-array` (etc.) + +Bug fixes: ++ Fix a bug where global functions, closures, and arrow functions may have inferred values from previous analysis unintentionally + left over in the original scope when analyzing that function again. (methods were unaffected) + +Maintenance: ++ Clarify a warning message about "None of the files to analyze in /path/to/project exist" + +Plugins: ++ Add a new plugin `RedundantAssignmentPlugin` to warn about assigning the same value a variable already has to that variable. (#3424) + New issue types: `PhanPluginRedundantAssignment`, `PhanPluginRedundantAssignmentInLoop`, `PhanPluginRedundantAssignmentInGlobalScope` ++ Warn about alignment directives and more padding directives (`'x`) without width directive in `PrintfCheckerPlugin` (#3317) ++ Also emit `PhanPluginPrintfNoArguments` in cases when the format string could not be determined. (#3198) + +Oct 26 2019, Phan 2.4.0 +----------------------- + +New features(CLI, Configs): ++ Support saving and loading baselines with `--save-baseline=.phan/baseline.php` and `--load-baseline=.phan/baseline.php`. (#2000) + `--save-baseline` will save all pre-existing issues for the provided analysis settings to a file. + When Phan is invoked later with `--load-baseline`, it will ignore any + issue kinds in the files from `file_suppressions` in the baseline. + + This is useful for setting up analysis with Phan on a new project, + or when enabling stricter analysis settings. + + Different baseline files can be used for different Phan configurations. + (e.g. `.phan/baseline_deadcode.php` for runs with `--dead-code-detection`) + +New features(Analysis): ++ Fix edge cases in checking if some nullable types were possibly falsey + (`?true` and literal floats (e.g. `?1.1`)) ++ Emit `PhanCoalescingNeverNull` instead of `PhanCoalescingNeverNullIn*` + if it's impossible for the node kind to be null. (#3386) ++ Warn about array destructuring syntax errors (`[] = $arr`, `[$withoutKey, 1 => $withKey] = $arr`) ++ Return a clone of an existing variable if one already exists in Variable::fromNodeInContext. (#3406) + This helps analyze `PassByReferenceVariable`s. ++ Don't emit PhanParamSpecial2 for min/max/implode/join with a single vararg. (#3396) ++ Properly emit PhanPossiblyInfiniteRecursionSameParams for functions with varargs. ++ Emit `PhanNoopNew` or `PhanNoopNewNoSideEffects` when an object is created with `new expr(...)` but the result is not used (#3410) + This can be suppressed for all instances of a class-like by adding the `@phan-constructor-used-for-side-effects` annotation to the class's doc comment. ++ Emit `PhanPluginUseReturnValueInternalKnown` for unused results of function calls on the right-hand side of control flow operators (`??`/`?:`/`&&`/`||`) (#3408) + +Oct 20 2019, Phan 2.3.1 +----------------------- + +New features(CLI, Configs): ++ Instead of printing the full help when Phan CLI args or configuration is invalid, + print just the errors/warnings and instructions and `Type ./phan --help (or --extended-help) for usage.` ++ Add an option `--debug-signal-handler` that can be used to diagnose + why Phan or a plugin is slow or hanging. (Requires the `pcntl` module) + This installs a signal handler that response to SIGINT (aka Ctrl-C), SIGUSR1, and SIGUSR2. ++ Print a single backtrace in the crash reporter with the file, line, and arguments instead of multiple backtraces. ++ Emit a warning suggesting using `--long-option` instead when `-long-option[=value]` is passed in. ++ Change colorization of some error messages. Print some warnings to stderr instead of using `error_log()`. + +New features(Analysis): ++ Emit `PhanTypeMismatchPropertyRealByRef` or `PhanTypeMismatchPropertyByRef` + when potentially assigning an incompatible type to a php 7.4 typed property + (or a property with a phpdoc type). ++ Warn about suspicious uses of `+` or `+=` on array shapes or lists. (#3364) + These operator will prefer the fields from the left hand side, + and will merge lists instead of concatenate them. + New issue types: `PhanSuspiciousBinaryAddLists`, `PhanUselessBinaryAddRight` ++ Improvements to inferred types of `sort`, `array_merge`, etc. (#3354) ++ Fix bug allowing any array shape type to cast to a list. ++ Warn about unnecessary branches leading to identical return statements in pure functions, methods, and closures (#3383) + This check is only run on pure methods. + + This requires that `UseReturnValuePlugin` be enabled and works best when `'plugin_config' => ['infer_pure_methods' => true]` is set. ++ Allow `list` to cast to `array{0:X, 1?:X}` (#3390) ++ Speed up computing line numbers of diagnostics in the polyfill/fallback parser when there are multiple diagnostics. + +Language Server/Daemon mode: ++ Reduce the CPU usage of the language server's main process when the `pcntl` module is used to fork analysis processes (Unix/Linux). ++ Speed up serializing large responses in language server mode (e.g. when a string has an unmatched quote). + +Oct 13 2019, Phan 2.3.0 +----------------------- + +New features(CLI, Configs): ++ Limit --debug-emitted-issues to the files that weren't excluded from analysis. + +New features(Analysis): ++ Add support for `list` and `non-empty-list` in phpdoc and in inferred values. + These represent arrays with consecutive integer keys starting at 0 without any gaps (e.g. `function (string ...$args) {}`) ++ Add support for `associative-array` and `non-empty-associative-array` in phpdoc and in inferred values. (#3357) + + These are the opposite of `list` and `non-empty-associative-list`. `list` cannot cast to `associative-array` and vice-versa. + These represent arrays that are unlikely to end up with consecutive integer keys starting at 0 without any gaps. + `associative-array` is inferred after analyzing code such as the following: + + - Expressions such as `[$uid1 => $value, $uid2 => $value2]` with unknown keys + - Unsetting an array key of a variable. + - Adding an unknown array key to an empty array. + - Certain built-in functions, such as `array_filter` or `array_unique`, + which don't preserve all keys and don't renumber array keys. + + Note that `array` is always treated like an associative array. + + However, `T[]` (i.e. `array`) is not treated like `associative-array` (i.e. `associative-array`). + Phan will warn about using the latter (`associative-array`) where a list is expected, but not the former (`array`). ++ Allow omitting keys from array shapes for sequential array elements + (e.g. `array{stdClass, array}` is equivalent to `array{0:stdClass, 1:array}`). ++ Add array key of array shapes in the same field order that php would for assignments such as `$x = [10]; $x[1] = 11;`. (#3359) ++ Infer that arrays are non-empty after analyzing code such as `$x[expr] = expr` or `$x[] = expr`. ++ Infer that arrays are possibly empty after analyzing code such as `unset($x[expr]);`. ++ Fix false positives in redundant condition detection when the source union type contains the `mixed` type. + +Oct 03 2019, Phan 2.2.13 +------------------------ + +New features(CLI, Configs): ++ Always print 100% in `--progress-bar` after completing any phase of analysis. + This is convenient for tools such as `tool/phoogle` that exit before starting the next phase. ++ Add GraphML output support to `DependencyGraphPlugin`. + This allows `tool/pdep` output to be imported by Neo4j, Gephi and yEd ++ Add json output and import to `tool/pdep` + For caching large graphs in order to generate multiple sub-graphs without re-scanning ++ Add setting `infer_default_properties_in_construct`. + When this is enabled, infer that properties of `$this` are initialized to their default values at the start of `__construct()`. (#3213) + (this is limited to instance properties which are declared in the current class (i.e. not inherited)). + Off by default. ++ Add a config setting `strict_object_checking`. (#3262) + When enabled, Phan will warn if some of the object types in the union type don't contain a property. + Additionally, warn about definite non-object types when accessing properties. + Also add `--strict-object-checking` to enable this setting. ++ Add CLI option `--debug-emitted-issues={basic,verbose}` to print the stack trace of when Phan emitted the issue to stderr. + Useful for understanding why Phan emitted an issue. + +New features(Analysis): ++ Disable `simplify_ast` by default. + Phan's analysis of compound conditions and assignments/negations in conditions has improved enough that it should no longer be necessary. ++ Import more specific phpdoc/real array return types for internal global functions from opcache. ++ Emit `PhanUndeclaredVariable` and other warnings about arguments when there are too many parameters for methods. (#3245) ++ Infer real types of array/iterable keys and values in more cases. ++ Expose the last compilation warning seen when tokenizing or parsing with the native parser, if possible (#3263) + New issue types: `PhanSyntaxCompileWarning` + Additionally, expose the last compilation warning or deprecation notice seen when tokenizing in the polyfill. ++ Improve inference of when the real result of a binary operation is a float. (#3256) ++ Emit stricter warnings for more real type mismatches (#3256) + (e.g. emit `PhanTypeMismatchArgumentReal` for `float->int` when `strict_types=1`, `'literal string'->int`, etc.) ++ Consistently infer that variadic parameters are arrays with integer keys. (#3294) ++ Improve types inferred when the config setting `enable_extended_internal_return_type_plugins` is enabled. ++ Speed up sorting the list of parsed files, and avoid unnecessary work in `--dump-parsed-file-list`. ++ Emit `PhanEmptyForeach` and `PhanEmptyYieldFrom` when iterating over empty arrays. ++ Infer that properties of `$this` are initialized to their default values at the start of `__construct()`. (#3213) + (this is limited to instance properties which are declared in the current class (i.e. not inherited)). + To disable this, set `infer_default_properties_in_construct` to false. ++ Improve analysis of conditions on properties of `$this`, such as `if (isset($this->prop['field1']['field2']))` (#3295) ++ Improve suggestions for `PhanUndeclaredFunction`. + Properly suggest similar global functions for non-fully qualified calls in namespaces. + Suggest `new ClassName()` as a suggestion for `ClassName()`. ++ Improve suggestions for global constants (`PhanUndeclaredConstant`). + Suggest similar constant names case-insensitively within the same namespace or the global namespace. ++ Suggest obvious getters and setters for instance properties in `PhanAccessPropertyProtected` and `PhanAccessPropertyPrivate` (#2540) ++ When `strict_method_checking` is enabled, + warn if some of the **object** types in the union type don't contain that method. (#3262) ++ Make stronger assumptions about real types of global constants. + Assume that constants defined with `define(...)` can have any non-object as its real type, + to avoid false positives in redundant condition detection. ++ Properly infer that parameter defaults and global constants will resolve to `null` in some edge cases. ++ Emit `PhanCompatibleDefaultEqualsNull` when using a different constant that resolves to null as the default of a non-nullable parameter. (#3307) ++ Emit `PhanPossiblyInfiniteRecursionSameParams` when a function or method calls itself with the same parameter values it was declared with (in a branch). (#2893) + (This requires unused variable detection to be enabled, when there are 1 or more parameters) ++ Analyze complex conditions such as `switch (true)`, `if (($x instanceof stdClass) == false)`, etc. (#3315) ++ Add a `non-empty-array` type, for arrays that have 1 or more elements. + This gets inferred for checks such as `if ($array)`, `if (!empty($array))` (checks on `count()` are not supported yet) + (`non-empty-array` and `non-empty-array` can also be used in phpdoc) ++ Support checking if comparisons of types with more than one possible literal scalar are redundant/impossible. + Previously, Phan would only warn if both sides had exactly one possible scalar value. + (e.g. warn about `'string literal' >= $nullableBool`) ++ Fix edge cases analyzing conditions on superglobals. ++ Be more consistent about when PhanTypeArraySuspiciousNullable is emitted, e.g. for `?mixed`, `array|null`, etc. ++ Fix false positive impossible condition for casting mixed to an array. + +Language Server/Daemon mode: ++ Fix logged Error when language server receives `didChangeConfiguration` events. (this is a no-op) + +Plugins: ++ Fix failure to emit `PhanPluginDescriptionlessComment*` when a description + would be automatically generated from the property or method's return type. (#3265) ++ Support checking for duplicate phpdoc descriptions of properties or methods within a class in `HasPHPDocPlugin`. + Set `'plugin_config' => ['has_phpdoc_check_duplicates' => true]` to enable these checks. + (this skips deprecated methods/properties) ++ Implement `LoopVariableReusePlugin`, to detect reusing loop variables in nested loops. (#3045) + (e.g. `for ($i = 0; $i < 10; $i++) { /* various code ... */ foreach ($array as $i => $value) { ... } }`) + +Maintenance: ++ Make `\Phan\Library\None` a singleton in internal uses. ++ Normalize folders in the config file generated by `phan --init` in the vendor autoload directories. ++ Update internal element types and documentation maps. + +Bug fixes: ++ Consistently deduplicate the real type set of union types (fixes some false positives in redundant condition detection). ++ Fix `\Phan\Debug`'s dumping representation of flags for `ast\AST_DIM`, `ast\AST_ARRAY_ELEM`, + `ast\AST_PARAM`, `ast\AST_ASSIGN_OP` (`??=`), and `ast\AST_CONDITIONAL`. + + This affects some crash reporting and tools such as `internal/dump_fallback_ast.php` ++ Fix some infinite recursion edge cases caused parsing invalid recursive class inheritance. (#3264) + +Sep 08 2019, Phan 2.2.12 +------------------------ + +New features(CLI): ++ Improve error messages when the `--init-*` flags are provided without passing `--init`. (#3153) + Previously, Phan would fail with a confusing error message. ++ New tool `tool/pdep` to visualize project dependencies - see `tool/pdep -h` + (uses the internal plugin `DependencyGraphPlugin`) ++ Support running `tool/phoogle` (search for functions/methods by signatures) in Windows. ++ Add support for `--limit ` and `--progress-bar` to `tool/phoogle`. + +New features(Analysis): ++ Support `@phan-immutable` annotation on class doc comments, to indicate that all instance properties are read-only. + + - Phan does not check if object fields of those immutable properties will change. (e.g. `$this->foo->prop = 'x';` is allowed) + - This annotation does not imply that methods have no side effects (e.g. I/O, modifying global state) + - This annotation does not imply that methods have deterministic return values or that methods' results should be used. + + `@phan-immutable` is an alias of `@phan-read-only`. `@phan-read-only` was previously supported on properties. ++ Support `@phan-side-effect-free` annotation on class doc comments, + to indicate that all instances of the class are `@phan-immutable` + and that methods of the class are free of external side effects. (#3182) + + - All instance properties are treated as read-only. + - Almost all instance methods are treated as `@phan-side-effect-free` - their return values must be used. + (excluding a few magic methods such as __wakeup, __set, etc.) + This does not imply that they are deterministic (e.g. `rand()`, `file_get_contents()`, and `microtime()` are allowed) ++ Add `@phan-side-effect-free` as a clearer name of what `@phan-pure` implied for methods. ++ Fix false positives for checking for redundant conditions with `iterable` and `is_iterable`. ++ Properly infer real types for `is_resource` checks and other cases where UnionType::fromFullyQualifiedRealString() was used. ++ Avoid false positives for the config setting `'assume_real_types_for_internal_functions'`. + Include all return types for many internal global functions for `--target-php-version` of `7.[0-4]`, + including those caused by invalid arguments or argument counts. ++ Warn about division, modulo, and exponentiation by 0 (or by values that would cast to 0). ++ Fix a bug converting absolute paths to relative paths when the project directory is a substring of a subdirectory (#3158) ++ Show the real signature of the abstract method in PhanClassContainsAbstractMethod issues. (#3152) ++ Support analyzing php 7.3's `is_countable()`, and warn when the check is redundant or impossible (#3172) ++ Don't suggest `$this->prop` as an alternative to the undeclared variable `$prop` from a static method/closure. (#3174) ++ Make real return types of `Closure::bind()` and other closure helpers more accurate. (#3184) ++ Include `use($missingVar)` in suggestions for `PhanUndeclaredVariable` if it is defined outside the closure(s) scope. + Also, suggest *hardcoded* globals such as `$argv`. ++ Warn about `$this instanceof self` and `$this instanceof static` being redundant. ++ Fix false positive `PhanInvalidConstantExpression` for php 7.4 argument unpacking (e.g. `function f($x = [1, ...SOME_CONST]) {}`) ++ Emit `PhanTypeMismatchArgumentInternalProbablyReal` when the real type of an argument doesn't match Phan's signature info for a function (#3199) + (but there is no Reflection type info for the parameter) + Continue emitting `PhanTypeMismatchArgumentInternal` when the real type info of the argument is unknown or is permitted to cast to the parameter. ++ Improve analysis of switch statements for unused variable detection and variable types (#3222, #1811) ++ Infer the value of `count()` for union types that have a real type with a single array shape. ++ Fix false positive `PhanSuspiciousValueComparisonInLoop` for value expressions that contain variables. ++ Warn about redundant condition detection in more cases in loops. ++ Warn about PHP 4 constructors such as `Foo::Foo()` if the class has no namespace and `__construct()` does not exist. (#740) + Infer that defining `Foo::Foo()` creates the method alias `Foo::__construct()`. ++ Don't emit `PhanTypeMismatchArgumentReal` if the only cause of the mismatch is nullability of real types (if phpdoc types were compatible) (#3231) + +Language Server/Daemon mode: ++ Ignore `'plugin_config' => ['infer_pure_methods' => true]` in language server and daemon mode. (#3220) + That option is extremely slow and memory intensive. + +Plugins: ++ If possible, suggest the types that Phan observed during analysis with `UnknownElementTypePlugin`. (#3146) ++ Make `InvalidVariableIssetPlugin` respect the `ignore_undeclared_variables_in_global_scope` option (#1403) + +Maintenance: ++ Correctly check for the number of cpus/cores on MacOS in Phan's unit tests (#3143) + +Bug fixes: ++ Don't parse `src/a.php` and `src\a.php` twice if both paths are generated from config or CLI options (#3166) + +Aug 18 2019, Phan 2.2.11 +------------------------ + +New features(Analysis): ++ Add a `@phan-real-return` annotation for functions/methods/closure (#3099), + to make Phan act as if that method has the specified union type + when analyzing callers for redundant conditions, etc. (if there was no real type). + This can be used for multiple types, e.g. `@phan-real-return string|false`. ++ Improve union type inferred for clone() - It must be an object if clone() doesn't throw. + Emit `PhanTypePossiblyInvalidCloneNotObject` for cloning possible non-objects when strict param checking is enabled. ++ Infer that `new $expr()` has a real type of object in all cases, not just common ones. ++ Improve real type inferred for `+(expr)`/`-(expr)`/`~(expr)` and warn about redundant conditions. + This does not attempt to account for custom behavior for objects provided by PECL extensions. ++ Show argument names and types in issue messages for functions/methods for `PhanParamTooFew` and `PhanParamTooMany`. ++ Show more accurate columns for `PhanSyntaxError` for unexpected tokens in more cases. ++ Ignore scalar and null type casting config settings when checking for redundant or impossible conditions. (#3105) ++ Infer that `empty($x)` implies that the value of $x is null, an empty scalar, or the empty array. ++ Avoid false positives with `if (empty($x['first']['second']))` - Do not infer any types for the offset 'first' if there weren't any already. (#3112) ++ Avoid some bad inferences when using the value of expressions of the form `A || B`. ++ Improve redundant condition detection for empty/falsey/truthy checks, `self`, and internal functions building or processing arrays. ++ Include strings that are suffixes of variable names, classes, methods, properties, etc. in issue suggestions for undeclared elements. (#2342) ++ Emit `PhanTypeNonVarReturnByRef` when an invalid expression is returned by a function declared to return a reference. ++ Support manually annotating that functions/methods/closures are pure with `/** @phan-pure */`. + This is automatically inherited by overriding methods. + Also see `UseReturnValuePlugin` and `'plugin_config' => ['infer_pure_methods' => true]`. + +Plugins: ++ In `UseReturnValuePlugin`, support inferring whether closures, functions, and methods are pure + when `'plugin_config' => ['infer_pure_methods' => true]` is enabled. + (they're expected to not have side effects and should have their results used) + + This is a best-effort heuristic. + This is done only for the functions and methods that are not excluded from analysis, + and it isn't done for methods that override or are overridden by other methods. + + Note that functions such as `fopen()` are not pure due to side effects. + UseReturnValuePlugin also warns about those because their results should be used. + + Automatic inference of function purity is done recursively. ++ Add `EmptyMethodAndFunctionPlugin` to warn about functions/methods/closures with empty statement lists. (#3110) + This does not warn about functions or methods that are deprecated, overrides, or overridden. ++ Fix false positive in InvalidVariableIssetPlugin for expressions such as `isset(self::$prop['field'])` (#3089) + +Maintenance: ++ Add example vim syntax highlighting snippet for Phan's custom phpdoc annotations to `plugins/vim/syntax/phan.vim` + This makes it easier to tell if annotations were correctly typed. + +Bug fixes: ++ Don't scan over folders that would be excluded by `'exclude_file_regex'` while parsing. (#3088) + That adds additional time and may cause unnecessary permissions errors. ++ Properly parse literal float union types starting with `0.` + +Aug 12 2019, Phan 2.2.10 +------------------------ + +New features(Analysis): ++ Add support for `@param MyClass &$x @phan-ignore-reference`, + to make Phan ignore the impact of references on the passed in argument. (#3082) + This can be used when the result should be treated exactly like the original type for static analysis. + +Plugins: ++ In EmptyStatementListPlugin, warn about switch statements where all cases are no-ops. (#3030) + +Bug fixes: ++ Fix infinite recursion seen when passing `void` to something expecting a non-null type. (#3085) + This only occurs with some settings, e.g. when `null_casts_as_any_type` is true. (introduced in 2.2.9) + +Aug 11 2019, Phan 2.2.9 +----------------------- + +New features(Analysis): ++ Emit the stricter issue type `PhanTypeMismatchReturnReal` instead of `PhanTypeMismatchReturn` + when Phan infers that the real type of the returned expression is likely to cause a TypeError (accounting for `strict_types` in the file). (#403) + See `internal/Issue-Types-Caught-by-Phan.md` for details on when it is thrown. ++ Emit the stricter issue type `PhanTypeMismatchArgumentReal` instead of `PhanTypeMismatchArgument` + when Phan infers that the real type of the argument is likely to cause a TypeError at runtime (#403) ++ Support php 7.4 typed property groups in the polyfill/fallback parser. ++ Warn about passing properties with incompatible types to reference parameters (#3060) + New issue types: `PhanTypeMismatchArgumentPropertyReference`, `PhanTypeMismatchArgumentPropertyReferenceReal` ++ Detect redundant conditions such as `is_array($this->array_prop)` on typed properties. + Their values will either be a value of the correct type, or unset. (Reading from unset properties will throw an Error at runtime) ++ Emit `PhanCompatibleTypedProperty` if the target php version is less than 7.4 but typed properties are used. ++ Emit `PhanTypeMismatchPropertyReal` instead of `PhanTypeMismatchProperty` if the properties have real types that are incompatible with the inferred type of the assignment. ++ Stop warning about `(float) $int` being redundant - there are small differences in how ints and floats are treated by `serialize`, `var_export`, `is_int`, etc. ++ Treat all assignments to `$this->prop` in a scope the same way (for real, dynamic, and magic properties) + Previously, Phan would not track the effects of some assignments to dynamic properties. ++ Make `unset($this->prop)` make Phan infer that the property is unset in the current scope (and treat it like null) (only affects `$this`). (#3025) + Emit `PhanPossiblyUnsetPropertyOfThis` if the property is read from without setting it. ++ Don't emit `PhanTypeArraySuspiciousNull` when array access is used with the null coalescing operator. (#3032) ++ Don't emit `PhanTypeInvalidDimOffset` when array access is used with the null coalescing operator. (#2123) ++ Make Phan check for `PhanUndeclaredTypeProperty` suppressions on the property's doc comment, not the class. (#3047) ++ Make inferred real/phpdoc types for results of division more accurate. ++ Improve analysis of for loops and while loops. + Account for the possibility of the loop iteration never occurring. (unless the condition is unconditionally true) ++ Fix some edge cases that can cause PhanTypeMismatchProperty (#3067, #1867) + If there was a phpdoc or real type, check against that instead when emitting issues. ++ Analyze assignments to fields of properties of `$this` (e.g. `$this->prop[] = 'value';`) + for correctness and for the new type combination. (#3059) ++ Infer that the `void` should be treated similarly to null + (in addition to existing checks, it's redundant to compare them to null). + Don't warn about `return null;` in functions/methods with phpdoc-only `@return void`. + +Plugins: ++ Add `StrictComparisonPlugin`, which warns about the following issue types: + + 1. Using `in_array` or `array_search` without specifying `$strict`. (`PhanPluginComparisonNotStrictInCall`) + 2. Using comparison or weak equality operators when both sides are possibly objects. (`PhanPluginComparisonObjectEqualityNotStrict`, `PhanPluginComparisonObjectOrdering`) ++ Don't warn in `EmptyStatementListPlugin` if a TODO/FIXME/"Deliberately empty" comment is seen around the empty statement list. (#3036) + (This may miss some TODOs due to `php-ast` not providing the end line numbers) + The setting `'plugin_config' => ['empty_statement_list_ignore_todos' => true]` can be used to make it unconditionally warn about empty statement lists. ++ Improve checks for UseReturnValuePlugin for functions where warning depend on their arg count (`call_user_func`, `trait`/`interface`/`class_exists`, `preg_match`, etc) + +Bug fixes: ++ When a typed property has an incompatible default, don't infer the union type from the default. (#3024) ++ Don't emit `PhanTypeMismatchProperties` for assignments to dynamic properties. (#3042) ++ Fix false positive RedundantConditions analyzing properties of `$this` in the local scope. (#3038) ++ Properly infer that real type is always `int` (or a subtype) after the `is_int($var)` condition. ++ Emit `TypeMismatchUnpack*` for nullable key types of iterables if the union type didn't contain any int/mixed types. (fix logic error) + +Jul 30 2019, Phan 2.2.8 +----------------------- + +New features(CLI): ++ Add heuristics to `tool/phoogle` to better handle `object`, and to include functions with nullable params in the results of searches for all functions. (#3014) + +New features(Analysis): ++ Emit `PhanCompatibleImplodeOrder` when the glue string is passed as the second instead of the first argument (#2089) ++ Emit `PhanCompatibleDimAlternativeSyntax` when using array and string array access syntax with curly braces + when using the polyfill parser or php 7.4+. (#2989) ++ Emit `PhanCompatibleUnparenthesizedTernary` for expressions such as `a ? b : c ? d : e`. (#2989) + (when using the polyfill parser or php 7.4+) ++ Emit `PhanConstructAccessSignatureMismatch` when a constructor is less visible than the parent class's constructor + and the target php version is 7.1 or older. (#1405) + +Plugins: ++ Make `EmptyStatementListPlugin` check `if` statements with negated conditions (those were previously skipped because they were simplified). + +Bug fixes: ++ Fix a crash analyzing a dynamic property by reference (introduced in 2.2.7) (#3020) + +Jul 27 2019, Phan 2.2.7 +----------------------- + +New features(CLI, Configs): ++ Include columns with most (but not all) occurrences of `PhanSyntaxError` + (inferred using the polyfill - these may be incorrect a small fraction of the time) + + When the error is from the native `php-ast` parser, this is a best guess at the column. + + `hide_issue_column` can be used to remove the column from issue messages. ++ Add `--absolute-path-issue-messages` to emit absolute paths instead of relative paths for the file of an issue. (#1640) + Note that this does not affect files within the issue message. ++ Properly render the progress bar when Phan runs with multiple processes (#2928) ++ Add an HTML output mode to generate an unstyled HTML fragment. + Example CSS styles can be generated with `internal/dump_html_styles.php` ++ Add a `light` color scheme for white backgrounds. + +New features(Analysis): ++ Fix failure to infer real types when an invoked function or method had a phpdoc `@return` in addition to the real type. ++ Infer union type from all classes that an instance method could possibly be, not just the first type seen in the expression's union type. (#2988) ++ Preserve remaining real union types after negation of `instanceof` checks (e.g. to check for redundant conditions). ++ Warn about throwing from `__toString()` in php versions prior to php 7.4. (#2805) ++ Emit `PhanTypeArraySuspiciousNull` for code such as `null['foo']` (#2965) ++ If a property with no phpdoc type has a default of an empty array, assume that it's type can be any array (when reading it) until the first assignment is seen. ++ Attempt to analyze modifying dynamic properties by reference (e.g. `$var->$prop` when $prop is a variable with a known string) ++ For undeclared variables in the global scope, emit `PhanUndeclaredGlobalVariable` instead of `PhanUndeclaredVariable` to distinguish those from undeclared variables within functions/methods. (#1652) ++ Emit `PhanCompatibleSyntaxNotice` for notices such as the deprecated `(real)` cast in php 7.4, when the real parser is used (#3012) + +Language Server/Daemon mode: ++ When `PhanSyntaxError` is emitted, make the start of the error range + the column of the error instead of the start of the line. + +Plugins: ++ Add `EmptyStatementListPlugin` to warn about empty statement lists involving if/elseif statements, try statements, and loops. ++ Properly warn about redundant `@return` annotations followed by other annotation lines in `PHPDocRedundantPlugin`. + +Bug fixes: ++ Treat `Foo::class` as a reference to the class/interface/trait `Foo` (#2945) ++ Fix crash for `(real)` cast in php 7.4. (#3012) ++ Work around crash due to deprecation notices in composer dependencies in php 7.4 + +Jul 17 2019, Phan 2.2.6 +----------------------- + +New features(CLI, Configs): ++ Include files in completion suggestions for `-P`/`--plugin` in the [completion script for zsh](plugins/zsh/_phan). + +Bug fixes: ++ Fix crash analyzing `&&` and `||` conditions with literals on both sides (#2975) ++ Properly emit `PhanParamTooFew` when analyzing uses of functions/methods where a required parameter followed an optional parameter. (#2978) + +Jul 14 2019, Phan 2.2.5 +----------------------- + +New features(CLI, Configs): ++ Add `-u` as an alias of `--unused-variable-detection`, and `-t` as an alias of `--redundant-condition-detection` ++ Added a zsh completion script ([`plugins/zsh/_phan`](plugins/zsh/_phan) has installation instructions). ++ Added a bash completion script ([`plugins/bash/phan`](plugins/bash/phan) has installation instructions). + +New features(Analysis): ++ Fix false positive `PhanSuspiciousValueComparisonInLoop` when both sides change in a loop. (#2919) ++ Detect potential infinite loops such as `while (true) { does_not_exit_loop(); }`. (Requires `--redundant-condition-detection`) + New issue types: `PhanInfiniteRecursion`. ++ Track that the **real** type of an array variable is an array after adding fields to it (#2932) + (affects redundant condition detection and unused variable detection) ++ Warn about adding fields to an unused array variable, if Phan infers the real variable type is an array. (#2933) ++ Check for `PhanInfiniteLoop` when the condition expression is omitted (e.g. `for (;;) {}`) ++ Avoid false positives in real condition checks from weak equality checks such as `if ($x == null) { if ($x !== null) {}}` (#2924) ++ Warn about `X ? Y : Y` and `if (cond1) {...} elseif (cond1) {...}` in DuplicateExpressionPlugin (#2955) ++ Fix failure to infer type when there is an assignment (or `++$x`, or `$x OP= expr`) in a condition (#2964) + (e.g. `return ($obj = maybeObj()) instanceof stdClass ? $obj : new stdClass();`) ++ Warn about no-ops in for loops (e.g. `for ($x; $x < 10, $x < 20; $x + 1) {}`) (#2926) ++ Treat `compact('var1', ['var2'])` as a usage of $var1 and $var2 in `--unused-variable-detection` (#1812) + +Bug fixes: ++ Fix crash in StringUtil seen in php 7.4-dev due to notice in `hexdec()` (affects polyfill/fallback parser). + +Plugins: ++ Add `InlineHTMLPlugin` to warn about inline HTML anywhere in an analyzed file's contents. + In the `plugin_config` config array, `inline_html_whitelist_regex` and `inline_html_blacklist_regex` can be used to limit the subset of analyzed files to check for inline HTML. ++ For `UnusedSuppressionPlugin`: `'plugin_config' => ['unused_suppression_whitelisted_only' => true]` will make this plugin report unused suppressions only for issues in `whitelist_issue_types`. (#2961) ++ For `UseReturnValuePlugin`: warn about unused results of function calls in loops (#2926) ++ Provide the `$node` causing the call as a 5th parameter to closures returned by `AnalyzeFunctionCallCapability->getAnalyzeFunctionCallClosuresStatic` + (this can be used to get the variable/expression for an instance method call, etc.) + +Maintenance: ++ Made `--polyfill-parse-all-element-doc-comments` a no-op, it was only needed for compatibility with running Phan with php 7.0. ++ Minor updates to CLI help for Phan. ++ Restart without problematic extensions unless the corresponding `PHAN_ALLOW_$extension` flag is set. (#2900) + These include uopz and grpc (when Phan would use `pcntl_fork`) - Phan already restarts without Xdebug. ++ Fix `Debug::nodeToString()` - Make it use a polyfill for `ast\get_kind_name` if the php-ast version is missing or outdated. + +Jul 01 2019, Phan 2.2.4 +----------------------- + +New features(CLI, Configs): ++ Warn if any of the files passed in `--include-analysis-file-list` don't exist. + +New features(Analysis): ++ Reduce false positives inferring the resulting type of `$x++`, `--$x`, etc. (#2877) ++ Fix false positives analyzing variable modification in `elseif` conditions (#2878, #2860) + (e.g. no longer emit `PhanRedundantCondition` analyzing `elseif ($offset = (int)$offset)`) + (e.g. do a better job inferring variables set in complex `if` condition expressions) ++ Warn about suspicious comparisons (e.g. `new stdClass() <= new ArrayObject`, `2 >= $bool`, etc.) (#2892) ++ Infer real union types from function/method calls. ++ Don't emit the specialized `*InLoop` or `*InGlobalScope` issues for `--redundant-condition-detection` + in more cases where being in a global or loop scope doesn't matter (e.g. `if (new stdClass())`) ++ Be more accurate about inferring real union types from array destructuring assignments. (#2901) ++ Be more accurate about inferring real union types from assertions that expressions are non-null. (#2901) ++ Support dumping Phan's internal representation of a variable's union type (and real union type) with `'@phan-debug-var $varName'` (useful for debugging) ++ Fix false positive `PhanRedundantCondition` analyzing `if ([$a] = (expr))` (#2904) ++ Warn about suspicious comparisons that are always true or always false, e.g. the initial check for `for ($i = 100; $i < 20; $i++)` (#2888) ++ Emit `PhanSuspiciousLoopDirection` when a for loop increases a variable, but the variable is checked against a maximum (or the opposite) (#2888) + e.g. `for ($i = 0; $i <= 10; $i--)` ++ Emit critical errors for duplicate use for class/namespace, function, or constant (#2897) + New issue types: `PhanDuplicateUseNormal`, `PhanDuplicateUseFunction`, `PhanDuplicateUseConstant` ++ Emit `PhanCompatibleUnsetCast` for uses of the deprecated `(unset)(expr)` cast. (#2871) ++ Emit `PhanDeprecatedClass`, `PhanDeprecatedTrait`, and `PhanDeprecatedInterface` on the class directly inheriting from the deprecated class, trait, or interface. (#972) + Stop emitting that issue when constructing a non-deprecated class inheriting from a deprecated class. ++ Include the deprecation reason for user-defined classes that were deprecated (#2807) ++ Fix false positives seen when non-template class extends a template class (#2573) + +Language Server/Daemon mode: ++ Fix a crash - always run the language server or daemon with a single analysis process, regardless of CLI or config settings (#2898) ++ Properly locate the defining class for `MyClass::class` when the polyfill/fallback is used. ++ Don't emit color in responses from the daemon or language server unless the CLI flag `--color` is passed in. + +Maintenance: ++ Warn if running Phan with php 7.4+ when the installed php-ast version is older than 1.0.2. ++ Make the AST caches for dev php versions (e.g. 7.4.0-dev, 8.0.0-dev) depend on the date when that PHP version was compiled. ++ Make the polyfill support PHP 7.4's array spread operator (e.g. `[$a, ...$otherArray]`) (#2786) ++ Make the polyfill support PHP 7.4's short arrow functions (e.g. `fn($x) => $x*2`) ++ Fix parsing of `some_call(namespace\func_name())` in the polyfill + +Jun 17 2019, Phan 2.2.3 +----------------------- + +New features(Analysis): ++ Reduce false positives about `float` not casting to `int` introduced in 2.2.2 + +Jun 17 2019, Phan 2.2.2 +----------------------- + +New features(Analysis): ++ Support inferring literal float types. Warn about redundant conditions with union types of floats that are always truthy/falsey. + +Maintenance: ++ Support parsing PHP 7.4's numeric literal separator (e.g. `1_000_000`, `0xCAFE_F00d`, `0b0101_1111`) in the polyfill (#2829) + +Bug fixes: ++ Fix a crash in the Phan daemon on Mac/Linux (#2881) + +Jun 16 2019, Phan 2.2.1 +----------------------- + +New features(CLI, Configs): ++ When printing help messages for errors in `phan --init`, print only the related options. ++ Make `phan --init` enable `redundant_condition_detection` when the strictest init level is requested. (#2849) ++ Add `--assume-real-types-for-internal-functions` to make stricter assumptions about the real types of internal functions (for use with `--redundant-condition-detection`). + Note that in PHP 7 and earlier, internal functions would return null/false for incorrect argument types/argument counts, so enabling this option may cause false positives. + +New features(Analysis): ++ Reduce the number of false positives of `--redundant-condition-detection` for variables in loops ++ Warn about more types of expressions causing redundant conditions (#2534, #822). ++ Emit `PhanRedundantCondition` and `PhanImpossibleCondition` for `$x instanceof SomeClass` expressions. ++ Emit `PhanImpossibleCondition` for `is_array` and `is_object` checks. + +Bug fixes: ++ Fix issue that would make Phan infer that a redundant/impossible condition outside a loop was in a loop. ++ Avoid false positives analyzing expressions within `assert()`. ++ Fix method signatures for php 7.4's `WeakReference`. ++ Fix false positives analyzing uses of `__call` and `__callStatic` (#702) ++ Fix false positive redundant conditions for casting `callable` to object types. + +Jun 14 2019, Phan 2.2.0 +----------------------- + +New features(CLI, Configs): ++ Add `--color-scheme ` for alternative colors of outputted issues (also configurable via environment variable as `PHAN_COLOR_SCHEME=`) + Supported values: `default`, `vim`, `eclipse_dark` ++ Be consistent about starting parameter/variable names with `$` in issue messages. ++ Add `--redundant-condition-detection` to attempt to detect redundant conditions/casts and impossible conditions based on the inferred real expression types. + +New features(Analysis): ++ New issue types: `PhanRedundantCondition[InLoop]`, `PhanImpossibleCondition[InLoop]` (when `--redundant-condition-detection` is enabled) + (e.g. `is_int(2)` and `boolval(true)` is redundant, `empty(2)` is impossible). + + Note: This has many false positives involving loops, variables set in loops, and global variables. + This will be split into more granular issue types later on. + + The real types are inferred separately (and more conservatively) from regular (phpdoc+real) expression types. + + (these checks can also be enabled with the config setting `redundant_condition_detection`) ++ New issue types: `PhanImpossibleTypeComparison[InLoop]` (when `--redundant-condition-detection` is enabled) (#1807) + (e.g. warns about `$x = new stdClass(); assert($x !== null)`) ++ New issue types: `PhanCoalescingAlwaysNull[InLoop]`, `PhanCoalescingNeverNull[InLoop]` (when `--redundant-condition-detection` is enabled) + (e.g. warns about `(null ?? 'other')`, `($a >= $b) ?? 'default'`) ++ Infer real return types from Reflection of php and the enabled extensions (affects `--redundant-condition-detection`) ++ Make Phan more accurately infer types for reference parameters set in conditionals. ++ Make Phan more accurately infer types after try-catch blocks. ++ Make Phan more accurately check if a loop may be executed 0 times. ++ Fix issue causing results of previous method analysis to affect subsequent analysis in some edge cases (#2857) ++ Support the type `callable-object` in phpdoc and infer it from checks such as `is_callable($var) && is_object($var)` (#1336) ++ Support the type `callable-array` in phpdoc and infer it from checks such as `is_callable($var) && is_array($var)` (#2833) ++ Fix false positives in more edge cases when analyzing variables with type `static` (e.g. `yield from $this;`) (#2825) ++ Properly emit `NonStaticCallToStatic` in more edge cases (#2826) ++ Infer that `<=>` is `-1|0|1` instead of `int` ++ Infer that eval with backticks is `?string` instead of `string` + +Maintenance: ++ Add updates to the function/method signature map from Psalm and PHPStan. + +Bug fixes: ++ Fix a crash that occurred when an expression containing `class-string` became nullable. + +01 Jun 2019, Phan 2.1.0 +----------------------- + +New features(CLI, Configs): ++ Add more options to configure colorized output. (#2799) + + The environment variable `PHAN_ENABLE_COLOR_OUTPUT=1` and the config setting `color_issue_messages_if_supported` can be used to enable colorized output by default + for the default output mode (`text`) when the terminal supports it. + + This can be disabled by setting `PHAN_DISABLE_COLOR_OUTPUT=1` or by passing the flag `--no-color`. ++ Colorize output of `--help` and `--extended-help` when `--color` is used or the terminal supports it. + This can be disabled by setting `PHAN_DISABLE_COLOR_OUTPUT=1` or by passing the flag `--no-color`. + +New features(Analysis): ++ Support unary and binary expressions on literals/constants in conditions. (#2812) + (e.g. `assert($x === -(1))` and `assert($x === 2+2)` now infer that $x is -1 and 4, respectively) ++ Infer that static variables with no default are `null`. ++ Improve control flow analysis of unconditionally true/false branches. ++ Improve analysis of some ways to initialize groups of static variables. + e.g. `static $a = null; static $b = null; if ($a === null) { $a = $b = rand(0,10); } use($a, $b)` + will now also infer that $b is non-null. ++ Infer from `return new static();` and `return $this;` that the return type of a method is `@return static`, not `@return self` (#2797) + (and propagate that to inherited methods) ++ Fix some false positives when casting array types containing `static` to types containing the class or its ancestors. (#2797) ++ Add `PhanTypeInstantiateAbstractStatic` and `PhanTypeInstantiateTraitStaticOrSelf` as lower-severity warnings about `return new self()` and `return new static()` (#2797) + (emitted in static methods of abstract classes) ++ Fix false positives passing `static` to other classes. (#2797) ++ Fix false positive seen when `static` implements `ArrayAccess` (#2797) + +Language Server/Daemon mode: ++ Add `--language-server-min-diagnostics-delay-ms `, to work around race conditions in some language clients. + +20 May 2019, Phan 2.0.0 +----------------------- + +New features(Analysis): ++ Add early support for PHP 7.4's typed properties. (#2314) + (This is incomplete, and does not support inheritance, assignment, impossible conditions, etc.) ++ Change warnings about undeclared `$this` into a critical `PhanUndeclaredThis` issue. (#2751) ++ Fix the check for `PhanUnusedVariableGlobal` (#2768) ++ Start work on supporting analyzing PHP 7.4's unpacking inside arrays. (e.g. `[1, 2, ...$arr1, 5]`) (#2779) + NOTE: This does not yet check all types of errors, some code is unmigrated, and the polyfill does not yet support this. ++ Improve the check for invalid array unpacking in function calls with iterable/Traversable parameters. (#2779) + +Plugins: ++ Improve help messages for `internal/dump_fallback_ast.php` (this tool may be of use when developing plugins) + +Bug fixes: ++ Work around issues parsing binary operators in PHP 7.4-dev. + Note that the latest version of php-ast (currently 1.0.2-dev) should be installed if you are testing Phan with PHP 7.4-dev. + +13 May 2019, Phan 2.0.0-RC2 +----------------------- + +New features(Analysis): ++ Support analysis of PHP 7.4's short arrow function syntax (`fn ($arg) => expr`) (#2714) + (requires php-ast 1.0.2dev or newer) + + Note that the polyfill does not yet support this syntax. ++ Infer the return types of PHP 7.4's magic methods `__serialize()` and `__unserialize()`. (#2755) + Improve analysis of return types of other magic methods such as `__sleep()`. ++ Support more of PHP 7.4's function signatures (e.g. `WeakReference`) (#2756) ++ Improve detection of unused variables inside of loops/branches. + +Plugins: ++ Detect some new php 7.3 functions (`array_key_first`, etc.) in `UseReturnValuePlugin`. ++ Don't emit a `PhanNativePHPSyntaxCheckPlugin` error in `InvokePHPNativeSyntaxCheckPlugin` due to a shebang before `declare(strict_types=1)` ++ Fix edge cases running `PhanNativePHPSyntaxCheckPlugin` on Windows (in language server/daemon mode) + +Bug fixes: ++ Analyze the remaining expressions in a statement after emitting `PhanTraitParentReference` (#2750) ++ Don't emit `PhanUndeclaredVariable` within a closure if a `use` variable was undefined outside of it. (#2716) + +09 May 2019, Phan 2.0.0-RC1 +----------------------- + +New features(CLI, Configs): ++ Enable language server features by default. (#2358) + `--language-server-disable-go-to-definition`, `--language-server-disable-hover`, and `--language-server-disable-completion` + can be used to disable those features. + +Backwards Incompatible Changes: ++ Drop support for running Phan with PHP 7.0. (PHP 7.0 reached its end of life in December 2018) + Analyzing codebases with `--target-php-version 7.0` continues to be supported. ++ Require php-ast 1.0.1 or newer (or the absence of php-ast with `--allow-polyfill-parser`) + Phan switched from using [AST version 50 to version 70](https://github.com/nikic/php-ast#ast-versioning). + +Plugins: ++ Change `PluginV2` to `PluginV3` + `PluginV2` and its capabilities will continue to work to make migrating to Phan 2.x easier, but `PluginV2` is deprecated and will be removed in Phan 3. + + `PluginV3` has the same APIs and capabilities as PluginV2, but uses PHP 7.1 signatures (`void`, `?MyClass`, etc.) ++ Third party plugins may need to be upgraded to support changes in AST version 70, e.g. the new node kinds `AST_PROP_GROUP` and `AST_CLASS_NAME` ++ Add `PHPDocToRealTypesPlugin` to suggest real types to replace (or use alongside) phpdoc return types. + This does not check that the phpdoc types are correct. + + `--automatic-fix` can be used to automate making these changes for issues that are not suppressed. ++ Add `PHPDocRedundantPlugin` to detect functions/methods/closures where the doc comment just repeats the types in the signature. + (or when other parts don't just repeat information, but the `@return void` at the end is redundant) ++ Add a `BeforeAnalyzePhaseCapability`. Unlike `BeforeAnalyzeCapability`, this will run after methods are analyzed, not before. + +09 May 2019, Phan 1.3.4 +----------------------- + +Bug fixes: ++ Fix bug in Phan 1.3.3 causing polyfill parser to be used if the installed version of php-ast was older than 1.0.1. + +08 May 2019, Phan 1.3.3 +----------------------- + +New features(CLI, Configs): ++ Make the progress bar guaranteed to display 100% at the end of the analysis phase (#2694) + Print a newline to stderr once Phan is done updating the progress bar. ++ Add `maximum_recursion_depth` - This setting specifies the maximum recursion depth that + can be reached during re-analysis. + Default is 2. ++ Add `--constant-variable-detection` - This checks for variables that can be replaced with literals or constants. (#2704) + This is almost entirely false positives in most coding styles, but may catch some dead code. ++ Add `--language-server-disable-go-to-definition`, `--language-server-disable-hover`, and `--language-server-disable-completion` + (These are already disabled by default, but will be enabled by default in Phan 2.0) + +New features(Analysis): ++ Emit `PhanDeprecatedClassConstant` for code using a constant marked with `@deprecated`. ++ When recursively inferring the return type of `BaseClass::method()` from its return statements, + make that also affect the inherited copies of that method (`SubClass::method()`). (#2718) + This change is limited to methods with no return type in the phpdoc or real signature. ++ Improve unused variable detection: Detect more unused variables for expressions such as `$x++` and `$x -= 2` (#2715) ++ Fix false positive `PhanUnusedVariable` after assignment by reference (#2730) ++ Warn about references, static variables, and uses of global variables that are probably unnecessary (never used/assigned to afterwards) (#2733) + New issue types: `PhanUnusedVariableReference`, `PhanUnusedVariableGlobal`, `PhanUnusedVariableStatic` ++ Warn about invalid AST nodes for defaults of properties and static variables. (#2732) ++ Warn about union types on properties that might have an incomplete suffix. (e.g. `/** @var arrayprop` inside of function scopes. (#805, #204) + + This supports only one level of nesting. (i.e. Phan will not track `$this->prop->subProp` or `$this->prop['field']`) + + Properties are deliberately tracked for just the variable `$this` (which can't be reassigned), and not other variables. ++ Fix false positives with dead code detection for internal stubs in `autoload_internal_extension_signatures`. (#2605) ++ Add a way to escape/unescape array shape keys (newlines, binary data, etc) (#1664) + + e.g. `@return array{\n\r\t\x01\\:true}` in phpdoc would correspond to `return ["\n\r\t\x01\\" => true];` + +Plugins: ++ Add `FFIAnalysisPlugin` to avoid false positives in uses of PHP 7.4's `FFI\CData` (#2659) + (C data of scalar types may be read and assigned as regular PHP data. `$x = FFI::new(“int”); $x = 42;`) + + Note that this is only implemented for variables right now. + +20 Apr 2019, Phan 1.3.1 +----------------------- + +New features(Analysis): ++ Fix false positive `PhanTypeMismatchReturnNullable` and `PhanTypeMismatchArgumentNullable` introduced in 1.3.0 (#2667) ++ Emit `PhanPossiblyNullTypeMismatchProperty` instead of `PhanTypeMismatchProperty` when assigning `?T` + to a property expecting a compatible but non-nullable type. + + (The same issue was already emitted when the internal union type representation was `T|null` (not `?T`) and strict property type checking was enabled) + +Plugins: ++ Add `PossiblyStaticMethodPlugin` to detect instance methods that can be changed to static methods (#2609) ++ Fix edge cases checking if left/right-hand side of binary operations are numbers in `NumericalComparisonPlugin` + +19 Apr 2019, Phan 1.3.0 +----------------------- + +New features(Analysis): ++ Fix false positive `UnusedSuppression` when a doc comment suppresses an issue about itself. (#2571) ++ Improve analysis of argument unpacking with reference parameters, fix false positive `PhanTypeNonVarPassByRef` (#2646) ++ In issue descriptions and suggestions, replace invalid utf-8 (and literal newlines) with placeholders (#2645) ++ Suggest typo fixes in `PhanMisspelledAnnotation` for `@phan-*` annotations. (#2640) ++ Emit `PhanUnreferencedClass` when the only references to a class or its elements are within that class. + Previously, it would fail to be emitted when a class referenced itself. ++ Emit `PhanUnusedPublicNoOverrideMethodParameter` for method parameters that are not overridden and are not overrides. (#2539) + + This is expected to have a lower false positive rate than `PhanUnusedPublicMethodParameter` because parameters + might be unused by some of the classes overriding/implementing a method. + + Setting `unused_variable_detection_assume_override_exists` to true in `.phan/config.php` can be used to continue emitting the old issue names instead of `*NoOverride*` equivalents. ++ Warn about more numeric operations(+, /, etc) on unknown strings and non-numeric literal strings (#2656) + The settings `scalar_implicit_cast` and `scalar_implicit_partial` affect this for the `string` union type but not for literals. ++ Improve types inferred from checks such as `if (is_array($var['field'])) { use($var['field']); }` and `if ($var['field'] instanceof stdClass) {...}` (#2601) ++ Infer that $varName is non-null and an object for conditions such as `if (isset($varName->field['prop']))` ++ Be more consistent about warning when passing `?SomeClass` to a parameter expecting non-null `SomeClass`. ++ Add `PhanTypeMismatchArgumentNullable*` and `PhanTypeMismatchReturnNullable` when the main reason the type check failed was nullability + + Previously, Phan would fail to detect that some nullable class instances were incompatible with the non-null expected types in some cases. ++ Improve analysis of negation of `instanceof` checks on nullable types. (#2663) + +Language Server/Daemon mode: ++ Analyze new but unsaved files, if they would be analyzed by Phan once they actually were saved to disk. + +Plugins: ++ Warn about assignments where the left-hand and right-hand side are the same expression in `DuplicateExpressionPlugin` (#2641) + New issue type: `PhanPluginDuplicateExpressionAssignment` + +Deprecations: ++ Print a message to stderr if the installed php-ast version is older than 1.0.1. + A future major Phan version of Phan will probably depend on AST version 70 to support new syntax found in PHP 7.4. ++ Print a message to stderr if the installed PHP version is 7.0. + A future major version of Phan will require PHP 7.1+ to run. + + Phan will still continue to support setting `target_php_version` to `'7.0'` and `--target-php-version 7.0` in that release. + +Bug fixes: ++ Fix edge cases in how Phan checks if files are in `exclude_analysis_directory_list` (#2651) ++ Fix crash parsing comma in string literal in array shape (#2597) + (e.g. `@param array{0:'test,other'} $x`) + +06 Apr 2019, Phan 1.2.8 +----------------------- + +New features(CLI): ++ Fix edge cases initializing directory list and target versions of config files (#2629, #2160) + +New features(Analysis): ++ Support analyzing `if (false !== is_string($var))` and similar complex conditions. (#2613) ++ Emit `PhanUnusedGotoLabel` for labels without a corresponding `goto` in the same function scope. (#2617) + (note that Phan does not understand the effects of goto on control flow) ++ Don't emit `PhanUnreferencedClass` for anonymous classes. (#2604) ++ Detect undeclared types in phpdoc callables and closures. (#2562) ++ Warn about unreferenced PHPDoc `@property`/`@property-read`/`@property-write` annotations in `--dead-code-detection`. + New issue types: `PhanWriteOnlyPHPDocProperty`, `PhanReadOnlyPHPDocProperty`, `PhanUnreferencedPHPDocProperty`. + +Maintenance: ++ Make escaped string arguments fit on a single line for more issue types. ++ Rename `UseContantNoEffect` to `UseConstantNoEffect`. ++ Rename `AddressableElement::isStrictlyMoreVisibileThan()` to `isStrictlyMoreVisibleThan`. + +Plugins: ++ Fix edge case where `WhitespacePlugin` would not detect trailing whitespace. ++ Detect `PhanPluginDuplicateSwitchCaseLooseEquality` in `DuplicateArrayKeyPlugin`. (#2310) + Warn about cases of switch cases that are loosely equivalent to earlier cases, and which might get unexpectedly missed because of that. (e.g. `0` and `'foo'`) + +Bug fixes: ++ Catch and handle "Cannot access parent when not in object context" when parsing global functions incorrectly using `parent` parameter type. (#2619) ++ Improve the performance of `--progress-bar` when the terminal width can't be computed by symfony. (#2634) + +22 Mar 2019, Phan 1.2.7 +----------------------- + +New features(CLI,Configs) ++ Use a progress bar for `--progress-bar` on Windows instead of printing dots. (#2572) + Use ASCII characters for the progress bar instead of UTF-8 if the code page isn't utf-8 or if Phan can't infer the terminal's code page (e.g. in PHP < 7.1) + +Language Server/Daemon mode: ++ Make "Go to Definition" work when the constructor of a user-defined class is inherited from an internal class. (#2598) + +Maintenance: ++ Update tolerant-php-parser version to 0.0.17 + (fix parsing of some edge cases, minor performance improvement, prepare to support php 7.4 in polyfill) ++ Use paratest for phpunit tests in Travis/Appveyor + +Bug fixes: ++ Make the codeclimate plugin analyze the correct directory. Update the dependencies of the codeclimate plugin. (#2139) ++ Fix false positive checking for undefined offset with `$foo['strVal']` when strings are in the union type of `$foo` (#2541) ++ Fix crash in analysis of `call_user_func` (#2576) ++ Fix a false positive PhanTypeInvalidDimOffset for `unset` on array fields in conditional branches. (#2591) ++ Fix edge cases where types for variables inferred in one branch affect unrelated branches (#2593) + +09 Mar 2019, Phan 1.2.6 +----------------------- + +New features(CLI,Configs) ++ Add config `enable_extended_internal_return_type_plugins` to more aggressively + infer literal values for functions such as `json_decode`, `strtolower`, `implode`, etc. (disabled by default), ++ Make `--dead-code-detection` load `UnreachableCodePlugin` if that plugin isn't already loaded (#1824) ++ Add `--automatic-fix` to fix any issues Phan is capable of fixing + (currently a prototype. Fixes are guessed based on line numbers). + This is currently limited to: + - unreferenced use statements on their own line (requires `--dead-code-detection`). + - issues emitted by `WhitespacePlugin` (#2523) + - unqualified global function calls/constant uses from namespaces (requires `NotFullyQualifiedUsagePlugin`) + (will do the wrong thing for functions that are both global and in the same namespace) + +New features(Analysis): ++ Make Phan infer more precise literal types for internal constants such as `PHP_EOF`. + These depend on the PHP binary used to run Phan. + + In most cases, that shouldn't matter. ++ Emit `PhanPluginPrintfVariableFormatString` in `PrintfCheckerPlugin` if the inferred format string isn't a single literal (#2431) ++ Don't emit `PhanWriteOnlyPrivateProperty` with dead code detection when at least one assignment is by reference (#1658) ++ Allow a single hyphen between words in `@suppress issue-name` annotations (and `@phan-suppress-next-line issue-name`, etc.) (#2515) + Note that CamelCase issue names are conventional for Phan and its plugins. ++ Emit `PhanCompatibleAutoload` when using `function __autoload() {}` instead of `spl_autoload_register() {}` (#2528) ++ Be more aggressive about inferring that the result is `null` when accessing array offsets that don't exist. (#2541) ++ Fix a false positive analyzing `array_map` when the closure has a dependent return type. (#2554) ++ Emit `PhanNoopArrayAccess` when an array field is fetched but not used (#2538) + +Language Server/Daemon mode: ++ Fix an error in the language server on didChangeConfiguration ++ Show hover text of ancestors for class elements (methods, constants, and properties) when no summary is available for the class element. (#1945) + +Maintenance ++ Don't exit if the AST version Phan uses (currently version 50) is deprecated by php-ast (#1134) + +Plugins: ++ Write `PhanSelfCheckPlugin` for self-analysis of Phan and plugins for Phan. (#1576) + This warns if too many/too few arguments are provided for the issue template when emitting an issue. ++ Add `AutomaticFixCapability` for plugins to provide fixes for issues for `--automatic-fix` (#2549) ++ Change issue messages for closures in `UnknownElementTypePlugin` (#2543) + +Bug fixes: ++ Fix bug: `--ignore-undeclared` failed to properly ignore undeclared elements since 1.2.3 (#2502) ++ Fix false positive `PhanTypeInvalidDimOffset` for functions nested within other functions. ++ Support commas in the union types of parameters of magic methods (#2507) ++ Fix parsing `?(A|B|C)` (#2551) + +27 Feb 2019, Phan 1.2.5 +----------------------- + +New features(Analysis): ++ Cache ASTs generated by the polyfill to disk by default, improving performance of the polyfill parser. + (e.g. affects use cases where `php-ast` is not installed and `--use-polyfill-parser` is enabled). + + ASTs generated by the native parser (`php-ast`) are not cached. + + (For the language server/daemon mode, Phan stops reading from/writing to the cache after it finishes initializing) ++ Be more consistent warning about invalid callables passed to internal functions such as `register_shutdown_function` (#2046) ++ Add `@phan-suppress-next-next-line` to suppress issues on the line 2 lines below the comment. This is useful in block comments/doc comments. (#2470) ++ Add `@phan-suppress-previous-line` to suppress issues on the line above the comment. (#2470) ++ Detect `PhanRedefineClassConstant` and `PhanRedefineProperty` when class constants and properties are redefined. (#2492) + +New features(CLI): ++ Add `--disable-cache` to disable the disk cache of ASTs generated by the polyfill. + +Language Server/Daemon mode: ++ Show plaintext summaries of internal classes, functions, methods, constants, and properties when hover text is requested. ++ Show descriptions of superglobals and function parameters when hovering over a variable. + +Maintenance ++ Render the constants in `PhanUndeclaredMagicConstant` as `__METHOD__` instead of `MAGIC_METHOD` + +Plugins: ++ Add `WhitespacePlugin` to check for trailing whitespace, tabs, and carriage returns in PHP files. ++ Add `HandleLazyLoadInternalFunctionCapability` so that plugins can modify Phan's information about internal global functions when those functions are loaded after analysis starts. ++ Add `SuspiciousParamOrderPlugin` which guesses if arguments to functions are out of order based on the names used in the argument expressions. + + E.g. warns about invoking `function example($first, $second, $third)` as `example($mySecond, $myThird, $myFirst)` ++ Warn if too many arguments are passed to `emitIssue`, `emitPluginIssue`, etc. (#2481) + +Bug fixes: ++ Support parsing nullable template types in PHPDoc (e.g. `@return ?T`) ++ Allow casting `null` to `?\MyClass<\Something>`. ++ Fix false positive PhanUndeclaredMagicConstant for `__METHOD__` and `__FUNCTION__` in function/method parameters (#2490) ++ More consistently emit `PhanParamReqAfterOpt` in methods (#1843). + +18 Feb 2019, Phan 1.2.4 +----------------------- + +New features(Analysis): ++ Inherit more specific phpdoc template types even when there are real types in the signature. (#2447) + e.g. inherit `@param MyClass` and `@return MyClass` from the + ancestor class of `function someMethod(MyClass $x) : MyClass {}`. + + This is only done when each phpdoc type is compatible with the real signature type. ++ Warn about `@var Type` without a variable name in doc comments of function-likes (#2445) ++ Infer side effects of `array_push` and `array_unshift` on complex expressions such as properties. (#2365) ++ Warn when a non-string is used as a property name for a dynamic property access (#1402) ++ Don't emit `PhanAccessMethodProtected` for `if ($this instanceof OtherClasslike) { $this->protectedMethod(); }` (#2372) + (This only applies to uses of the variable `$this`, e.g. in closures or when checking interfaces) + +Plugins: ++ Warn about unspecialized array types of elements in UnknownElementTypePlugin. `mixed[]` can be used when absolutely nothing is known about the array's key or value types. ++ Warn about failing to use the return value of `var_export($value, true)` (and `print_r`) in `UseReturnValuePlugin` (#2391) ++ Fix plugin causing `InvalidVariableIssetPlugin` to go into an infinite loop for `isset(self::CONST['offset'])` (#2446) + +Maintenance ++ Limit frames of stack traces in crash reports to 1000 bytes of encoded data. (#2444) ++ Support analysis of the upcoming php 7.4 `??=` operator (#2369) ++ Add a `target_php_version` option for PHP 7.4. + This only affects inferred function signatures, and does not allow parsing newer syntax. + +Bug fixes: ++ Fix a crash seen when parsing return typehint for `Closure` in a different case (e.g. `closure`) (#2438) ++ Fix an issue loading the autoloader multiple times when the `vendor` folder is not lowercase on case-sensitive filesystems (#2440) ++ Fix bug causing template types on methods to not work properly when inherited from a trait method. ++ Catch and warn when declaring a constant that would conflict with built in keywords (true/false/null) and prevent it from affecting inferences. (#1642) + +10 Feb 2019, Phan 1.2.3 +----------------------- + +New features(CLI): ++ Add `-I ` as an alias of `--include-analysis-file-list `. ++ Support repeating the include option (`-I -I `) + and the exclude option (`-3 -3 `). + +New features(Analysis): ++ Inherit more specific phpdoc types even when there are real types in the signature. (#2409) + e.g. inherit `@param array` and `@return MyClass[]` from the + ancestor class of `function someMethod(array $x) : array {}`. + + This is only done when each phpdoc type is compatible with the real signature type. ++ Detect more expressions without side effects: `PhanNoopEmpty` and `PhanNoopIsset` (for `isset(expr)` and `empty(expr)`) (#2389) ++ Also emit `PhanNoopBinaryOperator` for the `??`, `||`, and `&&` operators, + but only when the result is unused and the right-hand side has no obvious side effects. (#2389) ++ Properly analyze effects of a property/field access expression as the key of a `foreach` statement. (#1601) ++ Emit `PhanTypeInstantiateTrait` when calling `new TraitName()` (#2379) ++ Emit `PhanTemplateTypeConstant` when using `@var T` on a class constant's doc comment. (#2402) ++ Warn for invalid operands of a wider variety of binary operators (`/`, `/=`, `>>`, `<<=`, `-`, `%`, `**`, etc) (#2410) + New issue types: `PhanTypeInvalidRightOperandOfIntegerOp` and `PhanTypeInvalidLeftOperandOfIntegerOp`. + Also, mention the operator name in the issue message. + +Language Server/Daemon mode: ++ Attempted fixes for bugs with issue filtering in the language server on Windows. ++ Add `--language-server-disable-output-filter`, which disables the language server filter to limit outputted issues + to those in files currently open in the IDE. + +Maintenance ++ Don't emit a warning to stderr when `--language-server-completion-vscode` is used. ++ Catch the rare RecursionDepthException in more places, improve readability of its exception message. (#2386) ++ Warn that php-ast 1.0.0 and older always crash with PHP 7.4-dev or newer. + +Bug fixes: ++ Fix edge cases in checking if properties/methods are accessible from a trait (#2371) ++ Fix edge cases checking for `PhanTypeInstantiateInterface` and `PhanTypeInstantiateAbstract` (#2379) + +Plugins: ++ Infer a literal string return value when calling `sprintf` on known literal scalar types in `PrintfCheckerPlugin`. (#2131) ++ Infer that `;@foo();` is not a usage of `foo()` in `UseReturnValuePlugin`. (#2412) ++ Implement `NotFullyQualifiedUsagePlugin` to warn about uses of global functions and constants that aren't fully qualified. (#857) + +02 Feb 2019, Phan 1.2.2 +----------------------- + +New features(CLI): ++ Emit a warning to stderr if no files were parsed when Phan is invoked. (#2289) + +New features(Analysis): ++ Add `@phan-extends` and `@extends` as an alias of `@inherits` (#2351) ++ Make checks such as `$x !== 'a literal'` (and `!=`) remove the literal string/int type from the union type. (#1789) + +Language Server/Daemon mode: ++ Limit analysis results of the language server to only the currently open files. (#1722) ++ Limit analysis results of Phan daemon to just the requested files in **all** output formats (#2374) + (not just when `phan_client` post-processes the output) ++ Make code completion immediately after typing `->` and `::` behave more consistently (#2343) + Note: this fix only applies at the very last character of a line ++ Be more consistent about including types in hover text for properties (#2348) ++ Make "Go to Definition" on `new MyClass` go to `MyClass::__construct` if it exists. (#2276) ++ Support "Go to Definition" for references to global functions and global constants in comments and literal strings. + Previously, Phan would only look for class definitions in comments and literal strings. ++ Fix a crash requesting completion results for some class names/global constants. + +Maintenance: ++ Warn and exit immediately if any plugins are missing or invalid (instead of crashing after parsing all files) (#2099) ++ Emit warnings to stderr if any config settings seem to be the wrong type (#2376) ++ Standardize on logging to stderr. ++ Add more details about the call that crashed to the crash report. + +Bug fixes: ++ Emit a warning and exit if `--config-file ` does not exist (#2271) ++ Fix inferences about `foreach ($arr as [[$nested]]) {...}` (#2362) ++ Properly analyze accesses of `@internal` elements of the root namespace from other parts of the root namespace. (#2366) ++ Consistently emit `UseNormalNoEffect` (etc.) when using names/functions/constants of the global scrope from the global scope. ++ Fix a bug causing incorrect warnings due to uses of global/class constants. + +Plugins: ++ Add `UseReturnValuePlugin`, which will warn about code that calls a function/method such as `sprintf` or `array_merge` without using the return value. + + The list it uses is not comprehensive; it is a small subset of commonly used functions. + + This plugin can also be configured to automatically warn about failing to use a return value of **any** user-defined or internal function-like, + when over 98% of the other calls in the codebase did use the return value. + +18 Jan 2019, Phan 1.2.1 +----------------------- + +New features(CLI): ++ Add short flags: `-S` for `--strict-type-checking`, `-C` for `--color`, `-P` for `--plugin ` + +New features(Analysis): ++ Infer that the result of `array_map` has integer keys when passed two or more arrays (#2277) ++ Improve inferences about the left-hand side of `&&` statements such as `$leftVar && (other_expression);` (#2300) ++ Warn about passing an undefined variable to a function expecting a reference parameter with a real, non-nullable type (#1344) ++ Include variables in scope as alternative suggestions for undeclared properties (#1680) ++ Infer a string literal when analyzing calls to `basename` or `dirname` on an expression that evaluates to a string literal. (#2323) ++ Be stricter about warning when literal int/string values are passed to incompatible scalar types when `scalar_implicit_cast` or `scalar_implicit_partial` are used. (#2340) + +Maintenance: ++ End the output for `--output-mode ` with a newline. ++ Upgrade tolerant-php-parser, making the polyfill/fallback properly parse `$a && $b = $c` (#2180) ++ Add updates to the function/method signature map from Psalm and PHPStan. + +Language Server/Daemon mode: ++ Add `--output-mode ` to `phan_client`. (#1568) + + Supported formats: `phan_client` (default), `text`, `json`, `csv`, `codeclimate`, `checkstyle`, or `pylint` ++ Add `--color` to `phan_client` (e.g. for use with `--output-mode text`) ++ Add `--language-server-completion-vscode`. This is a workaround to make completion of variables and static properties work in [the Phan plugin for VS Code](https://github.com/tysonandre/vscode-php-phan) ++ Include Phan's signature types in hover text for internal and user-defined methods (instead of just the real types) (#2309) + Also, show defaults of non-nullable parameters as `= default` instead of `= null` ++ Properly return a result set when requesting variable completion of `$` followed by nothing. ++ Fix code completion when `--language-server-analyze-only-on-save` is on. (#2327) + +Plugins: ++ Add a new issue type to `DuplicateExpressionPlugin`: `PhanPluginBothLiteralsBinaryOp`. (#2297) + + (warns about suspicious expressions such as `null == 'a literal` in `$x ?? null == 'a literal'`) ++ Support `assertInternalType` in `PHPUnitAssertionPlugin` (#2290) ++ Warn when identical dynamic expressions (e.g. variables, function calls) are used as array keys in `DuplicateArrayKeyPlugin` ++ Allow plugins to include a `Suggestion` when calling `$this->emitIssue()` + +05 Jan 2019, Phan 1.2.0 +----------------------- + +New features(Analysis): ++ Infer match keys of `$matches` for a wider range of regexes (e.g. non-capturing groups, named subgroups) (#2294) ++ Improve detection of invalid arguments in code implicitly calling `__invoke`. ++ Support extracting template types from more forms of `callable` types. (#2264) ++ Support `@phan-assert`, `@phan-assert-true-condition`, and `@phan-assert-false-condition`. + Examples of side effects when this annotation is used on a function/method declaration: + + - `@phan-assert int $x` will assert that the argument to the parameter `$x` is of type `int`. + - `@phan-assert !false $x` will assert that the argument to the parameter `$x` is not false. + - `@phan-assert !\Traversable $x` will assert that the argument to the parameter `$x` is not `Traversable` (or a subclass) + - `@phan-assert-true-condition $x` will make Phan infer that the argument to parameter `$x` is truthy if the function returned successfully. + - `@phan-assert-false-condition $x` will make Phan infer that the argument to parameter `$x` is falsey if the function returned successfully. + - This can be used in combination with Phan's template support. + + See [tests/plugin_test/src/072_custom_assertions.php](tests/plugin_test/src/072_custom_assertions.php) for example uses of these annotations. ++ Suggest typo fixes when emitting `PhanUnusedVariable`, if only one definition was seen. (#2281) ++ Infer that `new $x` is of the template type `T` if `$x` is `class-string` (#2257) + +Plugins: +- Add `PHPUnitAssertionPlugin`. + This plugin will make Phan infer side effects of some of the helper methods PHPUnit provides within test cases. + + - Infer that a condition is truthy from `assertTrue()` and `assertNotFalse()` (e.g. `assertTrue($x instanceof MyClass)`) + - Infer that a condition is null/not null from `assertNull()` and `assertNotNull()` + - Infer class type of `$actual` from `assertInstanceOf(MyClass::class, $actual)` + - Infer that `$actual` has the exact type of `$expected` after calling `assertSame($expected, $actual)` + - Other methods aren't supported yet. + +Bug fixes: +- Infer that some internal classes' properties (such as `\Exception->message`) are protected (#2283) +- Fix a crash running Phan without php-ast when no files were parsed (#2287) + +30 Dec 2018, Phan 1.1.10 +------------------------ + +New features(Analysis): ++ Add suggestions if to `PhanUndeclaredConstant` issue messages about undeclared global constants, if possible. (#2240) + Suggestions include other global constants, variables, class constants, properties, and function names. ++ Warn about `continue` and `break` with no matching loop/switch scope. (#1869) + New issue types: `PhanContinueOrBreakTooManyLevels`, `PhanContinueOrBreakNotInLoop` ++ Warn about `continue` statements targeting `switch` control structures (doing the same thing as a `break`) (#1869) + New issue types: `PhanContinueTargetingSwitch` ++ Support inferring template types from array keys. + int/string/mixed can be inferred from `array` when `@template TKey` is in the class/function-like scope. ++ Phan can now infer template types from even more categories of parameter types in constructors and regular functions/methods. (#522) + + - infer `T` from `Closure(T):\OtherClass` and `callable(T):\OtherClass` + - infer `T` from `array{keyName:T}` + - infer `TKey` from `array` (as int, string, or mixed) + +Bug fixes: ++ Refactor the way `@template` annotations are parsed on classes and function-likes to avoid various edge cases (#2253) ++ Fix a bug causing Phan to fail to analyze closures/uses of closures when used inline (e.g. in function calls) + +27 Dec 2018, Phan 1.1.9 +----------------------- + +New features(Analysis): ++ Warn about `split` and other functions that were removed in PHP 7.0 by default. (#2235, #2236) + (`target_php_version` can now be set to `'5.6'` if you have a PHP 5.6 project that uses those) ++ Fix a false positive `PhanUnreferencedConstant` seen when calling `define()` with a dynamic name. (#2245) ++ Support analyzing `@template` in PHPDoc of closures, functions and methods. (#522) + + Phan currently requires the template type to be part of the parameter type(s) as well as the return type. + + New issue types: `PhanTemplateTypeNotUsedInFunctionReturn`, `PhanTemplateTypeNotDeclaredInFunctionParams` ++ Make `@template` on classes behave more consistently. (#522) + + Phan will now check the union types of parameters instead of assuming that arguments will always occur in the same order and positions as `@template`. ++ Phan can now infer template types from more categories of parameter types in constructors and regular functions/methods. (#522) + - `@param T[]` + - `@param Closure():T` + - `@param OtherClass<\stdClass,T>` + + - Note that this implementation is currently incomplete - Phan is not yet able to extract `T` from types not mentioned here (e.g. `array{0:T}`, `Generator`, etc.) ++ Add `callable-string` and `class-string` types. (#1346) + Warn if an invalid/undefined callable/class name is passed to parameters declared with those exact types. ++ Infer a more accurate literal string for the `::class` constant. + + Additionally, support inferring that a function/method will return instances of the passed in class name, when code has PHPDoc such as the following: + + ``` + /** + * @template T + * @param class-string $className + * @return T + */ + ``` + +Plugins: ++ Detect more possible duplicates in `DuplicateArrayKeyPlugin` + +Language Server/Daemon mode: ++ Be more consistent about how return types in methods (of files that aren't open) are inferred. + +Bug fixes: ++ Fix a bug parsing the CLI option `--target-php-version major.minor` (Phan will now correctly set the `target_php_version` config setting) ++ Fix type inferences of `$x['offset'] = expr` in a branch, when outside of that branch. (#2241) + +15 Dec 2018, Phan 1.1.8 +----------------------- + +New features(Analysis): ++ Infer more accurate types for return values/expected arguments of methods of template classes. ++ Support template types in magic methods and properties. (#776, related to #497) ++ Emit `PhanUndeclaredMagicConstant` when using a magic constant in a scope that doesn't make sense. + Infer more accurate literal strings for some magic constants. + +Bug fixes: ++ Fix a crash when an empty scalar value was passed to a function with variadic arguments (#2232) + +08 Dec 2018, Phan 1.1.7 +----------------------- + +Maintenance: ++ Improve checks for empty/invalid FQSENs. + Also, replace `PhanTypeExpectedObjectOrClassNameInvalidName` with `PhanEmptyFQSENInClasslike` or `PhanInvalidFQSENInClasslike`. + +Bug fixes: ++ Fix uncaught crash on startup analyzing `class OCI-Lob` from oci8 (#2222) + +08 Dec 2018, Phan 1.1.6 +----------------------- + +New features(Analysis): ++ Add suggestions to `PhanUndeclaredFunction` for functions in other namespaces + and similarly named functions in the same namespace. ++ Add issue types `PhanInvalidFQSENInCallable` and `PhanInvalidFQSENInClasslike` ++ Properly analyze closures generated by `Closure::fromCallable()` on a method. ++ Emit `PhanDeprecatedCaseInsensitiveDefine` when define is used to create case-insensitive constants (#2213) + +Maintenance: ++ Increase the default of the config setting `suggestion_check_limit` from 50 to 1000. ++ Shorten help messages for `phan --init` (#2162) + +Plugins: ++ Add a prototype tool `tool/dump_markdown_preview`, + which can be used to preview what description text Phan parses from a doc comment + (similar to the language server's hover text) + +Bug fixes: ++ Infer more accurate types after asserting `!empty($x)` ++ Fix a crash seen when analyzing anonymous class with 0 args + when checking if PhanInfiniteRecursion should be emitted (#2206) ++ Fix a bug causing Phan to fail to warn about nullable phpdoc types + replacing non-nullable param/return types in the real signature. ++ Infer the correct type for the result of the unary `+` operator. + Improve inferences when `+`/`-` operators are used on string literals. ++ Fix name inferred for global constants `define()`d within a namespace (#2207). + This now properly treats the constant name as being fully qualified. ++ Don't emit PhanParamSignatureRealMismatchReturnType for a return type of `T` replacing `?T`, + or for `array` replacing `iterable` (#2211) + +29 Nov 2018, Phan 1.1.5 +----------------------- + +Language Server: ++ Fix a crash in the Language Server when pcntl is not installed or enabled (e.g. on Windows) (#2186) + +27 Nov 2018, Phan 1.1.4 +----------------------- + +New features(Analysis): ++ Preserve original descendent object types after type assertions, when original object types are all subtypes + (e.g. infer `SubClass` for `$x = rand(0,1) ? new SubClass() : false; if ($x instanceof BaseClass) { ... }`) + +Maintenance: ++ Emit `UnusedPluginSuppression` on `@phan-suppress-next-line` and `@phan-file-suppress` + on the same line as the comment declaring the suppression. (#2167, #1731) ++ Don't emit `PhanInvalidCommentForDeclarationType` (or attempt to parse) unknown tags that have known tags as prefixes (#2156) + (e.g. `@param-some-unknown-tag`) + +Bug fixes: ++ Fix a crash when analyzing a nullable parameter of type `self` in traits (#2163) ++ Properly parse closures/generic arrays/array shapes when inner types also contain commas (#2141) ++ Support matching parentheses inside closure params, recursively. (e.g. `Closure(int[],Closure(int):bool):int[]`) ++ Don't warn about properties being read-only when they might be modified by reference (#1729) + +20 Nov 2018, Phan 1.1.3 +----------------------- + +New features(CLI): ++ Warn when calling method on union types that are definitely partially invalid. (#1885) + New config setting: `--strict-method-checking` (enabled as part of `--strict-type-checking`) + New issue type: `PhanPossiblyNonClassMethodCall` ++ Add a prototype tool `tool/phoogle`, which can be used to search for function/method signatures in user-declared and internal functions/methods. + E.g. to look for functions that return a string, given a string and an array: + `/path/phan/tool/phoogle 'string -> array -> string` + +New features(Analysis): ++ Add a heuristic check to detect potential infinite recursion in a functionlike calling itself (i.e. stack overflows) + New issue types: `PhanInfiniteRecursion` ++ Infer literal integer values from expressions such as `2 | 1`, `2 + 2`, etc. ++ Infer more accurate array shapes for `preg_match_all` (based on existing inferences for `preg_match`) ++ Make Phan infer union types of variables from switch statements on variables (#1291) + (including literal int and string types) ++ Analyze simple assertions on `get_class($var)` of various forms (#1977) + Examples: + - `assert(get_class($x) === 'someClass')` + - `if (get_class($x) === someClass::class)` + - `switch (get_class($x)) {case someClass::class: ...}` ++ Warn about invalid/possibly invalid callables in function calls. + New issue types: `PhanTypeInvalidCallable`, `PhanTypePossiblyInvalidCallable` (the latter check requires `--strict-method-checking`) ++ Reduce false positives for a few functions (such as `substr`) in strict mode. ++ Make Phan infer that variables are not null/false from various comparison expressions, e.g. `assert($x > 0);` ++ Detect invalid arguments to `++`/`--` operators (#680). + Improve the analysis of the side effects of `++`/`--` operators. + New issue type: `PhanTypeInvalidUnaryOperandIncOrDec` + +Plugins: ++ Add `BeforeAnalyzeCapability`, which will be executed once before starting the analysis phase. (#2086) + +Bug fixes: ++ Fix false positives analyzing `define()` (#2128) ++ Support declaring instance properties as the union type `static` (#2145) + New issue types: `PhanStaticPropIsStaticType` ++ Fix a crash seen when Phan attempted to emit `PhanTypeArrayOperator` for certain operations (#2153) + +05 Nov 2018, Phan 1.1.2 +----------------------- + +New features(CLI): ++ Make `phan --progress-bar` fit within narrower console widths. (#2096) + (Make the old width into the new **maximum** width) + Additionally, use a gradient of shades for the progress bar. + +New features(Analysis): ++ Warn when attempting to read from a write-only real/magic property (or vice-versa) (#595) + + New issue types: `PhanAccessReadOnlyProperty`, `PhanAccessReadOnlyMagicProperty`, `PhanAccessWriteOnlyProperty`, `PhanAccessWriteOnlyMagicProperty` + + New annotations: `@phan-read-only` and `@phan-write-only` (on its own line) in the doc comment of a real property. ++ Warn about use statements that are redundant. (#2048) + + New issue types: `PhanUseConstantNoEffect`, `PhanUseFunctionNoEffect`, `PhanUseNormalNamespacedNoEffect`, `PhanUseNormalNoEffect` + + By default, this will only warn about use statements made from the global namespace, of elements also in the global namespace. + To also warn about redundant **namespaced** uses of classes/namespaces (e.g. `namespace Foo; use Foo\MyClass;`), enable `warn_about_redundant_use_namespaced_class` ++ Warn when using a trait as a real param/return type of a method-like (#2007) + New issue types: `PhanTypeInvalidTraitParam`, `PhanTypeInvalidTraitReturn` ++ Improve the polyfill/fallback parser's heredoc and nowdoc lexing (#1537) ++ Properly warn about an undefined variable being passed to `array_shift` (it expects an array but undefined is converted to null) (related to fix for #2100) ++ Stop adding generic int/string to the type of a class property when the doc comment mentions only literal int/string values (#2102) + (e.g. `@var 1|2`) ++ Improve line number of warning about extra comma in arrays (i.e. empty array elements). (#2066) ++ Properly parse [flexible heredoc/nowdoc syntaxes](https://wiki.php.net/rfc/flexible_heredoc_nowdoc_syntaxes) that were added in PHP 7.3 (#1537) ++ Warn about more invalid operands of the binary operators `^`,`/`,`&` (#1825) + Also, fix cases where `PhanTypeArrayOperator` would not be emitted. + New issue types: `PhanTypeInvalidBitwiseBinaryOperator`, `PhanTypeMismatchBitwiseBinaryOperands` ++ Indicate when warnings about too many arguments are caused only by argument unpacking. (#1324) + New issue types: `PhanParamTooManyUnpack`, `PhanParamTooManyUnpackInternal` ++ Properly warn about undefined namespaced constants/functions from within a namespace (#2112) + Phan was failing to warn in some cases. ++ Always infer `int` for `<<` and `>>` ++ Support using dynamic values as the name for a `define()` statement (#2116) + +Maintenance: ++ Make issue messages more consistent in their syntax used to describe closures/functions (#1695) ++ Consistently refer to instance properties as `Class->propname` and static properties as `Class::$staticpropname` in issue messages. + +Bug fixes: ++ Properly type check `static::someMethodName()`. + Previously, Phan would fail to infer types for the results of those method calls. ++ Improve handling of `array_shift`. Don't warn when it's used on a global or superglobal (#2100) ++ Infer that `self` and `static` in a trait refer to the methods of that trait. (#2006) + +22 Oct 2018, Phan 1.1.1 +----------------------- + +New features(Analysis): ++ Add `defined at {FILE}:{LINE}` to warnings about property visibility. ++ Warn about missing references (`\n` or `$n`) in the replacement template string of `preg_replace()` (#2047) ++ Make `@suppress` on closures/functions/methods apply more consistently to issues emitted when analyzing the closure/function/method declaration. (#2071) ++ Make `@suppress` on warnings about unparseable doc comments work as expected (e.g. for `PhanInvalidCommentForDeclarationType on a class`) (#1429) ++ Support checking for missing/invalid files in `require`/`include`/`require_once`/`include_once` statements. + + To enable these checks, set `enable_include_path_checks` to `true` in your Phan config. + + New issue types: `PhanRelativePathUsed`, `PhanTypeInvalidEval`, `PhanTypeInvalidRequire`, `PhanInvalidRequireFile`, `PhanMissingRequireFile` + + New config settings: `enable_include_path_checks`, `include_paths`, `warn_about_relative_include_statement` ++ Warn when attempting to unset a property that was declared (i.e. not a dynamic or magic property) (#569) + New issue type: `PhanTypeObjectUnsetDeclaredProperty` + + - This warning is emitted because declared properties are commonly expected to exist when they are accessed. ++ Warn about iterating over an object that's not a `Traversable` and not `stdClass` (#1115) + New issue types (for those objects) were added for the following cases: + + 1. Has no declared properties (`TypeNoPropertiesForeach`) + 2. Has properties and none are accessible. (`TypeNoAccessiblePropertiesForeach`) + 3. Has properties and some are accessible. (`TypeSuspiciousNonTraversableForeach`) ++ Add `@phan-template` and `@phan-inherits` as aliases for `@template` and `@inherits` (#2063) ++ Warn about passing non-objects to `clone()` (`PhanTypeInvalidCloneNotObject`) (#1798) + +Maintenance: ++ Minor performance improvements. ++ Increase the default value of `max_literal_string_type_length` from 50 to 200. ++ Include Phan version in Phan's error handler and exception handler output. (#1639) + +Bug fixes: ++ Don't crash when parsing an invalid cast expression. Only the fallback/polyfill parsers were affected. + +Language Server/Daemon mode: ++ Fix bugs in the language server. + + 1. The language server was previously using the non-PCNTL fallback + implementation unconditionally due to an incorrect default configuration value. + After this fix, the language server properly uses PCNTL by default + if PCNTL is available. + + This bug was introduced by PR #1743 + + 2. Fix a bug causing the language server to eventually run out of memory when PCNTL was disabled. + +08 Oct 2018, Phan 1.1.0 +----------------------- + +Maintenance: ++ Work on making this compatible with `php-ast` 1.0.0dev. (#2038) + (Phan continues to support php-ast 0.1.5 and newer) + + Remove dead code (such as helper functions and references to constants) that aren't needed when using AST version 50 (which Phan uses). + + Some plugins may be affected if they call these helper methods or use those constants when the shim is used. + +Bug fixes: ++ Fix a crash parsing an empty `shell_exec` shorthand string when using the fallback parser + (i.e. two backticks in a row) ++ Fix a false positive `PhanUnusedVariable` warning about a variable declared prior to a do/while loop (#2026) + +02 Oct 2018, Phan 1.0.7 +----------------------- + +New features(Analysis): ++ Support the `(int|string)[]` syntax of union types (union of multiple types converted to an array) in PHPDoc (#2008) + + e.g. `@param (int|string)[] $paramName`, `@return (int|string)[]` ++ Support spaces after commas in array shapes (#1966) ++ Emit warnings when using non-strings as dynamic method names (e.g. `$o->{$notAString}()`) + New issue types: `PhanTypeInvalidMethodName`, `PhanTypeInvalidStaticMethodName`, `PhanTypeInvalidCallableMethodName` + +Plugins: ++ In HasPHPDocPlugin, use a more compact representation to show what Phan sees from the raw doc comment. ++ In HasPHPDocPlugin, warn about global functions without extractable PHPDoc summaries. + + New issue types: `PhanPluginNoCommentOnFunction`, `PhanPluginDescriptionlessCommentOnFunction` ++ In HasPHPDocPlugin, warn about methods without extractable PHPDoc summaries. + + New issue types: `PhanPluginNoCommentOn*Method`, `PhanPluginDescriptionlessCommentOn*Method` + + These can be suppressed based on the method FQSEN with `plugin_config => [..., 'has_phpdoc_method_ignore_regex' => (a PCRE regex)]` + (e.g. to suppress issues about tests, or about missing documentation about getters and setters, etc.) + +Bug fixes: ++ Fix false positive `PhanUnusedVariable` for variables declared before break/continue that are used after the loop. (#1985) ++ Properly emit `PhanUnusedVariable` for variables where definitions are shadowed by definitions in branches and/or loops. (#2012) ++ Properly emit `PhanUnusedVariable` for variables which are redefined in a 'do while' loop. ++ Be more consistent about emitting `PhanUnusedVariableCaughtException` when exception variable names are reused later on. ++ Fix a crash when parsing `@method` annotations with many parameters (#2019) + +25 Sep 2018, Phan 1.0.6 +----------------------- + +New features(Analysis): ++ Be more consistent about warning about undeclared properties in some edge cases. + New issue types: `PhanUndeclaredClassProperty`, `PhanUndeclaredClassStaticProperty` + +Maintenance: ++ Restore test files in future published releases' **git tags** (#1986) + (But exclude them from the zip/tar archives published on GitHub Releases) + + - When `--prefer-dist` (the default) is used in composer to download a stable release, + the test files will not be part of the downloaded files. + +Language Server/Daemon mode: ++ Add support for code completion suggestions. (#1706) + + This can be enabled by passing `--language-server-enable-completion` + + This will complete references to the following element types: + + - variable names (using superglobals and local variables that have been declared in the scope) + - global constants, global functions, and class names. + - class constants, instance and static properties, and instance and static method names. + + NOTE: If you are completing from the empty string (e.g. immediately after `->` or `::`), + Phan may interpret the next word token (e.g. on the next line) as the property/constant name/etc. to complete, + due to the nature of the parser used (The cursor position doesn't affect the parsing logic). + + - Completion requests before tokens that can't be treated that way will not cause that problem. + (such as `}`, `;`, `)`, the end of the file, etc.) + +Bug fixes: ++ Fix various uncaught errors in Phan that occurred when parsing invalid ASTs. + Instead of crashing, warn about the bug or invalid AST. + + New issue types: `PhanInvalidConstantFQSEN`, `PhanContextNotObjectUsingSelf`, `PhanInvalidTraitUse` (for unparsable trait uses) + +21 Sep 2018, Phan 1.0.5 +----------------------- + +New Features(Analysis) ++ Warn if a PHPDoc annotation for an element(`@param`, `@method`, or `@property*`) is repeated. (#1963) + + New issue types: `PhanCommentDuplicateMagicMethod`, `PhanCommentDuplicateMagicProperty`, `PhanCommentDuplicateParam` ++ Add basic support for `extract()` (#1978) ++ Improve line numbers for warnings about `@param` and `@return` annotations (#1369) + +Maintenance: ++ Make `ext-ast` a suggested composer dependency instead of a required composer dependency (#1981) + + `--use-fallback-parser` allows Phan to analyze files even when php-ast is not installed or enabled. ++ Remove test files from future published releases (#1982) + +Plugins: ++ Properly warn about code after `break` and `continue` in `UnreachableCodePlugin`. + Previously, Phan only warned about code after `throw` and `return`. + +Bug fixes: ++ Don't infer bad types for variables when analyzing `array_push` using expressions containing those variables. (#1955) + + (also fixes other `array_*` functions taking references) ++ Fix false negatives in PHP5 backwards compatibility heuristic checks (#1939) ++ Fix false positive `PhanUnanalyzableInheritance` for a method inherited from a trait (which itself uses trait) (#1968) ++ Fix an uncaught `RuntimeException` when type checking an array that was roughly 12 or more levels deep (#1962) ++ Improve checks of the return type of magic methods against methods inherited from ancestor classes (#1975) + + Don't emit a false positive `PhanParamSignaturePHPDocMismatchReturnType` + +Language Server/Daemon mode: ++ Fix an uncaught exception when extracting a URL with an unexpected scheme (not `file:/...`) (#1960) ++ Fix false positive `PhanUnreferencedUseNormal` issues seen when the daemon was running without pcntl (#1860) + +10 Sep 2018, Phan 1.0.4 +----------------------- + +Plugins: ++ Fix a crash in `DuplicateExpressionPlugin`. ++ Add `HasPHPDocPlugin`, which checks if an element (class or property) has a PHPDoc comment, + and that Phan can extract a plaintext summary/description from that comment. + +Language Server/Daemon mode: ++ Support generating a hover description for variables. + + - For union types with a single non-nullable class/interface type, the hover text include the full summary description of that class-like. + - For non-empty union types, this will just show the raw union type (e.g. `string|false`) ++ Improve extraction of summaries of elements (e.g. hover description) + + - Support using `@return` as a summary for function-likes. + - Parse the lines after `@var` tag (before subsequent tags) + as an additional part of the summary for constants/properties. + +Maintenance: ++ Update Phar file to contain missing LICENSEs (#1950) ++ Add more documentation to Phan's code. + +07 Sep 2018, Phan 1.0.3 +----------------------- + +Bug fixes ++ Fix bugs in analysis of assignments within conditionals (#1937) ++ Fix a crash analyzing comparison with variable assignment expression (#1940) + (e.g. `if (1 + 1 > ($var = 1))`) + +Plugins: ++ Update `SleepCheckerPlugin` to warn about properties that aren't returned in `__sleep()` + that don't have a doc comment annotation of `@phan-transient` or `@transient`. + (This is not an officially specified annotation) + + New issue type: `SleepCheckerPropertyMissingTransient` + + New setting: `$config['plugin_config']['sleep_transient_warning_blacklist_regex']` + can be used to prevent Phan from warning about certain properties missing `@phan-transient` + +06 Sep 2018, Phan 1.0.2 +----------------------- + +New features(Analysis) ++ Allow spaces on either side of `|` in union types + (e.g. `@param array | ArrayAccess $x`) ++ Warn about array destructuring assignments from non-arrays (#1818) + (E.g. `[$x] = 2`) + + New issue type: `PhanTypeInvalidExpressionArrayDestructuring` ++ Infer the number of groups for $matches in `preg_match()` + + Named subpatterns, non-capturing patterns, and regular expression options are not supported yet. + Phan will just infer a more generic type such as `string[]` (depending on the bit flags). ++ Warn about ambiguous uses of `Closure():void` in phpdoc. + Also, make that syntax count as a reference to `use Closure;` in that namespace. ++ Track the line number of magic method and magic properties (Instead of reporting the line number of the class). + +Bug fixes ++ Fix a crash seen when using a temporary expression in a write context. (#1915) + + New issue type: `PhanInvalidWriteToTemporaryExpression` ++ Fix a crash seen with --use-fallback-parser with an invalid expression after `new` ++ Properly infer that closures have a class name of `Closure` for some issue types. + (e.g. `call_user_func([function() {}, 'invalidMethod'])`) ++ Fix a bug analyzing nested assignment in conditionals (#1919) ++ Don't include impossible types when analyzing assertions such as `is_string($var)` (#1932) + +26 Aug 2018, Phan 1.0.1 +----------------------- + +New features(CLI,Configs) ++ Support setting a `target_php_version` of PHP 7.3 in the config file or through `--target-php-version`. ++ Assume that `__construct`, `__destruct`, `__set`, `__get`, `__unset`, `__clone`, and `__wakeup` have return types of void if unspecified. + +New features(Analysis) ++ Add function signatures for functions added/modified in PHP 7.3. (#1537) ++ Improve the line number for warnings about unextractable `@property*` annotations. ++ Make Phan aware that `$x` is not false inside of loops such as `while ($x = dynamic_value()) {...}` (#1646) ++ Improve inferred types of `$x` in complex equality/inequality checks such as `if (($x = dynamic_value()) !== false) {...}` ++ Make `!is_numeric` assertions remove `int` and `float` from the union type of an expression. (#1895) ++ Preserve any matching original types in scalar type assertions (#1896) + (e.g. a variable `$x` of type `?int|?MyClass` will have type `int` after `assert(is_numeric($x))`) + +Maintenance: ++ Add/modify various function, methods, and property type signatures. + +Plugins: ++ Add `UnknownElementTypePlugin` to warn about functions/methods + that have param/return types that Phan can't infer anything about. + (it can still infer some things in non-quick mode about parameters) + + New issue types: `PhanPluginUnknownMethodReturnType`, `PhanPluginUnknownClosureReturnType`, `PhanPluginUnknownFunctionReturnType`, `PhanPluginUnknownPropertyType` ++ Add `DuplicateExpressionPlugin` to warn about duplicated expressions such as: + - `X == X`, `X || X`, and many other binary operators (for operators where it is likely to be a bug) + - `X ? X : Y` (can often simplify to `X ?: Y`) + - `isset(X) ? X : Y` (can simplify to `??` in PHP 7) + + New issue types: `PhanPluginDuplicateExpressionBinaryOp`, `PhanPluginDuplicateConditionalTernaryOperation`, `PhanPluginDuplicateConditionalNullCoalescing` ++ Improve types inferred for `$matches` for PregRegexCheckerPlugin. + +Bug fixes: ++ Properly handle `CompileError` (that are not the subclass `ParseError`). CompileError was added in PHP 7.3. + (Phan now logs these the same way it would log other syntax errors, instead of treating this like an unexpected Error.) ++ Make sure that private methods that are generators, that are inherited from a trait, aren't treated like a `void`. ++ Fix a crash analyzing a dynamic call to a static method, which occurred when dead code detection or reference tracking was enabled. (#1889) ++ Don't accidentally emit false positive issues about operands of binary operators in certain contexts. (#1898) + +12 Aug 2018, Phan 1.0.0 +----------------------- + +The Phan 1.0.0 release supports analysis of php 7.0-7.2, and can be executed with php 7.0+. +This release replaces the previous 0.12.x releases. +Because Phan uses PHP's Reflection APIs, it's recommended to use the same PHP minor version for analyzing the code as would be used to run the code. +(For the small number of function/method signatures, etc., that were added or changed in each minor release of PHP.) + +Plugins: ++ Plugins: Remove V1 plugins (and V1 plugin examples), as well as legacy plugin capabilities. (#249) + Third party plugin authors must use V2 of the plugin system. + + Removed capabilities: + + - `AnalyzeNodeCapability`, `LegacyAnalyzeNodeCapability`, `LegacyPostAnalyzeNodeCapability` (use `PostAnalyzeNodeCapability` instead) + - `LegacyPreAnalyzeNodeCapability` (use `PreAnalyzeNodeCapability` instead) ++ API: Remove various methods that were deprecated. (#249) + Any plugins using those methods will need to be updated. + (e.g. `Config::getValue('config_value')` must be used instead of `Config::get()->config_value`) ++ Config: Remove `progress_bar_sample_rate` (#249) + (`progress_bar_sample_interval` must be used instead if you want the progress bar to be faster or slower) ++ Maintenance: Immediately report the exception and exit if any plugins threw an uncaught `Throwable` during initialization. + (E.g. this results in a better error message when a third party plugin requires PHP 7.1 syntax but PHP 7.0 is used to run Phan) + +21 Jul 2018, Phan 0.12.15 +------------------------- + +New features(Analysis) ++ Make Phan's unused variable detection also treat exception variables as variable definitions, + and warn if the caught exception is unused. (#1810) + New issue types: `PhanUnusedVariableCaughtException` ++ Be more aggressive about inferring that a method has a void return type, when it is safe to do so ++ Emit `PhanInvalidConstantExpression` in some places where PHP would emit `"Constant expression contains invalid operations"` + + Phan will replace the default parameter type (or constant type) with `mixed` for constants and class constants. + + Previously, this could cause Phan to crash, especially with `--use-fallback-parser` on invalid ASTs. ++ Improve analysis of arguments passed to `implode()` + +New features(CLI) ++ Add `--daemonize-tcp-host` CLI option for specifying the hostname for daemon mode (#1868). + The default will remain `127.0.0.1` when not specified. + It can be overridden to values such as `0.0.0.0` (publicly accessible, e.g. for usage with Docker) + +Language Server/Daemon mode: ++ Implement support for hover requests in the Language Server (#1738) + + This will show a preview of the element definition (showing signature types instead of PHPDoc types) + along with the snippet of the element description from the doc comment. + + Clients that use this should pass in the CLI option `--language-server-enable-hover` when starting the language server. + + - Note that this implementation assumes that clients sanitize the mix of markdown and HTML before rendering it. + - Note that this may slow down some language server clients if they pause while waiting for the hover request to finish. + +Maintenance: ++ Add a workaround for around a notice in PHP 7.3alpha4 (that Phan treats as fatal) (#1870) + +Bug fixes: ++ Fix a bug in checking if nullable versions of specialized type were compatible with other nullable types. (#1839, #1852) + Phan now correctly allows the following type casts: + + - `?1` can cast to `?int` + - `?'a string'` can cast to `?string` + - `?Closure(T1):T2` can cast to `?Closure` + - `?callable(T1):T2` can cast to `?callable`, ++ Make `exclude_file_list` work more consistently on Windows. + +08 Jul 2018, Phan 0.12.14 +------------------------- + +New features(CLI, Configs) ++ Add `warn_about_undocumented_throw_statements` and `exception_classes_with_optional_throws_phpdoc` config. (#90) + + If `warn_about_undocumented_throw_statements` is true, Phan will warn about uncaught throw statements that aren't documented in the function's PHPDoc. + (excluding classes listed in `exception_classes_with_optional_throws_phpdoc` and their subclasses) + This does not yet check function and method calls within the checked function that may themselves throw. + + Add `warn_about_undocumented_exceptions_thrown_by_invoked_functions`. + If enabled (and `warn_about_undocumented_throw_statements` is enabled), + Phan will warn about function/closure/method invocations that have `@throws` + that aren't caught or documented in the invoking method. + New issue types: `PhanThrowTypeAbsent`, `PhanThrowTypeAbsentForCall`, + `PhanThrowTypeMismatch`, `PhanThrowTypeMismatchForCall` + + Add `exception_classes_with_optional_throws_phpdoc` config. + Phan will not warn about lack of documentation of `@throws` for any of the configured classes or their subclasses. + The default is the empty array (Don't suppress any warnings.) + (E.g. Phan suppresses `['RuntimeException', 'AssertionError', 'TypeError']` for self-analysis) + +New Features (Analysis): ++ Warn when string literals refer to invalid class names (E.g. `$myClass::SOME_CONSTANT`). (#1794) + New issue types: `PhanTypeExpectedObjectOrClassNameInvalidName` (emitted if the name can't be used as a class) + This will also emit `PhanUndeclaredClass` if the class name could not be found. ++ Make Phan aware that `$this` doesn't exist in a static closure (#768) + +Language Server/Daemon mode: ++ Fix another rare bug that can cause crashes in the polyfill/fallback parser when parsing invalid or incomplete ASTs. ++ Add a `--language-server-hide-category` setting to hide the issue category from diagnostic messages. ++ Remove the numeric diagnostic code from the language server diagnostics (a.k.a. issues). + (Certain language clients such as LanguageClient-neovim would render that the code in the quickfix menu, wasting space) ++ Support "go to definition" for union types within all code comment types (#1704) + (e.g. can go to definition in `// some annotation or comment mentioning MyType`) + +New features(Analysis) ++ Support analysis of [`list()` reference assignment](https://wiki.php.net/rfc/list_reference_assignment) for php 7.3 (which is still in alpha). (#1537) ++ Warn about invalid operands of the unary operators `+`, `-`, and `~` + New issue types: `PhanTypeInvalidUnaryOperandNumeric` and `PhanTypeInvalidUnaryOperandBitwiseNot` (#680) + +Bug fixes: ++ Fix a bug causing Phan to infer extra wrong types (`ancestorClass[][]`) for `@return className[]` (#1822) ++ Start warning about assignment operations (e.g. `+=`) when the modified variable isn't referenced later in the function. ++ Make exceptions in `catch{}` always include the type `Throwable` even if the declared type doesn't. (#336) + +16 Jun 2018, Phan 0.12.13 +------------------------- + +New features(Analysis) ++ Support integer literals both in PHPDoc and in Phan's type system. (E.g. `@return -1|string`) + Include integer values in issue messages if the values are known. ++ Support string literals both in PHPDoc and in Phan's type system. (E.g. `@return 'example\n'`) + Phan can now infer possible variable values for dynamic function/method calls, etc. + + Note: By default, Phan does not store representations of strings longer than 50 characters. This can be increased with the `'max_literal_string_type_length'` config. + + Supported escape codes: `\\`, `\'`, `\r`, `\n`, `\t`, and hexadecimal (`\xXX`). ++ Improve inferred types of unary operators. ++ Warn about using `void`/`iterable`/`object` in use statements based on `target_php_version`. (#449) + New issue types: `PhanCompatibleUseVoidPHP70`, `PhanCompatibleUseObjectPHP71`, `PhanCompatibleUseObjectPHP71` ++ Warn about making overrides of inherited property and constants less visible (#788) + New issue types: `PhanPropertyAccessSignatureMismatch`, `PhanPropertyAccessSignatureMismatchInternal`, + `PhanConstantAccessSignatureMismatch`, `PhanConstantAccessSignatureMismatchInternal`. ++ Warn about making static properties into non-static properties (and vice-versa) (#615) + New issue types: `PhanAccessNonStaticToStaticProperty`, `PhanAccessStaticToNonStaticProperty` ++ Warn about inheriting from a class/trait/interface that has multiple possible definitions (#773) + New issue types: `PhanRedefinedExtendedClass`, `PhanRedefinedUsedTrait`, `PhanRedefinedInheritedInterface` ++ Infer more accurate types for the side effects of assignment operators (i.e. `+=`, `.=`, etc) and other binary operations. (#1775) ++ Warn about invalid arguments to binary operators or assignment operators. + New issue types: `PhanTypeInvalidLeftOperandOfAdd`, `PhanTypeInvalidLeftOperandOfNumericOp`, + `PhanTypeInvalidRightOperandOfAdd`, `PhanTypeInvalidRightOperandOfNumericOp` ++ Warn about using negative string offsets and multiple catch exceptions in PHP 7.0 (if `target_php_version` is less than `'7.1'`). (#1771, #1778) + New issue types: `PhanCompatibleMultiExceptionCatchPHP70`, `PhanCompatibleNegativeStringOffset`. + +Maintenance: ++ Update signature map with more accurate signatures (#1761) ++ Upgrade tolerant-php-parser, making the polyfill/fallback able to parse PHP 7.1's multi exception catch. + +Bug fixes: ++ Don't add more generic types to properties with more specific PHPDoc types (#1783). + For example, don't add `array` to a property declared with PHPDoc type `/** @var string[] */` ++ Fix uncaught `AssertionError` when `parent` is used in PHPDoc (#1758) ++ Fix various bugs that can cause crashes in the polyfill/fallback parser when parsing invalid or incomplete ASTs. ++ Fix unparsable/invalid function signature entries of rarely used functions ++ Warn about undefined variables on the left-hand side of assignment operations (e.g. `$x .= 'string'`) (#1613) + +08 Jun 2018, Phan 0.12.12 +------------------------- + +Maintenance: ++ Increase the severity of some issues to critical + (if they are likely to cause runtime Errors in the latest PHP version). + +Bug fixes: ++ Allow suppressing `PhanTypeInvalidThrows*` with doc comment suppressions + in the phpdoc of a function/method/closure. ++ Fix crashes when fork pool is used and some issue types are emitted (#1754) ++ Catch uncaught exception for PhanContextNotObject when calling `instanceof self` outside a class scope (#1754) + +30 May 2018, Phan 0.12.11 +------------------------- + +Language Server/Daemon mode: ++ Make the language server work more reliably when `pcntl` is unavailable. (E.g. on Windows) (#1739) ++ By default, allow the language server and daemon mode to start with the fallback even if `pcntl` is unavailable. + (`--language-server-require-pcntl` can be used to make the language server refuse to start without `pcntl`) + +Bug fixes: ++ Don't crash if `ext-tokenizer` isn't installed (#1747) ++ Fix invalid output of `tool/make_stubs` for APCu (#1745) + +27 May 2018, Phan 0.12.10 +------------------------- + +New features(CLI, Configs) ++ Add CLI flag `--unused-variable-detection`. ++ Add config setting `unused_variable_detection` (disabled by default). + Unused variable detection can be enabled by `--unused-variable-detection`, `--dead-code-detection`, or the config. + +New features(Analysis): ++ Add built-in support for unused variable detection. (#345) + Currently, this is limited to analyzing inside of functions, methods, and closures. + This has some minor false positives with loops and conditional branches. + + Warnings about unused parameters can be suppressed by adding `@phan-unused-param` on the same line as `@param`, + e.g. `@param MyClass $x @phan-unused-param`. + (as well as via standard issue suppression methods.) + + The built in unused variable detection support will currently not warn about any of the following issue types, to reduce false positives. + + - Variables beginning with `$unused` or `$raii` (case-insensitive) + - `$_` (the exact variable name) + - Superglobals, used globals (`global $myGlobal;`), and static variables within function scopes. + - Any references, globals, or static variables in a function scope. + + New Issue types: + - `PhanUnusedVariable`, + - `PhanUnusedVariableValueOfForeachWithKey`, (has a high false positive rate) + - `PhanUnusedPublicMethodParameter`, `PhanUnusedPublicFinalMethodParameter`, + - `PhanUnusedProtectedMethodParameter`, `PhanUnusedProtectedFinalMethodParameter`, + - `PhanUnusedPrivateMethodParameter`, `PhanUnusedProtectedFinalMethodParameter`, + - `PhanUnusedClosureUseVariable`, `PhanUnusedClosureParameter`, + - `PhanUnusedGlobalFunctionParameter` + + This is similar to the third party plugin `PhanUnusedVariable`. + The built-in support has the following changes: + + - Emits fewer/different false positives (e.g. when analyzing loops), but also detects fewer potential issues. + - Reimplemented using visitors extensively (Similar to the code for `BlockAnalysisVisitor`) + - Uses a different data structure from `PhanUnusedVariable`. + This represents all definitions of a variable, instead of just the most recent one. + This approximately tracks the full graph of definitions and uses of variables within a function body. + (This allows warning about all unused definitions, or about definitions that are hidden by subsequent definitions) + - Integration: This is planned to be integrated with other features of Phan, e.g. "Go to Definition" for variables. (Planned for #1211 and #1705) + +Bug fixes: ++ Minor improvements to `UnusedSuppressionPlugin` + +Misc: ++ Support `composer.json`'s `vendor-dir` for `phan --init` + +22 May 2018, Phan 0.12.9 +------------------------ + +New features(CLI, Configs): ++ Add CLI flag `--language-server-enable-go-to-definition`. See the section "Language Server/Daemon mode". ++ Add Config setting `disable_line_based_suppression` to disable line-based suppression from internal comments. See the section "New Features" ++ Add Config setting `disable_file_based_suppression` to disable file-based issue suppressions. + +New features(Analysis): ++ Make `@suppress`, `@phan-suppress`, `@phan-file-suppress` accept a comma separated issue list of issue types to suppress. (#1715) + Spaces aren't allowed before the commas. ++ Implement `@phan-suppress-current-line` and `@phan-suppress-next-line` to suppress issues on the current or next line. + + These can occur within any comment or doc comment (i.e. the comment types for `/*`, `//`, and `/**`) + + These suppressions accept a comma separated list of issue type names. + Commas must be immediately after the previous issue type. + + Note: Phan currently does not support inline comments anywhere else. + Phan also does not associate these inline comments with any information about the current scope. + This suppression is based on tokenizing the PHP file and determining the line based on that comment line. + + Examples: + + ```php + // @phan-suppress-next-line PhanUndeclaredVariable, PhanUndeclaredFunction optional reason goes here + $result = call_undefined_function() + $undefined_variable; + + $closure(); /* @phan-suppress-current-line PhanParamTooFew optional reason for suppression */ + + /** + * This can also be used within doc comments: + + * @phan-suppress-next-line PhanInvalidCommentForDeclarationType optional reason for suppression + * @property int $x + */ + function my_example() { + } + ``` + + `PhanUnusedSuppressionPlugin` is capable of detecting if line-based suppressions are unused. ++ Allow using `@phan-file-suppress` as a regular comment anywhere within a file (`//`, `/*`, or `/**` comments). + Previously, `@phan-file-suppress` could only be used inside the doc comment of an element. + + `@phan-file-suppress` in no-op string literals will be deprecated in a future Phan release. ++ Emit class name suggestions for undeclared types in param, property, return type, and thrown type declarations. (#1689) + + Affects `PhanUndeclaredTypeParameter`, `PhanUndeclaredTypeProperty`, `PhanUndeclaredTypeReturnType`, + `PhanUndeclaredTypeThrowsType`, and `PhanInvalidThrowsIs*` ++ Add `pretend_newer_core_methods_exist` config setting. + If this is set to true (the default), + and `target_php_version` is newer than the version used to run Phan, + Phan will act as though functions added in newer PHP versions exist. + + Note: Currently only affects `Closure::fromCallable()`, which was added in PHP 7.1. + This setting will affect more functions and methods in the future. + +Language Server/Daemon mode: ++ Support "Go to definition" for properties, classes, global/class constants, and methods/global functions (Issue #1483) + (Must pass the CLI option `--language-server-enable-go-to-definition` when starting the server to enable this) ++ Support "Go to type definition" for variables, properties, classes, and methods/global functions (Issue #1702) + (Must pass the CLI option `--language-server-enable-go-to-definition` when starting the server to enable this) + Note that constants can't have object types in PHP, so there's no implementation of "Go To Type Definition" for those. + +Plugins: ++ Add a new plugin capability `SuppressionCapability` + that allows users to suppress issues in additional ways. (#1070) ++ Add a new plugin `SleepCheckerPlugin`. (PR #1696) + Warn about returning non-arrays in sleep, + as well as about returning array values with invalid property names. + + Issue types: `SleepCheckerInvalidReturnStatement`, `SleepCheckerInvalidPropNameType`, `SleepCheckerInvalidPropName`, + `SleepCheckerMagicPropName`, and `SleepCheckerDynamicPropName` ++ Make `PhanPregRegexCheckerPlugin` warn about the `/e` modifier on regexes (#1692) + +Misc: ++ Add simple integration test for the language server mode. + +Bug fixes: ++ Be more consistent about emitting `PhanUndeclaredType*` for invalid types within array shapes. ++ Avoid a crash when the left-hand side of an assignment is invalid. (#1693) ++ Prevent an uncaught `TypeError` when integer variable names (e.g. `${42}`) are used in branches (Issue #1699) + +12 May 2018, Phan 0.12.8 +------------------------ + +Bug fixes ++ Fix a crash that occurs when the `iterable<[KeyType,]ValueType>` annotation is used in phpdoc. (#1685) + +08 May 2018, Phan 0.12.7 +------------------------ + +New features: ++ For `PhanUndeclaredMethod` and `PhanUndeclaredStaticMethod` issues, suggest visible methods (in the same class) with similar names. ++ For `PhanUndeclaredConstant` issues (for class constants), suggest visible constants (in the same class) with similar names. ++ For `PhanUndeclaredProperty` and `PhanUndeclaredStaticProperty` issues, suggest visible properties (in the same class) with similar names. ++ When suggesting alternatives to undeclared classes, + also include suggestions for similar class names within the same namespace as the undeclared class. + (Comparing Levenshtein distance) + +Language Server/Daemon mode ++ Make the latest version of `phan_client` include any suggestion alongside the issue message (for daemon mode). ++ Include text from suggestions in Language Server Protocol output + +Bug fixes ++ Fix a bug generating variable suggestions when there were multiple similar variable names + (The suggestions that would show up might not be the best set of suggestions) ++ Fix a crash in the tolerant-php-parser polyfill seen when typing out an echo statement ++ Fix incorrect suggestions to use properties (of the same name) instead of undeclared variables in class scopes. + (Refer to static properties as `self::$name` and don't suggest inaccessible inherited private properties) ++ Don't suggest obviously invalid alternatives to undeclared classes. + (E.g. don't suggest traits or interfaces for `new MisspelledClass`, don't suggest interfaces for static method invocations) + +06 May 2018, Phan 0.12.6 +------------------------ + +New features(Analysis) ++ Warn about properties that are read but not written to when dead code detection is enabled + (Similar to existing warnings about properties that are written to but never read) + New issue types: `PhanReadOnlyPrivateProperty`, `PhanReadOnlyProtectedProperty`, `PhanReadOnlyPublicProperty` ++ When warning about undeclared classes, mention any classes that have the same name (but a different namespace) as suggestions. + + E.g. `test.php:26 PhanUndeclaredClassInstanceof Checking instanceof against undeclared class \MyNS\InvalidArgumentException (Did you mean class \InvalidArgumentException)` ++ When warning about undeclared variables (outside of the global scope), mention any variables that have similar names (based on case-insensitive Levenshtein distance) as suggestions. + + In method scopes: If `$myName` is undeclared, but `$this->myName` is declared (or inherited), `$this->myName` will be one of the suggestions. ++ Warn about string and numeric literals that are no-ops. (E.g. ``) + New issue types: `PhanNoopStringLiteral`, `PhanNoopEncapsulatedStringLiteral`, `PhanNoopNumericLiteral`. + + Note: This will not warn about Phan's [inline type checks via string literals](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#inline-type-checks-via-string-literals) ++ When returning an array literal (with known keys) directly, + make Phan infer the array literal's array shape type instead of a combination of generic array types. ++ Make type casting rules stricter when checking if an array shape can cast to a given generic array type. + (E.g. `array{a:string,b:int}` can no longer cast to `array`, but can cast to `array|array`). + + E.g. Phan will now warn about `/** @return array */ function example() { $result = ['a' => 'x', 'b' => 2]; return $result; }` ++ Warn about invalid expressions/variables encapsulated within double-quoted strings or within heredoc strings. + New issue type: `TypeSuspiciousStringExpression` (May also emit `TypeConversionFromArray`) + ++ Add support for template params in iterable types in phpdoc. (#824) + Phan supports `iterable` and `iterable` syntaxes. (Where TKey and TValue are union types) + Phan will check that generic arrays and array shapes can cast to iterable template types. ++ Add support for template syntax of Generator types in phpdoc. (#824) + Supported syntaxes are: + + 1. `\Generator` + 2. `\Generator` + 3. `\Generator` (TSend is the expected type of `$x` in `$x = yield;`) + 4. `\Generator` (TReturn is the expected type of `expr` in `return expr`) + + New issue types: `PhanTypeMismatchGeneratorYieldValue`, `PhanTypeMismatchGeneratorYieldKey` (For comparing yield statements against the declared `TValue` and `TKey`) + + Additionally, Phan will use `@return Generator|TValue[]` to analyze the yield statements + within a function/method body the same way as it would analyze `@return Generator`. + (Analysis outside the method would not change) + ++ Add support for template params in Iterator and Traversable types in phpdoc. (#824) + NOTE: Internal subtypes of those classes (e.g. ArrayObject) are not supported yet. + Supported syntaxes are: + + 1. `Traversable`/`Iterator` + 2. `Traversable`/`Iterator` + ++ Analyze `yield from` statements. + + New issue types: `PhanTypeInvalidYieldFrom` (Emitted when the expression passed to `yield from` is not a Traversable or an array) + + Warnings about the inferred keys/values of `yield from` being invalid reuse `PhanTypeMismatchGeneratorYieldValue` and `PhanTypeMismatchGeneratorYieldKey` ++ Make the union types within the phpdoc template syntax of `iterator`/`Traversable`/`Iterator`/`Generator` affect analysis of the keys/values of `foreach` statements ++ Improve Phan's analysis of array functions modifying arguments by reference, reducing false positives. (#1662) + Affects `array_shift`/`array_unshift`/`array_push`/`array_pop`/`array_splice`. + +Misc ++ Infer that a falsey array is the empty array shape. + +Bug Fixes ++ Consistently warn about unreferenced declared properties (i.e. properties that are not magic or dynamically added). + Previously, Phan would just never warn if the class had a `__get()` method (as a heuristic). + +03 Apr 2018, Phan 0.12.5 +------------------------ + +Plugins ++ Add an option `'php_native_syntax_check_max_processes'` to `'plugin_config'` for `InvokePHPNativeSyntaxCheckPlugin`. + +Bug Fixes ++ Remove extra whitespace from messages of comment text in `UnextractableAnnotationElementName` (e.g. `"\r"`) ++ Fix bugs in `InvokePHPNativeSyntaxCheckPlugin` + +31 Mar 2018, Phan 0.12.4 +------------------------ + +New Features(CLI, Configs) ++ Add a `strict_param_checking` config setting. (And a `--strict-param-checking` CLI flag) + If this is set to true, then Phan will warn if at least one of the types + in an argument's union type can't cast to the expected parameter type. + New issue types: `PhanPartialTypeMismatchArgument`, `PhanPossiblyNullTypeArgument`, and `PhanPossiblyFalseTypeArgument` + (along with equivalents for internal functions and methods) + + Setting this to true will likely introduce large numbers of warnings. + Those issue types would need to be suppressed entirely, + or with `@phan-file-suppress`, or with `@suppress`. ++ Add a `strict_property_checking` config setting. (And a `--strict-property-checking` CLI flag) + If this is set to true, then Phan will warn if at least one of the types + in an assignment's union type can't cast to the expected property type. + New issue types: `PhanPartialTypeMismatchProperty`, `PhanPossiblyNullTypeProperty`, and `PhanPossiblyFalseTypeProperty` + + NOTE: This option does not make Phan check if all possible expressions have a given property, but may do that in the future. ++ Add a `strict_return_checking` config setting. (And a `--strict-return-checking` CLI flag) + If this is set to true, then Phan will warn if at least one of the types + in a return statement's union type can't cast to the expected return type. + New issue types: `PhanPartialTypeMismatchReturn`, `PhanPossiblyNullTypeReturn`, and `PhanPossiblyFalseTypeReturn` + + Setting this to true will likely introduce large numbers of warnings. + Those issue types would need to be suppressed entirely, + or with `@phan-file-suppress`, or with `@suppress`. ++ Add a `--strict-type-checking` CLI flag, to enable all of the new strict property/param/return type checks. ++ Add a `guess_unknown_parameter_type_using_default` config, + which can be enabled to make Phan more aggressively infer the types of undocumented optional parameters + from the parameter's default value. + E.g. `function($x = 'val')` would make Phan infer that the function expects $x to have a type of `string`, not `string|mixed`. + +Plugins ++ Add a new plugin `InvokePHPNativeSyntaxCheckPlugin` on all analyzed files (but not files excluded from analysis) (#629) ++ Add a new plugin capability `AfterAnalyzeFileCapability` that runs after a given file is analyzed. + This does not get invoked for files that are excluded from analysis, or for empty files. + +New Features(Analysis) ++ Detect unreachable catch statements (#112) + (Check if an earlier catch statement caught an ancestor of a given catch statement) ++ Support phpdoc3's `scalar` type in phpdoc. (#1589) + That type is equivalent to `bool|float|int|string`. ++ Improve analysis of return statements with ternary conditionals (e.g. `return $a ?: $b`). ++ Start analyzing negated `instanceof` conditionals such as `assert(!($x instanceof MyClass))`. ++ Infer that the reference parameter's resulting type for `preg_match` is a `string[]`, not `array` (when possible) + (And that the type is `array{0:string,1:int}[]` when `PREG_OFFSET_CAPTURE` is passed as a flag) ++ Warn in more places when Phan can't extract union types or element identifiers from a doc comment. + New issue types: `UnextractableAnnotationElementName`, `UnextractableAnnotationSuffix`. + (E.g. warn about `@param int description` (ideally has param name) and `@return int?` (Phan doesn't parse the `?`, should be `@return ?int`)) + +Bug Fixes ++ Don't emit false positive `PhanTypeArraySuspiciousNullable`, etc. for complex isset/empty/unset expressions. (#642) ++ Analyze conditionals wrapped by `@(cond)` (e.g. `if (@array_key_exists('key', $array)) {...}`) (#1591) ++ Appending an unknown type to an array shape should update Phan's inferred keys(int) and values(mixed) of an array. (#1560) ++ Make line numbers for arguments more accurate ++ Infer that the result of `|` or `&` on two strings is a string. ++ Fix a crash caused by empty FQSENs for classlike names or function names (#1616) + +24 Mar 2018, Phan 0.12.3 +------------------------ + +New Features(CLI, Configs) ++ Add `--polyfill-parse-all-element-doc-comments` for PHP 7.0. + If you're using the polyfill (e.g. using `--force-polyfill-parser`), this will parse doc comments on class constants in php 7.0. + (Normally, the polyfill wouldn't include that information, to closely imitate `php-ast`'s behavior) + +New Features(Analysis) ++ Infer the type of `[]` as `array{}` (the empty array), not `array`. (#1382) ++ Allow phpdoc `@param` array shapes to contain optional fields. (E.g. `array{requiredKey:int,optionalKey?:string}`) (#1382) + An array shape is now allowed to cast to another array shape, as long as the required fields are compatible with the target type, + and any optional fields from the target type are absent in the source type or compatible. ++ In issue messages, represent closures by their signatures instead of as `\closure_{hexdigits}` ++ Emit `PhanTypeArrayUnsetSuspicious` when trying to unset the offset of something that isn't an array or array-like. ++ Add limited support for analyzing `unset` on variables and the first dimension of arrays. + Unsetting variables does not yet work in conditional branches. ++ Don't emit `PhanTypeInvalidDimOffset` in `isset`/`empty`/`unset` ++ Improve Phan's analysis of loose equality (#1101) ++ Add new issue types `PhanWriteOnlyPublicProperty`, `PhanWriteOnlyProtectedProperty`, and `PhanWriteOnlyPrivateProperty`, + which will be emitted on properties that are written to but never read from. + (Requires that dead code detection be enabled) ++ Improve Phan's analysis of switch statements and fix bugs. (#1561) ++ Add `PhanTypeSuspiciousEcho` to warn about suspicious types being passed to echo/print statements. + This now warns about booleans, arrays, resources, null, non-stringable classes, combinations of those types, etc. + (`var_export` or JSON encoding usually makes more sense for a boolean/null) ++ Make Phan infer that top-level array keys for expressions such as `if (isset($x['keyName']))` exist and are non-null. (#1514) ++ Make Phan infer that top-level array keys for expressions such as `if (array_key_exists('keyName', $x))` exist. (#1514) ++ Make Phan aware of types after negated of `isset`/`array_key_exists` checks for array shapes (E.g. `if (!array_key_exists('keyName', $x)) { var_export($x['keyName']); }`) + Note: This will likely fail to warn if the variable is already a mix of generic arrays and array shapes. ++ Make Phan check that types in `@throws` annotations are valid; don't warn about classes in `@throws` being unreferenced. (#1555) + New issue types: `PhanUndeclaredTypeThrowsType`, `PhanTypeInvalidThrowsNonObject`, `PhanTypeInvalidThrowsNonThrowable`, `PhanTypeInvalidThrowsIsTrait`, `PhanTypeInvalidThrowsIsInterface` + +New types: ++ Add `Closure` and `callable` with annotated param types and return to Phan's type system(#1578, #1581). + This is not a part of the phpdoc2 standard or any other standard. + These can be used in any phpdoc tags that Phan is aware of, + to indicate their expected types (`@param`, `@var`, `@return`, etc.) + + Examples: + + - `function(int $x) : ?int {return $x;}` has the type `Closure(int):?int`, + which can cast to `callable(int):?int` + - `function(array &$x) {$x[] = 2;}` has the type `Closure(array&):void` + - `function(int $i = 2, int ...$args) : void {}` + has the type `Closure(int=,int...):void` + + Note: Complex return types such as `int[]` or `int|false` + **must** be surrounded by brackets to avoid potential ambiguities. + + - e.g. `Closure(int|array): (int[])` + - e.g. `Closure(): (int|false)` + - e.g. `Closure(): (array{key:string})` is not ambiguous, + but the return type must be surrounded by brackets for now + + Other notes: + - For now, the inner parameter list of `Closure(...)` + cannot contain the characters `(` or `)` + (or `,`, except to separate the arguments) + Future changes are planned to allow those characters. + - Phan treats `Closure(T)` as an alias of `Closure(T):void` + - Placeholder variable names can be part of these types, + similarly to `@method` (`Closure($unknown,int $count=0):T` + is equivalent to `Closure(mixed,int):T` + +Maintenance ++ Add `--disable-usage-on-error` option to `phan_client` (#1540) ++ Print directory which phan daemon is going to await analysis requests for (#1544) ++ Upgrade the dependency `Microsoft/tolerant-php-parser` to 0.0.10 (includes minor bug fixes) + +Bug Fixes ++ Allow phpdoc `@param` array shapes to contain union types (#1382) ++ Remove leading `./` from Phan's relative paths for files (#1548, #1538) ++ Reduce false positives in dead code detection for constants/properties/methods. ++ Don't warn when base classes access protected properties of their subclasses. + +02 Mar 2018, Phan 0.12.2 +------------------------ + +New Features(Analysis) + ++ Emit `PhanTypeInvalidDimOffsetArrayDestructuring` when an unknown offset value is used in an array destructuring assignment (#1534, #1477) + (E.g. `foreach ($expr as ['key' => $value])`, `list($k) = [2]`, etc.) + +Plugins ++ Add a new plugin capability `PostAnalyzeNodeCapability` (preferred) and `LegacyPostAnalyzeNodeCapability`. + These capabilities give plugins for post-order analysis access to a list of parent nodes, + instead of just the last parent node. + Plugin authors should use these instead of `AnalyzeNodeCapability` and `LegacyAnalyzeNodeCapability`. + + (`parent_node_list` is set as an instance property on the visitor returned by PostAnalyzeNodeCapability + if the instance property was declared) + +Maintenance: ++ Speed up analysis when quick mode isn't used. + +Bug Fixes ++ Reduce false positives in `PhanTypeInvalidDimOffset` ++ Don't warn when adding new keys to an array when assigning multiple dimensions at once (#1518) ++ Reduce false positives when a property's type gets inferred as an array shape(#1520) ++ Reduce false positives when adding fields to an array in the global scope. ++ Reduce false positives by converting array shapes to generic arrays before recursively analyzing method/function invocations (#1525) + +28 Feb 2018, Phan 0.12.1 +------------------------ + +New Features(Analysis) ++ Emit `PhanTypeInvalidDimOffset` when an unknown offset is fetched from an array shape type. (#1478) + +Bug Fixes ++ Fix an "Undefined variable" error when checking for php 7.1/7.0 incompatibilities in return types. (#1511) + Fix other crashes. + +25 Feb 2018, Phan 0.12.0 +------------------------ + +The Phan 0.12.0 release supports analysis of php 7.0-7.2, and can be executed with php 7.0+. +This release replaces the previous releases (The 0.11 releases for php 7.2, the 0.10 releases for php 7.1, and the 0.8 releases for php 7.0) +Because Phan uses Reflection, it's recommended to use the same PHP minor version for analyzing the code as would be used to run the code. +(For the small number of function/method signatures, etc., that were added or changed in each minor release of PHP.) + +After upgrading Phan, projects using phan should add a `target_php_version` setting to their `.phan/config.php`. + +New Features(CLI, Configs) ++ Add a `target_php_version` config setting, which can be set to `'7.0'`, `'7.1'`, `'7.2'`, or `null`/`'native'`. (#1174) + This defaults to the same PHP minor version as the PHP binary used to run Phan. + `target_php_version` can be overridden via the CLI option `--target-php-version {7.0,7.1,7.2,native}` + + NOTE: This setting does not let a PHP 7.0 installation parse PHP 7.1 nullable syntax or PHP 7.1 array destructuring syntax. + + If you are unable to upgrade the PHP version used for analysis to php 7.1, the polyfill parser settings may help + (See `--force-polyfill-parser` or `--use-fallback-parser`. Those have a few known bugs in edge cases.) ++ Add `--init` CLI flag and CLI options to affect the generated config. (#145) + (Options: `--init-level=1..5`, `--init-analyze-dir=path/to/src`, `--init-analyze-file=path/to/file.php`, `--init-no-composer`, `--init-overwrite`) + +New Features(Analysis) ++ In doc comments, support `@phan-var`, `@phan-param`, `@phan-return`, `@phan-property`, and `@phan-method`. (#1470) + These annotations will take precedence over `@var`, `@param`, `@return`, `@property`, and `@method`. ++ Support `@phan-suppress` as an alias of `@suppress`. ++ Add a non-standard way to explicitly set var types inline. (#890) + `; '@phan-var T $varName'; expression_using($varName);` and + `; '@phan-var-force T $varName'; expression_using($varName);` + + If Phan sees a string literal containing `@phan-var` as a top-level statement of a statement list, it will immediately set the type of `$varName` to `T` without any type checks. + (`@phan-var-force T $x` will do the same thing, and will create the variable if it didn't already exist). + + Note: Due to limitations of the `php-ast` parser, Phan isn't able to use inline doc comments, so this is the solution that was used instead. + + Example Usage: + + ```php + $values = mixed_expression(); + + // Note: This annotation must go **after** setting the variable. + // This has to be a string literal; phan cannot parse inline doc comments. + '@phan-var array $values'; + + foreach ($x as $instance) { + function_expecting_myclass($x); + } + ``` ++ Add a way to suppress issues for the entire file (including within methods, etc.) (#1190) + The `@phan-file-suppress` annotation can also be added to phpdoc for classes, etc. + This feature is recommended for use at the top of the file or on the first class in the file. + It may or may not affect statements above the suppression. + This feature may fail to catch certain issues emitted during the parse phase. + + ```php + 'value']` (#1383) + +Language Server ++ Support running Language Server and daemon mode on Windows (#819) + (the `pcntl` dependency is no longer mandatory for running Phan as a server) + The `--language-server-allow-missing-pcntl` option must be set by the client. + + When this fallback is used, Phan **manually** saves and restores the + data structures that store information about the project being analyzed. + + This fallback is new and experimental. ++ Make Phan Language Server analyze new files added to a project (Issue #920) ++ Analyze all of the PHP files that are currently opened in the IDE + according to the language server client, + instead of just the most recently edited file (Issue #1147) + (E.g. analyze other files open in tabs or split windows) ++ When closing or deleting a file, clear the issues that were emitted + for that file. ++ If analysis requests (opening files, editing files, etc) + are arriving faster than Phan can analyze and generate responses, + then buffer the file changes (until end of input) + and then begin to generate analysis results. + + Hopefully, this should reduce the necessity for limiting Phan to + analyzing only on save. + +Bug fixes ++ In files with multiple namespaces, don't use `use` statements from earlier namespaces. (#1096) ++ Fix bugs analyzing code using functions/constants provided by group use statements, in addition to `use function` and `use const` statements. + +14 Feb 2018, Phan 0.11.3 +------------------------ + +### Ported from Phan 0.10.5 + +New Features(CLI, Configs) ++ Add `--allow-polyfill-parser` and `--force-polyfill-parser` options. + These allow Phan to be run without installing `php-ast`. + + Using the native `php-ast` extension is still recommended. + The polyfill is slower and has several known bugs. + + Additionally, the way doc comments are parsed by the polyfill is different. + Doc comments for elements such as closures may be parsed differently from `php-ast` + +Maintenance: ++ Fix bugs in the `--use-fallback-parser` mode. + Upgrade the `tolerant-php-parser` dependency (contains bug fixes and performance improvements) + +Bug fixes ++ Fix a bug in `tool/make_stubs` when generating stubs of namespaced global functions. ++ Fix a refactoring bug that caused methods and properties to fail to be inherited (#1456) ++ If `ignore_undeclared_variables_in_global_scope` is true, then analyze `assert()` + and conditionals in the global scope as if the variable was defined after the check. + +11 Feb 2018, Phan 0.11.2 +------------------------ + +### Ported from Phan 0.10.4 + +New Features(Analysis) + ++ Support array key types of `int`, `string`, and `mixed` (i.e. `int|string`) in union types such as `array` (#824) + + Check that the array key types match when assigning expected param types, return types, property types, etc. + By default, an array with a key type of `int` can't cast to an array key type of `string`, or vice versa. + Mixed union types in keys can cast to/from any key type. + + - To allow casting `array` to `array`, enable `scalar_array_key_cast` in your `.phan/config.php`. + ++ Warn when using the wrong type of array keys offsets to fetch from an array (E.g. `string` key for `array`) (Issue #1390) ++ Infer array key types of `int`, `string`, or `int|string` in `foreach` over arrays. (#1300) + (Phan's type system doesn't support inferring key types for `iterable` or `Traversable` right now) ++ Support **parsing** PHPDoc array shapes + (E.g. a function expecting `['field' => 'a string']` can document this as `@param array{field:string}` $options) + For now, this is converted to generic arrays (Equivalent to `string[]`). + + `[[0, ...], new stdClass]` would have type `array{0:int[], 1:string}` + + - The field value types can be any union type. + - Field keys are currently limited to keys matching the regex `[-_.a-zA-Z0-9\x7f-\xff]+`. (Identifiers, numbers, '-', and '.') + Escape mechanisms such as backslashes (e.g. "\x20" for " ") may be supported in the future. ++ Add `PhanTypeMismatchUnpackKey` and `PhanTypeMismatchUnpackValue` to analyze array unpacking operator (also known as splat) (#1384) + + Emit `PhanTypeMismatchUnpackKey` when passing iterables/arrays with invalid keys to the unpacking operator (i.e. `...`). + + Emit `PhanTypeMismatchUnpackValue` when passing values that aren't iterables or arrays to the unpacking operator. + (See https://secure.php.net/manual/en/migration56.new-features.php#migration56.new-features.splat) ++ When determining the union type of an array literal, + base it on the union types of **all** of the values (and all of the keys) instead of just the first 5 array elements. ++ When determining the union type of the possible value types of an array literal, + combine the generic types into a union type instead of simplifying the types to `array`. + In practical terms, this means that `[1,2,'a']` is seen as `array`, + which Phan represents as `array|array`. + + In the previous Phan release, the union type of `[1,2,'a']` would be represented as `int[]|string[]`, + which is equivalent to `array|array` + + Another example: `[$strKey => new MyClass(), $strKey2 => $unknown]` will be represented as + `array|array`. + (If Phan can't infer the union type of a key or value, `mixed` gets added to that key or value.) ++ Improve analysis of try/catch/finally blocks (#1408) + Analyze `catch` blocks with the inferences about the `try` block. + Analyze a `finally` block with the combined inferences from the `try` and `catch` blocks. ++ Account for side effects of `&&` and `||` operators in expressions, outside of `if`/`assert` statements. (#1415) + E.g. `$isValid = ($x instanceof MyClass && $x->isValid())` will now consistently check that isValid() exists on MyClass. ++ Improve analysis of expressions within conditionals, such as `if (!($x instanceof MyClass) || $x->method())` + or `if (!(cond($x) && othercond($x)))` + + (Phan is now aware of the types of the right-hand side of `||` and `&&` in more cases) ++ Add many param and return type signatures for internal functions and methods, + for params and return types that were previously untyped. + (Imported from docs.php.net's SVN repo) ++ More precise analysis of the return types of `var_export()`, `print_r()`, and `json_decode()` (#1326, #1327) ++ Improve type narrowing from `iterable` to `\Traversable`/`array` (#1427) + This change affects `is_array()`/`is_object()` checks and their negations. ++ Fix more edge cases which would cause Phan to fail to infer that properties, constants, or methods are inherited. (PR #1440 for issues #311, #1426, #454) + +Plugins ++ Fix bugs in `NonBoolBranchPlugin` and `NonBoolInLogicalArithPlugin` (#1413, #1410) ++ **Make UnionType instances immutable.** + This will affect plugins that used addType/addUnionType/removeType. withType/withUnionType/withoutType should be used instead. + To modify the type of elements(properties, method return types, parameters, variables, etc), + plugin authors should use `Element->setUnionType(plugin_modifier_function(Element->getUnionType()))`. + +Language server: ++ Add a CLI option `--language-server-analyze-only-on-save` to prevent the client from sending change notifications. (#1325) + (Only notify the language server when the user saves a document) + This significantly reduces CPU usage, but clients won't get notifications about issues immediately. + +Bug fixes ++ Warn when attempting to call an instance method on an expression with type string (#1314). ++ Fix a bug in `tool/make_stubs` when generating stubs of global functions. ++ Fix some bugs that occurred when Phan resolved inherited class constants in class elements such as properties. (#537 and #454) ++ Emit an issue when a function/method's parameter defaults refer to an undeclared class constant/global constant. + +20 Jan 2018, Phan 0.11.1 +------------------------ + +### Ported from Phan 0.10.3 + +New Features(CLI, Configs) + ++ For `--fallback-parser`: Switch to [tolerant-php-parser](https://github.com/Microsoft/tolerant-php-parser) + as a dependency of the fallback implementation. (#1125) + This does a better job of generating PHP AST trees when attempting to parse code with a broader range of syntax errors. + Keep `PHP-Parser` as a dependency for now for parsing strings. + +Maintenance ++ Various performance optimizations, including caching of inferred union types to avoid unnecessary recalculation. ++ Make `phan_client` and the vim snippet in `plugins/vim/phansnippet.vim` more compatible with neovim ++ Upgrade felixfbecker/advanced-json-rpc dependency to ^3.0.0 (#1354) ++ Performance improvements. + Changed the internal representation of union types to no longer require `spl_object_id` or the polyfill. + +Bug Fixes ++ Allow `null` to be passed in where a union type of `mixed` was expected. ++ Don't warn when passing `?T` (PHPDoc or real) where the PHPDoc type was `T|null`. (#609, #1090, #1192, #1337) + This is useful for expressions used for property assignments, return statements, function calls, etc. ++ Fix a few of Phan's signatures for internal functions and methods. + +17 Nov 2017, Phan 0.11.0 +------------------------ + +New Features (Analysis of PHP 7.2) ++ Support analyzing the `object` type hint in real function/method signatures. (#995) ++ Allow widening an overriding method's param types in php 7.2 branch (#1256) + Phan continues warning about `ParamSignatureRealMismatchHasNoParamType` by default, in case a project needs to work with older php releases. + Add `'allow_method_param_type_widening' => true` if you wish for Phan to stop emitting that issue category. ++ Miscellaneous function signature changes for analysis of PHP 7.2 codebases (#828) + +### Ported from Phan 0.10.2 + +New Features(Analysis) ++ Enable `simplify_ast` by default. + The new default value should reduce false positives when analyzing conditions of if statements. (#407, #1066) ++ Support less ambiguous `?(T[])` and `(?T)[]` in phpdoc (#1213) + Note that `(S|T)[]` is **not** supported yet. ++ Support alternate syntax `array` and `array` in phpdoc (PR #1213) + Note that Phan ignores the provided value of `Key` completely right now (i.e. same as `T[]`); Key types will be supported in Phan 0.10.3. ++ Speed up Phan analysis on small projects, reduce memory usage (Around 0.15 seconds and 15MB) + This was done by deferring loading the information about internal classes and functions until that information was needed for analysis. ++ Analyze existence and usage of callables passed to (internal and user-defined) function&methods expecting callable. (#1194) + Analysis will now warn if the referenced function/method of a callable array/string + (passed to a function/method expecting a callable param) does not exist. + + This change also reduces false positives in dead code detection (Passing in these callable arrays/strings counts as a reference now) ++ Warn if attempting to read/write to a property or constant when the expression is a non-object. (or not a class name, for static elements) (#1268) ++ Split `PhanUnreferencedClosure` out of `PhanUnreferencedFunction`. (Emitted by `--dead-code-detection`) ++ Split `PhanUnreferencedMethod` into `PhanUnreferencedPublicMethod`, `PhanUnreferencedProtectedMethod`, and `PhanUnreferencedPrivateMethod`. ++ Split errors for class constants out of `PhanUnreferencedConst`: + Add `PhanUnreferencedPublicClassConst`, `PhanUnreferencedProtectedClassConst`, and `PhanUnreferencedPrivateClassConst`. + `PhanUnreferencedConst` is now exclusively used for global constants. ++ Analyze uses of `compact()` for undefined variables (#1089) ++ Add `PhanParamSuspiciousOrder` to warn about mixing up variable and constant/literal arguments in calls to built in string/regex functions + (`explode`, `strpos`, `mb_strpos`, `preg_match`, etc.) ++ Preserve the closure's function signature in the inferred return value of `Closure::bind()`. (#869) ++ Support indicating that a reference parameter's input value is unused by writing `@phan-output-reference` on the same line as an `@param` annotation. + This indicates that Phan should not warn about the passed in type, and should not preserve the passed in type after the call to the function/method. + (In other words, Phan will analyze a user-defined reference parameter the same way as it would `$matches` in `preg_match($pattern, $string, $matches)`) + Example usage: `/** @param string $x @phan-output-reference */ function set_x(&$x) { $x = 'result'; }` ++ Make phan infer unreachability from fatal errors such as `trigger_error($message, E_USER_ERROR);` (#1224) ++ Add new issue types for places where an object would be expected: + `PhanTypeExpectedObjectPropAccess`, `PhanTypeExpectedObjectPropAccessButGotNull`, `PhanTypeExpectedObjectStaticPropAccess`, + `PhanTypeExpectedObject`, and `PhanTypeExpectedObjectOrClassName ++ Emit more accurate line numbers for phpdoc comments, when warning about phpdoc in doc comments being invalid. (#1294) + This gives up and uses the element's line number if the phpdoc ends over 10 lines before the start of the element. ++ Work on allowing union types to be part of template types in doc comments, + as well as types with template syntax. + (e.g. `array` is now equivalent to `int[]|string[]`, + and `MyClass` can now be parsed in doc comments) ++ Disambiguate the nullable parameter in output. + E.g. an array of nullable integers will now be printed in error messages as `(?int)[]` + A nullable array of integers will continue to be printed in error messages as `?int[]`, and can be specified in PHPDoc as `?(int[])`. + +New Features (CLI, Configs) ++ Improve default update rate of `--progress-bar` (Update it every 0.10 seconds) + +Bug Fixes ++ Fixes bugs in `PrintfCheckerPlugin`: Alignment goes before width, and objects with __toString() can cast to %s. (#1225) ++ Reduce false positives in analysis of gotos, blocks containing gotos anywhere may do something other than return or throw. (#1222) ++ Fix a crash when a magic method with a return type has the same name as a real method. ++ Allow methods to have weaker PHPdoc types than the overridden method in `PhanParamSignatureMismatch`. (#1253) + `PhanParamSignatureRealMismatch*` is unaffected, and will continue working the same way in Phan releases analyzing PHP < 7.2. ++ Stop warning about `PhanParamSignatureMismatch`, etc. for private methods. The private methods don't affect each other. (#1250) ++ Properly parse `?self` as a *nullable* instance of the current class in union types (#1264) ++ Stop erroneously warning about inherited constants being unused in subclasses for dead code detection (#1260) ++ For dead code detection, properly track uses of inherited class elements (methods, properties, classes) as uses of the original definition. (#1108) + Fix the way that uses of private/protected methods from traits were tracked. + Also, start warning about a subset of issues from interfaces and abstract classes (e.g. unused interface constants) ++ Properly handle `static::class` as a class name in an array callable, or `static::method_name` in a string callable (#1232) ++ Make `@template` tag for [Generic Types](https://github.com/phan/phan/wiki/Generic-Types) case-sensitive. (#1243) ++ Fix a bug causing Phan to infer an empty union type (which can cast to any type) for arrays with elements of empty union types. (#1296) + +Plugins ++ Make DuplicateArrayKeyPlugin start warning about duplicate values of known global constants and class constants. (#1139) ++ Make DuplicateArrayKeyPlugin start warning about case statements with duplicate values (This resolves constant values the same way as array key checks) ++ Support `'plugins' => ['AlwaysReturnPlugin']` as shorthand for full relative path to a bundled plugin such as AlwaysReturnPlugin.php (#1209) + +Maintenance ++ Performance improvements: Phan analysis is 13%-22% faster than 0.10.1, with `simplify_ast` enabled. ++ Used PHP_CodeSniffer to automatically make Phan's source directory adhere closely to PSR-1 and PSR-2, making minor changes to many files. + (e.g. which line each brace goes on, etc.) ++ Stop tracking references to internal (non user-defined) elements (constants, properties, functions, classes, and methods) during dead code detection. + (Dead code detection now requires an extra 15MB instead of 17MB for self-analysis) + +20 Oct 2017, Phan 0.10.1 +------------------------ + +New Features(Analysis) ++ Support `@return $this` in phpdoc for methods and magic methods. + (but not elsewhere. E.g. `@param $this $varName` is not supported, use `@param static $varName`) (#634) ++ Check if functions/methods passed to `array_map` and `array_filter` are compatible with their arguments. + Recursively analyze the functions/methods passed to `array_map`/`array_filter` if no types were provided. (unless quick mode is being used) + +New Features (CLI, Configs) + ++ Add Language Server Protocol support (Experimental) (#821) + Compatibility: Unix, Linux (depends on php `pcntl` extension). + This has the same analysis capabilities provided by [daemon mode](https://github.com/phan/phan/wiki/Using-Phan-Daemon-Mode#using-phan_client-from-an-editor). + Supporting a standard protocol should make it easier to write extensions supporting Phan in various IDEs. + See https://github.com/Microsoft/language-server-protocol/blob/master/README.md ++ Add config (`autoload_internal_extension_signatures`) to allow users to specify PHP extensions (modules) used by the analyzed project, + along with stubs for Phan to use (instead of ReflectionFunction, etc) if the PHP binary used to run Phan doesn't have those extensions enabled. (#627) + Add a script (`tool/make_stubs`) to output the contents of stubs to use for `autoload_internal_extension_signatures` (#627). ++ By default, automatically restart Phan without Xdebug if Xdebug is enabled. (#1161) + If you wish to analyze a project using Xdebug's functions, set `autoload_internal_extension_signatures` + (e.g. `['xdebug' => 'vendor/phan/phan/.phan/internal_stubs/xdebug.phan_php']`) + If you wish to use Xdebug to debug Phan's analysis itself, set and export the environment variable `PHAN_ALLOW_XDEBUG=1`. ++ Improve analysis of return types of `array_pop`, `array_shift`, `current`, `end`, `next`, `prev`, `reset`, `array_map`, `array_filter`, etc. + See `ArrayReturnTypeOverridePlugin.php.` + Phan can analyze callables (for `array_map`/`array_filter`) of `Closure` form, as well as strings/2-part arrays that are inlined. ++ Add `--memory-limit` CLI option (e.g. `--memory-limit 500M`). If this option isn't provided, there is no memory limit. (#1148) + +Maintenance ++ Document the `--disable-plugins` CLI flag. + +Plugins ++ Add a new plugin capability `ReturnTypeOverrideCapability` which can override the return type of functions and methods on a case by case basis. + (e.g. based on one or more of the argument types or values) (related to #612, #1181) ++ Add a new plugin capability `AnalyzeFunctionCallCapability` which can add logic to analyze calls to a small subset of functions. + (e.g. based on one or more of the argument types or values) (#1181) ++ Make line numbers more accurate in `DuplicateArrayKeyPlugin`. ++ Add `PregRegexCheckerPlugin` to check for invalid regexes. (uses `AnalyzeFunctionCallCapability`). + This plugin is able to resolve literals, global constants, and class constants as regexes. + See [the corresponding section of .phan/plugins/README.md](.phan/plugins/README.md#pregregexcheckerpluginphp) ++ Add `PrintfCheckerPlugin` to check for invalid format strings or incorrect arguments in printf calls. (uses `AnalyzeFunctionCallCapability`) + This plugin is able to resolve literals, global constants, and class constants as format strings. + See [the corresponding section of .phan/plugins/README.md](.phan/plugins/README.md#printfcheckerpluginphp) + +Bug Fixes ++ Properly check for undeclared classes in arrays within phpdoc `@param`, `@property`, `@method`, `@var`, and `@return` (etc.) types. + Also, fix a bug in resolving namespaces of generic arrays that are nested 2 or more array levels deep. ++ Fix uncaught TypeError when magic property has the same name as a property. (#1141) ++ Make AlwaysReturnPlugin warn about functions/methods with real nullable return types failing to return a value. ++ Change the behavior of the `-d` flag, make it change the current working directory to the provided directory. ++ Properly set the real param type and return types of internal functions, in rare cases where that exists. ++ Support analyzing the rare case of namespaced internal global functions (e.g. `\ast\parse_code($code, $version)`) ++ Improve analysis of shorthand ternary operator: Remove false/null from cond_expr in `(cond_expr) ?: (false_expr)` (#1186) + +24 Sep 2017, Phan 0.10.0 +------------------------ + +New Features(Analysis) ++ Check types of dimensions when using array access syntax (#406, #1093) + (E.g. for an `array`, check that the array dimension can cast to `int|string`) + +New Features (CLI, Configs) ++ Add option `ignore_undeclared_functions_with_known_signatures` which can be set to `false` + to always warn about global functions Phan has signatures for + but are unavailable in the current PHP process (and enabled extensions, and the project being analyzed) (#1080) + The default was/is to not warn, to reduce false positives. ++ Add CLI flag `--use-fallback-parser` (Experimental). If this flags is provided, then when Phan analyzes a syntactically invalid file, + it will try again with a parser which tolerates a few types of errors, and analyze the statements that could be parsed. + Useful in combination with daemon mode. ++ Add `phpdoc_type_mapping` config setting. + Projects can override this to make Phan ignore or substitute non-standard phpdoc2 types and common typos (#294) + (E.g. `'phpdoc_type_mapping' => ['the' => '', 'unknown_type' => '', 'number' => 'int|float']`) + +Maintenance ++ Increased minimum `ext-ast` version constraint to 0.1.5, switched to AST version 50. ++ Update links to project from github.com/etsy/phan to github.com/phan/phan. ++ Use the native `spl_object_id` function if it is available for the union type implementation. + This will make phan 10% faster in PHP 7.2. + (for PHP 7.1, https://github.com/runkit7/runkit_object_id 1.1.0+ also provides a native implementation of `spl_object_id`) ++ Reduce memory usage by around 5% by tracking only the file and lines associated with variables, instead of a full Context object. + + +Plugins ++ Increased minimum `ext-ast` version constraint to 0.1.5, switched to AST version 50. + Third party plugins will need to create a different version, Decls were changed into regular Nodes ++ Implement `AnalyzePropertyCapability` and `FinalizeProcessCapability`. + Make `UnusedSuppressionPlugin` start using `AnalyzePropertyCapability` and `FinalizeProcessCapability`. + Fix bug where `UnusedSuppressionPlugin` could run before the suppressed issues would be emitted, + making it falsely emit that suppressions were unused. + +Bug Fixes ++ Fix a few incorrect property names for Phan's signatures of internal classes (#1085) ++ Fix bugs in lookup of relative and non-fully qualified class and function names (#1097) ++ Fix a bug affecting analysis of code when `simplify_ast` is true. ++ Fix uncaught NodeException when analyzing complex variables as references (#1116), + e.g. `function_expecting_reference($$x)`. + +15 Aug 2017, Phan 0.9.4 +----------------------- + +New Features (Analysis) ++ Check (the first 5) elements of returned arrays against the declared return union types, individually (Issue #935) + (E.g. `/** @return int[] */ function foo() {return [2, "x"]; }` will now warn with `PhanTypeMismatchReturn` about returning `string[]`) ++ Check both sides of ternary conditionals against the declared return union types + (E.g. `function foo($x) : int {return is_string($x) ? $x : 0; }` will now warn with `PhanTypeMismatchReturn` + about returning a string). ++ Improved analysis of negations of conditions within ternary conditional operators and else/else if statements. (Issue #538) + Support analysis of negation of the `||` operator. (E.g. `if (!(is_string($x) || is_int($x))) {...}`) ++ Make phan aware of blocks of code which will unconditionally throw or return. (Issue #308, #817, #996, #956) + + Don't infer variable types from blocks of code which unconditionally throw or return. + + Infer the negation of type assertions from if statements that unconditionally throw/return/break/continue. + (E.g. `if (!is_string($x)) { return false; } functionUsingX($x);`) + + When checking if a variable is defined by all branches of an if statement, ignore branches which unconditionally throw/return/break/continue. ++ To reduce the false positives from analysis of the negation of type assertions, + normalize nullable/boolean union types after analyzing code branches (E.g. if/else) affecting the types of those variables. + (e.g. convert "bool|false|null" to "?bool") ++ Add a new plugin file `AlwaysReturnPlugin`. (Issue #996) + This will add a stricter check that a function with a non-null return type *unconditionally* returns a value (or explicitly throws, or exit()s). + Currently, Phan just checks if a function *may* return, or unconditionally throws. ++ Add a new plugin file `UnreachableCodePlugin` (in development). + This will warn about statements that appear to be unreachable + (statements occurring after unconditional return/break/throw/return/exit statements) + +New Features (CLI, Configs) ++ Add config setting `prefer_narrowed_phpdoc_return_type` (See "New Features (CLI, Configs)), + which will use only the phpdoc return types for inferences, if they're narrowed. + This config is enabled by default, and requires `check_docblock_signature_return_type_match` to be enabled. + +Bug Fixes ++ Work around notice about COMPILER_HALT_OFFSET on Windows. ++ Fixes #462 : Fix type inferences for instanceof for checks with dynamic class names are provided. + Valid class names are either a string or an instance of the class to check against. + Warn if the class name is definitely invalid. ++ Fix false positives about undefined variables in isset()/empty() (Issue #1039) + (Fixes bug introduced in Phan 0.9.3) ++ Fix false positive warnings about accessing protected methods from traits (Issue #1033) + Act as though the class which used a trait is the place where the method was defined, + so that method visibility checks work properly. + Additionally, fix false positive warnings about visibility of method aliases from traits. ++ Warn about instantiation of class with inaccessible constructor (Issue #1043) ++ Fix rare uncaught exceptions (Various) ++ Make issues and plugin issues on properties consistently use suppressions from the plugin doc comment. + +Changes In Emitted Issues ++ Improve `InvalidVariableIssetPlugin`. Change the names and messages for issue types. + Emit `PhanPluginUndeclaredVariableInIsset` and `PhanPluginComplexVariableIsset` + instead of `PhanUndeclaredVariable`. + Stop erroneously warning about valid property fetches and checks of fields of superglobals. + +0.9.3 Jul 11, 2017 +------------------ + +New Features (Analysis) ++ Automatically inherit `@param` and `@return` types from parent methods. + This is controlled by the boolean config `inherit_phpdoc_types`, which is true by default. + `analyze_signature_compatibility` must also be set to true (default is true) for this step to be performed. ++ Better analysis of calls to parent::__construct(). (Issue #852) ++ Warn with `PhanAccessOwnConstructor` if directly invoking self::__construct or static::__construct in some cases (partial). ++ Start analyzing the inside of for/while loops using the loop's condition (Issue #859) + (Inferences may leak to outside of those loops. `do{} while(cond)` is not specially analyzed yet) ++ Improve analysis of types in expressions within compound conditions (Issue #847) + (E.g. `if (is_array($x) && fn_expecting_array($x)) {...}`) ++ Evaluate the third part of a for loop with the context after the inner body is evaluated (Issue #477) ++ Emit `PhanUndeclaredVariableDim` if adding an array field to an undeclared variable. (Issue #841) + Better analyze `list($var['field']) = values` ++ Improve accuracy of `PhanTypeMismatchDeclaredReturn` (Move the check to after parse phase is finished) ++ Enable `check_docblock_signature_return_type_match` and `check_docblock_signature_param_type_match` by default. + Improve performance of those checks. + Switch to checking individual types (of the union type) of the phpdoc types and emitting issues for each invalid part. ++ Create `PhanTypeMismatchDeclaredParam` (Move the check to after parse phase is finished) + Also add config setting `prefer_narrowed_phpdoc_param_type` (See "New Features (CLI, Configs)) + This config is enabled by default. + + Also create `PhanTypeMismatchDeclaredParamNullable` when params such as `function foo(string $x = null)` + are documented as the narrowed forms `@param null $x` or `@param string $x`. + Those should be changed to either `string|null` or `?string`. ++ Detect undeclared return types at point of declaration, and emit `PhanUndeclaredTypeReturnType` (Issue #835) ++ Create `PhanParamSignaturePHPDocMismatch*` issue types, for mismatches between `@method` and real signature/other `@method` tag. ++ Create `PhanAccessWrongInheritanceCategory*` issue types to warn about classes extending a trait/interface instead of class, etc. (#873) ++ Create `PhanExtendsFinalClass*` issue types to warn about classes extending from final classes. ++ Create `PhanAccessOverridesFinalMethod*` issue types to warn about methods overriding final methods. ++ Create `PhanTypeMagicVoidWithReturn` to warn if `void` methods such as `__construct`, `__set`, etc return a value that would be ignored. (Issue #913) ++ Add check for `PhanTypeMissingReturn` within closures. Properly emit `PhanTypeMissingReturn` in functions/methods containing closures. (Issue #599) ++ Improved checking for `PhanUndeclaredVariable` in array keys and conditional conditions. (Issue #912) ++ Improved warnings and inferences about internal function references for functions such as `sort`, `preg_match` (Issue #871, #958) + Phan is now aware of many internal functions which normally ignore the original values of references passed in (E.g. `preg_match`) ++ Properly when code attempts to access static/non-static properties as if they were non-static/static. (Issue #936) ++ Create `PhanCommentOverrideOnNonOverrideMethod` and `PhanCommentOverrideOnNonOverrideConstant`. (Issue #926) + These issue types will be emitted if `@override` is part of doc comment of a method or class constant which doesn't override or implement anything. + (`@Override` and `@phan-override` can also be used as aliases of `@override`. `@override` is not currently part of any phpdoc standard.) ++ Add `@phan-closure-scope`, which can be used to annotate closure definitions with the namespaced class it will be bound to (Issue #309, #590, #790) + (E.g. if the intent was that Closure->bindTo or Closure->bind would be called to bind it to `\MyNS\MyClass` (or an instance of that class), + then a closure could be declared as `/** @phan-closure-scope \MyNS\MyClass */ function() { $this->somePrivateMyClassMethod(); }` ++ Add `Closure` as a first class type, (Previously, closures were treated as `callable` in some places) (Issue #978) + +New Features (CLI, Configs) ++ Create `check_docblock_signature_param_type_match` (similar to `check_docblock_signature_return_type_match`) config setting + to enable warning if phpdoc types are incompatible with the real types. True(enabled) by default. + + Create `prefer_narrowed_phpdoc_param_type` config setting (True by default, requires `check_docblock_signature_return_type_match` to be enabled). + When it is true, Phan will analyze each function using the phpdoc param types instead of the provided signature types + if the possible phpdoc types are narrower and compatible with the signature. + (E.g. indicate that subclasses are expected over base classes, indicate that non-nullable is expected instead of nullable) + This affects analysis both inside and outside the method. + + Aside: Phan currently defaults to preferring phpdoc type over real return type, and emits `PhanTypeMismatchDeclaredReturn` if the two are incompatible. ++ Create `enable_class_alias_support` config setting (disabled by default), which enables analyzing basic usage of class_alias. (Issue #586) + Set it to true to enable it. + NOTE: this is still experimental. ++ Warn to stderr about running Phan analysis with Xdebug (Issue #116) + The warning can be disabled by the Phan config setting `skip_slow_php_options_warning` to true. ++ Add a config setting 'scalar_implicit_partial' to allow moving away from 'scalar_implicit_cast' (Issue #541) + This allows users to list out (and gradually remove) permitted scalar type casts. ++ Add `null_casts_as_array` and `array_casts_as_null` settings, which can be used while migrating away from `null_casts_as_any_type`. + These will be checked if one of the types has a union type of `null`, as well as when checking if a nullable array can cast to a regular array. + +Plugins + ++ Redesign plugin system to be more efficient. (Issue #600) + New plugins should extend `\Phan\PluginV2` and implement the interfaces for capabilities they need to have, + such as `\Phan\PluginV2\AnalyzeClassCapability`. + In the new plugin system, plugins will only be run when they need to (Phan no longer needs to invoke an empty method body). + Old subclasses of `\Phan\Plugin\PluginImplementation` will continue to work, but will be less efficient. + +Maintenance ++ Reduce memory usage by around 15% by using a more efficient representation of union types (PR #729). + The optional extension https://github.com/runkit7/runkit_object_id can be installed to boost performance by around 10%. ++ Check method signatures compatibility against all overridden methods (e.g. interfaces with the same methods), not just the first ones (Issue #925) + +Bug Fixes ++ Work around known bugs in current releases of two PECL extensions (Issue #888, #889) ++ Fix typo - Change `PhanParamSignatureRealMismatch` to `PhanParamSignatureRealMismatchReturnType` ++ Consistently exit with non-zero exit code if there are multiple processes, and any process failed to return valid results. (Issue #868) ++ Fixes #986 : PhanUndeclaredVariable used to fail to be emitted in some deeply nested expressions, such as `return $undefVar . 'suffix';` ++ Make Phan infer the return types of closures, both for closures invoked inline and closures declared then invoked later (Issue #564) ++ Phan now correctly analyze global functions for mismatches of phpdoc types and real parameter types. + Previously, it wouldn't emit warnings for global functions, only for methods. ++ Don't add `mixed` to inferred union types of properties which already have non-empty phpdoc types. (Issue #512) + mixed would just result in Phan failing to emit any types of issues. ++ When `simplify_ast` is true, simplify the ASTs parsed in the parse mode as well. + Makes analysis consistent when `quick_mode` is false (AST nodes from the parse phase would also be used in the analysis phase) ++ Don't emit PhanTypeNonVarPassByRef on arguments that are function/method calls returning references. (Issue #236) ++ Emit PhanContextNotObject more reliably when not in class scope. + +Backwards Incompatible Changes ++ Fix categories of some issue types, renumber error ids for the pylint error formatter to be unique and consistent. + +0.9.2 Jun 13, 2017 +------------------ + +New Features (Analysis) ++ Add `PhanParamSignatureRealMismatch*` (e.g. `ParamSignatureRealMismatchTooManyRequiredParameters`), + which ignores phpdoc types and imitates PHP's inheritance warning/error checks as closely as possible. (Issue #374) + This has a much lower rate of false positives than `PhanParamSignatureMismatch`, which is based on Liskov Substitution Principle and also accounts for phpdoc types. + (`PhanParamSignatureMismatch` continues to exist) ++ Create `PhanUndeclaredStaticProperty` (Issue #610) + This is of higher severity than PhanUndeclaredProperty, because PHP 7 throws an Error. + Also add `PhanAccessPropertyStaticAsNonStatic` ++ Supports magic instance/static `@method` annotations. (Issue #467) + This is enabled by default. ++ Change the behavior of non-quick recursion (Affects emitted issues in large projects). + Improve performance of non-quick analysis by checking for redundant analysis steps + (E.g. calls from two different places passing the same union types for each parameter), + continuing to recurse when passing by reference. ++ Support for checking for misuses of "@internal" annotations. Phan assumes this means it is internal to a namespace. (Issue #353) + This checks properties, methods, class constants, and classes. + (Adds `PhanAccessConstantInternal`, `PhanAccessClassInternal`, `PhanAccessClassConstantInternal`, `PhanAccessPropertyInternal`, `PhanAccessMethodInternal`) + (The implementation may change) ++ Make conditionals such as `is_string` start applying to the condition in ternary operators (`$a ? $b : $c`) ++ Treat `resource`, `object`, and `mixed` as native types only when they occur in phpdoc. + Outside of phpdoc (e.g. `$x instanceof resource`), analyze those names as if they were class names. ++ Emit low severity issues if Phan can't extract types from phpdoc, + the phpdoc `@param` is out of sync with the code, + or if the phpdoc annotation doesn't apply to an element type (Issue #778) ++ Allow inferring the type of variables from `===` conditionals such as `if ($x === true)` ++ Add issue type for non-abstract classes containing abstract methods from itself or its ancestors + (`PhanClassContainsAbstractMethod`, `PhanClassContainsAbstractMethodInternal`) ++ Partial support for handling trait adaptations (`as`/`insteadof`) when using traits (Issue #312) ++ Start checking if uses of private/protected class methods *defined in a trait* are visible outside of that class. + Before, Phan would always assume they were visible, to reduce false positives. ++ If Phan has inferred/been provided generic array types for a variable (e.g. `int[]`), + then analysis of the code within `if (is_array($x))` will act as though the type is `int[]`. + The checks `is_object` and `is_scalar` now also preserve known sub-types of the group of types. + (If Phan isn't aware of any sub-types, it will infer the generic version, e.g. `object`) ++ Start checking if unanalyzable variable accesses such as `$$x` are very likely to be invalid or typos (e.g. $x is an object or array or null) + Emit `PhanTypeSuspiciousIndirectVariable` if those are seen. (PR #809) ++ Add partial support for inferring the union types of the results of expressions such as `$x ^= 5` (e.g. in `foo($x ^= 5)`) (PR #809) ++ Thoroughly analyze the methods declared within traits, + using only the information available within the trait. (Issue #800, PR #815) + If new emitted issues are seen, users can (1) add abstract methods to traits, (2) add `@method` annotations, or (3) add `@suppress` annotations. + +New Features (CLI, Configs) ++ (Linux/Unix only) Add Experimental Phan Daemon mode (PR #563 for Issue #22), which allows phan to run in the background, and accept TCP requests to analyze single files. + (The implementation currently requires the `pcntl` extension, which does not in Windows) + Server usage: `path/to/phan --daemonize-tcp-port 4846` (In the root directory of the project being analyzed) + Client usage: `path/to/phan_client --daemonize-tcp-port 4846 -l src/file1.php [ -l src/file2.php ]` ++ Add `--color` CLI flag, with rudimentary unix terminal coloring for the plain text output formatter. (Issue #363) + Color schemes are customizable with `color_scheme`, in the config file. ++ Add the `exclude_file_regex` config to exclude file paths based on a regular expression (e.g. tests or example files mixed with the codebase) (#635) + The regular expression is run against the relative path within the project. ++ Add `--dump-parsed-file-list` option to print files which Phan would parse. ++ Add experimental `simplify_ast` config, to simplify the AST into a form which improves Phan's type inference. + (E.g. handles some variable declarations within `if ()` statements. + Infers that $x is a string for constructs such as `if (!is_string($x)) {return;} function_using_x($x);`) + This is slow, and disabled by default. ++ Add `--include-analysis-file-list` option to define files that will be included in static analysis, to the exclusion of others. ++ Start emitting `PhanDeprecatedFunctionInternal` if an internal (to PHP) function/method is deprecated. + (Phan emits `PhanUndeclaredFunction` if a function/method was removed; Functions deprecated in PHP 5.x were removed in 7.0) + +Maintenance ++ Update function signature map to analyze `iterable` and `is_iterable` from php 7.1 ++ Improve type inferences on functions with nullable default values. ++ Update miscellaneous new functions in php 7.1 standard library (e.g. `getenv`) + +Bug Fixes +- Fix PhanTypeMismatchArgument, etc. for uses of `new static()`, static::CONST, etc in a method. (Issue #632) +- Fix uncaught exception when conditional node is a scalar (Issue #613) +- Existence of __get() no longer affects analyzing static properties. (Issue #610) +- Phan can now detect the declaration of constants relative to a `use`d namespace (Issue #509) +- Phan can now detect the declaration of functions relative to a `use`d namespace (Issue #510) +- Fix a bug where the JSON output printer accidentally escaped some output ("<"), causing invalid JSON. +- Fix a bug where a print/echo/method call erroneously marked methods/functions as having a return value. (Issue #811) +- Improve analysis of SimpleXMLElement (Issues #542, #539) +- Fix crash handling trait use aliases which change only the method's visibility (Issue #861) + +Backwards Incompatible Changes +- Declarations of user-defined constants are now consistently + analyzed in a case-sensitive way. + This may affect projects using `define(name, value, case_insensitive = true)`. + Change the code being analyzed to exactly match the constant name in define()) + +0.9.1 Mar 15, 2017 +------------------ + +New Features (Analysis) ++ Conditions in `if(cond(A) && expr(A))` (e.g. `instanceof`, `is_string`, etc) now affect analysis of right-hand side of `&&` (PR #540) ++ Add `PhanDeprecatedInterface` and `PhanDeprecatedTrait`, similar to `PhanDeprecatedClass` ++ Supports magic `@property` annotations, with aliases `@property-read` and @property-write`. (Issue #386) + This is enabled by default. + + Phan also supports the `@phan-forbid-undeclared-magic-properties` annotation, + which will make it warn about undeclared properties if no real property or `@property` annotation exists. + +New Features (CLI, Configs) ++ Add `--version` CLI flag ++ Move some rare CLI options from `--help` into `--extended-help` + +Maintenance ++ Improved stability of analyzing phpdoc and real nullable types (Issue #567) ++ Fix type signatures Phan has for some internal methods. ++ Improve CLI `--progress-bar` tracking by printing 0% immediately. ++ Add Developer Certificate of Origin + +Bug Fixes ++ Fix uncaught issue exception analyzing class constants (Issue #551) ++ Fix group use in ASTs ++ Fix false positives checking if native types can cast to/from nullable native types (Issue #567, #582) ++ Exit with non-zero exit code if an invalid CLI argument is passed to Phan + +Backwards Incompatible Changes ++ Change the way that parameter's default values affect type inferences. + (May now add to the union type or ignore default values. Used to always add the default value types) + Add `@param` types if you encounter new issues. + This was done to avoid false positives in cases such as `function foo($maybeArray = false)` ++ Increase minimum `ext-ast` version constraint to 0.1.4 + +0.9.0 Feb 21, 2017 +------------------ + +The 0.9.x versions will be tracking syntax from PHP versions 7.1.x and is runnable on PHP 7.1+. +Please use version 0.8.x if you're using a version of PHP < 7.1. + +New Features (Analysis) ++ Support php 7.1 class constant visibility ++ Support variadic phpdoc in `@param`, e.g. `@param string ...$args` + Avoid ambiguity by emitting `PhanTypeMismatchVariadicComment` and `PhanTypeMismatchVariadicParam`. ++ Initial support for php 7.1 nullable types and void, both in phpdoc and real parameters. ++ Initial support for php 7.1 `iterable` type ++ Both conditions from `if(cond(A) && cond(B))` (e.g. `instanceof`, `is_string`, etc.) now affect analysis of the if element's block (PR #540) ++ Apply conditionals such as `is_string` to type guards in ternary operators (Issue #465) ++ Allow certain checks for removing null from Phan's inferred types, reducing false positives (E.g. `if(!is_null($x) && $x->method())`) (#518) ++ Incomplete support for specifying the class scope in which a closure will be used/bound (#309) ++ Support `@return self` in class context + +New Features (CLI, Configs) ++ Introduce `check_docblock_signature_return_type_match` config (slow, disabled by default) + (Checks if the phpdoc types match up with declared return types) + +Maintenance ++ Add Code of Conduct ++ Fix type signatures for some internal methods and internal class properties. + +Bug Fixes ++ Allow asserting `object` is a specific object type without warning (Issue #516) ++ Fix bugs in analysis of varargs within a function(Issue #516) ++ Treat null defaults in functions and methods the same way (Issue #508) + In both, add null defaults to the UnionType only if there's already another type. + In both, add non-null defaults to the UnionType (Contains `mixed` if there weren't any explicit types) ++ Specially handle phpdoc type aliases such as `boolean` only in phpdoc (Issue #471) + (Outside of phpdoc, it refers to a class with the name `boolean`) ++ Add some internal classes other than `stdClass` which are allowed to have dynamic, undeclared properties (Issue #433) ++ Fix assertion errors when passing references by reference (Issue #500) + +Backwards Incompatible Changes ++ Requires newer `ext-ast` version (Must support version 35). + +0.8.3 Jan 26, 2017 +------------------ + +The 0.8.x versions will be tracking syntax from PHP versions 7.0.x and is runnable on PHP 7.0+. +Please use version 0.8.x if you're using a version of PHP < 7.1. +For best results, run version 0.8.x with PHP 7.0 if you are analyzing a codebase which normally runs on php <= 7.0 +(If php 7.1 is used, Phan will think that some new classes, methods, and functions exist or have different parameter lists because it gets this info from `Reflection`) + +??? diff --git a/bundled-libs/phan/phan/README.md b/bundled-libs/phan/phan/README.md new file mode 100644 index 000000000..bad9eea43 --- /dev/null +++ b/bundled-libs/phan/phan/README.md @@ -0,0 +1,294 @@ +Phan is a static analyzer for PHP that prefers to minimize false-positives. Phan attempts to prove incorrectness rather than correctness. + +Phan looks for common issues and will verify type compatibility on various operations when type +information is available or can be deduced. Phan has a good (but not comprehensive) understanding of flow control +and can track values in a few use cases (e.g. arrays, integers, and strings). + +[![Build Status](https://dev.azure.com/tysonandre775/phan/_apis/build/status/phan.phan?branchName=master)](https://dev.azure.com/tysonandre775/phan/_build/latest?definitionId=3&branchName=master) +[![Build Status (Windows)](https://ci.appveyor.com/api/projects/status/github/phan/phan?branch=master&svg=true)](https://ci.appveyor.com/project/TysonAndre/phan/branch/master) +[![Gitter](https://badges.gitter.im/phan/phan.svg)](https://gitter.im/phan/phan?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![Latest Stable Version](https://img.shields.io/packagist/v/phan/phan.svg)](https://packagist.org/packages/phan/phan) +[![License](https://img.shields.io/packagist/l/phan/phan.svg)](https://github.com/phan/phan/blob/master/LICENSE) + +# Getting Started + +The easiest way to use Phan is via Composer. + +``` +composer require phan/phan +``` + +With Phan installed, you'll want to [create a `.phan/config.php` file](https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file) in +your project to tell Phan how to analyze your source code. Once configured, you can run it via `./vendor/bin/phan`. + +Phan depends on PHP 7.2+ with the [php-ast](https://github.com/nikic/php-ast) extension (1.0.10+ is preferred) and supports analyzing PHP version 7.0-7.4 syntax. +Installation instructions for php-ast can be found [here](https://github.com/nikic/php-ast#installation). +(Phan can be used without php-ast by using the CLI option `--allow-polyfill-parser`, but there are slight differences in the parsing of doc comments) + +* **Alternative Installation Methods**
+ See [Getting Started](https://github.com/phan/phan/wiki/Getting-Started) for alternative methods of using +Phan and details on how to configure Phan for your project.
+* **Incrementally Strengthening Analysis**
+ Take a look at [Incrementally Strengthening Analysis](https://github.com/phan/phan/wiki/Incrementally-Strengthening-Analysis) for some tips on how to slowly ramp up the strictness of the analysis as your code becomes better equipped to be analyzed.
+* **Installing Dependencies**
+ Take a look at [Installing Phan Dependencies](https://github.com/phan/phan/wiki/Getting-Started#installing-phan-dependencies) for help getting Phan's dependencies installed on your system. + +The [Wiki has more information about using Phan](https://github.com/phan/phan/wiki#using-phan). + +# Features + +Phan is able to perform the following kinds of analysis: + +* Check that all methods, functions, classes, traits, interfaces, constants, properties and variables are defined and accessible. +* Check for type safety and arity issues on method/function/closure calls. +* Check for PHP7/PHP5 backward compatibility. +* Check for features that weren't supported in older PHP 7.x minor releases (E.g. `object`, `void`, `iterable`, `?T`, `[$x] = ...;`, negative string offsets, multiple exception catches, etc.) +* Check for sanity with array accesses. +* Check for type safety on binary operations. +* Check for valid and type safe return values on methods, functions, and closures. +* Check for No-Ops on arrays, closures, constants, properties, variables, unary operators, and binary operators. +* Check for unused/dead/[unreachable](https://github.com/phan/phan/tree/master/.phan/plugins#unreachablecodepluginphp) code. (Pass in `--dead-code-detection`) +* Check for unused variables and parameters. (Pass in `--unused-variable-detection`) +* Check for redundant or impossible conditions and pointless casts. (Pass in `--redundant-condition-detection`) +* Check for unused `use` statements. + These and a few other issue types can be automatically fixed with `--automatic-fix`. +* Check for classes, functions and methods being redefined. +* Check for sanity with class inheritance (e.g. checks method signature compatibility). + Phan also checks for final classes/methods being overridden, that abstract methods are implemented, and that the implemented interface is really an interface (and so on). +* Supports namespaces, traits and variadics. +* Supports [Union Types](https://github.com/phan/phan/wiki/About-Union-Types). +* Supports [Generic Types (i.e. `@template`)](https://github.com/phan/phan/wiki/Generic-Types). +* Supports generic arrays such as `int[]`, `UserObject[]`, `array`, etc.. +* Supports array shapes such as `array{key:string,otherKey:?stdClass}`, etc. (internally and in PHPDoc tags) + This also supports indicating that fields of an array shape are optional + via `array{requiredKey:string,optionalKey?:string}` (useful for `@param`) +* Supports phpdoc [type annotations](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code). +* Supports inheriting phpdoc type annotations. +* Supports checking that phpdoc type annotations are a narrowed form (E.g. subclasses/subtypes) of the real type signatures +* Supports inferring types from [assert() statements](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code) and conditionals in if elements/loops. +* Supports [`@deprecated` annotation](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#deprecated) for deprecating classes, methods and functions +* Supports [`@internal` annotation](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#internal) for elements (such as a constant, function, class, class constant, property or method) as internal to the package in which it's defined. +* Supports `@suppress ` annotations for [suppressing issues](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#suppress). +* Supports [magic @property annotations](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#property) (`@property `) +* Supports [magic @method annotations](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#method) (`@method ( )`) +* Supports [`class_alias` annotations (experimental, off by default)](https://github.com/phan/phan/pull/586) +* Supports indicating the class to which a closure will be bound, via `@phan-closure-scope` ([example](tests/files/src/0264_closure_override_context.php)) +* Supports analysis of closures and return types passed to `array_map`, `array_filter`, and other internal array functions. +* Offers extensive configuration for weakening the analysis to make it useful on large sloppy code bases +* Can be run on many cores. (requires `pcntl`) +* Output is emitted in text, checkstyle, json, pylint, csv, or codeclimate formats. +* Can run [user plugins on source for checks specific to your code](https://github.com/phan/phan/wiki/Writing-Plugins-for-Phan). + [Phan includes various plugins you may wish to enable for your project](https://github.com/phan/phan/tree/master/.phan/plugins#2-general-use-plugins). + +See [Phan Issue Types](https://github.com/phan/phan/wiki/Issue-Types-Caught-by-Phan) for descriptions +and examples of all issues that can be detected by Phan. Take a look at the +[\Phan\Issue](https://github.com/phan/phan/blob/master/src/Phan/Issue.php) to see the +definition of each error type. + +Take a look at the [Tutorial for Analyzing a Large Sloppy Code Base](https://github.com/phan/phan/wiki/Tutorial-for-Analyzing-a-Large-Sloppy-Code-Base) to get a sense of what the process of doing ongoing analysis might look like for you. + +Phan can be used from [various editors and IDEs](https://github.com/phan/phan/wiki/Editor-Support) for its error checking, "go to definition" support, etc. via the [Language Server Protocol](https://github.com/Microsoft/language-server-protocol). +Editors and tools can also request analysis of individual files in a project using the simpler [Daemon Mode](https://github.com/phan/phan/wiki/Using-Phan-Daemon-Mode). + +See the [tests](https://github.com/phan/phan/blob/master/tests/files) directory for some examples of the various checks. + +Phan is imperfect and shouldn't be used to prove that your PHP-based rocket guidance system is free of defects. + +## Features provided by plugins + +Additional analysis features have been provided by [plugins](https://github.com/phan/phan/tree/master/.phan/plugins#plugins). + +- [Checking for syntactically unreachable statements](https://github.com/phan/phan/tree/master/.phan/plugins#unreachablecodepluginphp) (E.g. `{ throw new Exception("Message"); return $value; }`) +- [Checking `*printf()` format strings against the provided arguments](https://github.com/phan/phan/tree/master/.phan/plugins#printfcheckerplugin) (as well as checking for common errors) +- [Checking that PCRE regexes passed to `preg_*()` are valid](https://github.com/phan/phan/tree/master/.phan/plugins#pregregexcheckerplugin) +- [Checking for `@suppress` annotations that are no longer needed.](https://github.com/phan/phan/tree/master/.phan/plugins#unusedsuppressionpluginphp) +- [Checking for duplicate or missing array keys.](https://github.com/phan/phan/tree/master/.phan/plugins#duplicatearraykeypluginphp) +- [Checking coding style conventions](https://github.com/phan/phan/tree/master/.phan/plugins#3-plugins-specific-to-code-styles) +- [Others](https://github.com/phan/phan/tree/master/.phan/plugins#plugins) + +Example: [Phan's plugins for self-analysis.](https://github.com/phan/phan/blob/3.2.8/.phan/config.php#L601-L674) + +# Usage + +After [installing Phan](#getting-started), Phan needs to be configured with details on where to find code to analyze and how to analyze it. The +easiest way to tell Phan where to find source code is to [create a `.phan/config.php` file](https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file). +A simple `.phan/config.php` file might look something like the following. + +```php + null, + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in exclude_analysis_directory_list, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'src', + 'vendor/symfony/console', + ], + + // A directory list that defines files that will be excluded + // from static analysis, but whose class and method + // information should be included. + // + // Generally, you'll want to include the directories for + // third-party code (such as "vendor/") in this list. + // + // n.b.: If you'd like to parse but not analyze 3rd + // party code, directories containing that code + // should be added to the `directory_list` as + // to `exclude_analysis_directory_list`. + "exclude_analysis_directory_list" => [ + 'vendor/' + ], + + // A list of plugin files to execute. + // Plugins which are bundled with Phan can be added here by providing their name + // (e.g. 'AlwaysReturnPlugin') + // + // Documentation about available bundled plugins can be found + // at https://github.com/phan/phan/tree/master/.phan/plugins + // + // Alternately, you can pass in the full path to a PHP file + // with the plugin's implementation (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php') + 'plugins' => [ + // checks if a function, closure or method unconditionally returns. + // can also be written as 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php' + 'AlwaysReturnPlugin', + 'DollarDollarPlugin', + 'DuplicateArrayKeyPlugin', + 'DuplicateExpressionPlugin', + 'PregRegexCheckerPlugin', + 'PrintfCheckerPlugin', + 'SleepCheckerPlugin', + // Checks for syntactically unreachable statements in + // the global scope or function bodies. + 'UnreachableCodePlugin', + 'UseReturnValuePlugin', + 'EmptyStatementListPlugin', + 'LoopVariableReusePlugin', + ], +]; +``` + +Take a look at [Creating a Config File](https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file) and +[Incrementally Strengthening Analysis](https://github.com/phan/phan/wiki/Incrementally-Strengthening-Analysis) for +more details. + +Running `phan --help` will show [usage information and command-line options](./internal/CLI-HELP.md). + +## Annotating Your Source Code + +Phan reads and understands most [PHPDoc](http://www.phpdoc.org/docs/latest/guides/types.html) +type annotations including [Union Types](https://github.com/phan/phan/wiki/About-Union-Types) +(like `int|MyClass|string|null`) and generic array types (like `int[]` or `string[]|MyClass[]` or `array`). + +Take a look at [Annotating Your Source Code](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code) +and [About Union Types](https://github.com/phan/phan/wiki/About-Union-Types) for some help +getting started with defining types in your code. + +Phan supports `(int|string)[]` style annotations, and represents them internally as `int[]|string[]` +(Both annotations are treated like array which may have integers and/or strings). +When you have arrays of mixed types, just use `array`. + +The following code shows off the various annotations that are supported. + +```php +/** + * @return void + */ +function f() {} + +/** @deprecated */ +class C { + /** @var int */ + const C = 42; + + /** @var string[]|null */ + public $p = null; + + /** + * @param int|null $p + * @return string[]|null + */ + public static function f($p) { + if (is_null($p)) { + return null; + } + + return array_map( + /** @param int $i */ + function($i) { + return "thing $i"; + }, + range(0, $p) + ); + } +} +``` + +Just like in PHP, any type can be nulled in the function declaration which also +means a null is allowed to be passed in for that parameter. + +Phan checks the type of every single element of arrays (Including keys and values). +In practical terms, this means that `[$int1=>$int2,$int3=>$int4,$int5=>$str6]` is seen as `array`, +which Phan represents as `array|array`. +`[$strKey => new MyClass(), $strKey2 => $unknown]` will be represented as +`array|array`. + +- Literals such as `[12,'myString']` will be represented internally as array shapes such as `array{0:12,1:'myString'}` + +# Generating a file list + +This static analyzer does not track includes or try to figure out autoloader magic. It treats +all the files you throw at it as one big application. For code encapsulated in classes this +works well. For code running in the global scope it gets a bit tricky because order +matters. If you have an `index.php` including a file that sets a bunch of global variables and +you then try to access those after the `include(...)` in `index.php` the static analyzer won't +know anything about these. + +In practical terms this simply means that you should put your entry points and any files +setting things in the global scope at the top of your file list. If you have a `config.php` +that sets global variables that everything else needs, then you should put that first in the list followed by your +various entry points, then all your library files containing your classes. + +# Development + +Take a look at [Developer's Guide to Phan](https://github.com/phan/phan/wiki/Developer's-Guide-To-Phan) for help getting started hacking on Phan. + +When you find an issue, please take the time to create a tiny reproducing code snippet that illustrates +the bug. And once you have done that, fix it. Then turn your code snippet into a test and add it to +[tests](tests) then `./test` and send a PR with your fix and test. Alternatively, you can open an Issue with +details. + +To run Phan's unit tests, just run `./test`. + +To run all of Phan's unit tests and integration tests, run `./tests/run_all_tests.sh` + +# Code of Conduct + +We are committed to fostering a welcoming community. Any participant and +contributor is required to adhere to our [Code of Conduct](./CODE_OF_CONDUCT.md). + +# Online Demo + +**This requires an up to date version of Firefox/Chrome and at least 4 GB of free RAM.** (this is a 10 MB download) + +[Run Phan entirely in your browser](https://phan.github.io/demo/). + +[![Preview of analyzing PHP](https://raw.githubusercontent.com/phan/demo/master/static/preview.png)](https://phan.github.io/demo/) diff --git a/bundled-libs/phan/phan/azure-pipelines.yml b/bundled-libs/phan/phan/azure-pipelines.yml new file mode 100644 index 000000000..3c3856afc --- /dev/null +++ b/bundled-libs/phan/phan/azure-pipelines.yml @@ -0,0 +1,26 @@ +# https://aka.ms/yaml + +trigger: +- master + +jobs: + - template: .azure/job.yml + parameters: + configurationName: PHP_72_NTS + phpVersion: 7.2 + vmImage: 'ubuntu-16.04' + - template: .azure/job.yml + parameters: + configurationName: PHP_73_NTS + phpVersion: 7.3 + vmImage: 'ubuntu-18.04' + - template: .azure/job.yml + parameters: + configurationName: PHP_74_NTS + phpVersion: 7.4 + vmImage: 'ubuntu-20.04' + - template: .azure/job.yml + parameters: + configurationName: PHP_80_NTS + phpVersion: 8.0 + vmImage: 'ubuntu-20.04' diff --git a/bundled-libs/phan/phan/composer.json b/bundled-libs/phan/phan/composer.json new file mode 100644 index 000000000..94b7a4bac --- /dev/null +++ b/bundled-libs/phan/phan/composer.json @@ -0,0 +1,56 @@ +{ + "name": "phan/phan", + "description": "A static analyzer for PHP", + "keywords": ["php", "static", "analyzer"], + "type": "project", + "license": "MIT", + "authors": [ + { + "name": "Tyson Andre" + }, + { + "name": "Rasmus Lerdorf" + }, + { + "name": "Andrew S. Morrison" + } + ], + "config": { + "sort-packages": true, + "platform": { + "php": "7.2.24" + } + }, + "require": { + "php": "^7.2.0|^8.0.0", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "composer/semver": "^1.4|^2.0|^3.0", + "composer/xdebug-handler": "^1.3.2", + "felixfbecker/advanced-json-rpc": "^3.0.4", + "microsoft/tolerant-php-parser": "0.0.23", + "netresearch/jsonmapper": "^1.6.0|^2.0|^3.0", + "sabre/event": "^5.0.3", + "symfony/console": "^3.2|^4.0|^5.0", + "symfony/polyfill-mbstring": "^1.11.0", + "symfony/polyfill-php80": "^1.20.0" + }, + "suggest": { + "ext-ast": "Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed, 1.0.8+ is recommended.", + "ext-iconv": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", + "ext-igbinary": "Improves performance of polyfill when ext-ast is unavailable", + "ext-mbstring": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", + "ext-tokenizer": "Needed for fallback/polyfill parser support and file/line-based suppressions." + }, + "require-dev": { + "phpunit/phpunit": "^8.5.0" + }, + "autoload": { + "psr-4": {"Phan\\": "src/Phan"} + }, + "autoload-dev": { + "psr-4": {"Phan\\Tests\\": "tests/Phan"} + }, + "bin": ["phan", "phan_client", "tocheckstyle"] +} diff --git a/bundled-libs/phan/phan/phan b/bundled-libs/phan/phan/phan new file mode 100755 index 000000000..2998f4e6c --- /dev/null +++ b/bundled-libs/phan/phan/phan @@ -0,0 +1,10 @@ +#!/usr/bin/env php +verbose; + + $failure_code = 0; + $temporary_file_mapping_contents = []; + // TODO: Check that path gets defined + foreach ($opts->file_list as $path) { + if (isset($opts->temporary_file_map[$path])) { + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable + $temporary_path = $opts->temporary_file_map[$path]; + $temporary_contents = file_get_contents($temporary_path); + if ($temporary_contents === false) { + self::debugError(sprintf("Could not open temporary input file: %s", $temporary_path)); + $failure_code = 1; + continue; + } + $exit_code = 0; + $output = ''; + if (!$opts->use_fallback_parser) { + ob_start(); + try { + system("php -l --no-php-ini " . escapeshellarg($temporary_path), $exit_code); + } finally { + $output = ob_get_clean(); + } + } + if ($exit_code === 0) { + $temporary_file_mapping_contents[$path] = $temporary_contents; + } + if ($exit_code !== 0) { + echo $output; + } + } else { + // TODO: use popen instead + // TODO: add option to capture output, suppress "No syntax error"? + // --no-php-ini is a faster way to parse since php doesn't need to load multiple extensions. Assumes none of the extensions change the way php is parsed. + $exit_code = 0; + $output = ''; + if (!$opts->use_fallback_parser) { + ob_start(); + try { + system("php -l --no-php-ini " . escapeshellarg($path), $exit_code); + } finally { + $output = ob_get_clean(); + } + } + if ($exit_code !== 0) { + echo $output; + } + } + if ($exit_code !== 0) { + // The file is syntactically invalid. Or php somehow isn't able to be invoked from this script. + $failure_code = $exit_code; + } + } + // Exit if any of the requested files are syntactically invalid. + if ($failure_code !== 0) { + self::debugError("Files were syntactically invalid\n"); + exit($failure_code); + } + + if (!isset($path)) { + self::debugError("Unexpectedly parsed no files\n"); + exit($failure_code); + } + + // TODO: Check that everything in $this->file_list is in the same path. + // $path = reset($opts->file_list); + $real = realpath($path); + if (!is_string($real)) { + self::debugError("Could not resolve $path\n"); + } + // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal + $dirname = dirname($real); + $old_dirname = null; + unset($real); + + // TODO: In another PR, have an alternative way to run the daemon/server on Windows (Serialize and unserialize global state? + // The server side is unsupported on Windows, due to the `pcntl` extension not being supported. + $found_phan_config = false; + while ($dirname !== $old_dirname) { + if (file_exists($dirname . '/.phan/config.php')) { + $found_phan_config = true; + break; + } + $old_dirname = $dirname; + $dirname = dirname($dirname); + } + if (!$found_phan_config) { + self::debugInfo("Not in a Phan project, nothing to do."); + exit(0); + } + + $file_mapping = []; + $real_files = []; + foreach ($opts->file_list as $path) { + $real = realpath($path); + if (!is_string($real)) { + self::debugInfo("could not find real path to '$path'"); + continue; + } + // Convert this to a relative path + if (in_array(substr($real, 0, strlen($dirname) + 1), + [$dirname . DIRECTORY_SEPARATOR, $dirname . '/'], + true + )) { + $real = substr($real, strlen($dirname) + 1); + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable not able to analyze. Not using coalescing because this supports php 5. + $mapped_path = isset($opts->temporary_file_map[$path]) ? $opts->temporary_file_map[$path] : $path; + // If we are analyzing a temporary file, but it's within a project, then output the path to a temporary file for consistency. + // (Tools which pass something a temporary path expect a temporary path in the output.) + $file_mapping[$real] = $mapped_path; + $real_files[] = $real; + } else { + self::debugInfo("Not in a Phan project, nothing to do."); + } + } + if (count($file_mapping) === 0) { + self::debugInfo("Not in a real project"); + } + // The file is syntactically valid. Run phan. + $request = [ + 'method' => 'analyze_files', + 'files' => $real_files, + 'format' => 'json', + ]; + if ($opts->output_mode) { + $request['format'] = $opts->output_mode; + $request['is_user_specified_format'] = true; + } + if ($opts->color === null && (!$opts->output_mode || $opts->output_mode === 'text')) { + $opts->color = self::supportsColor(STDOUT); + } + if ($opts->color) { + $request['color'] = true; + } + + if (count($temporary_file_mapping_contents) > 0) { + $request['temporary_file_mapping_contents'] = $temporary_file_mapping_contents; + } + + $serialized_request = json_encode($request); + if (!is_string($serialized_request)) { + self::debugError("Could not serialize this request\n"); + exit(1); + } + + // TODO: check if the folder is within a folder with subdirectory .phan/config.php + // TODO: Check if there is a lock before attempting to connect? + $client = @stream_socket_client($opts->url, $errno, $errstr, 20.0); + if (!\is_resource($client)) { + // TODO: This should attempt to start up the phan daemon for the given folder? + self::debugError("Phan daemon not running on " . ($opts->url)); + exit(0); + } + fwrite($client, $serialized_request); + stream_set_timeout($client, (int)floor(self::TIMEOUT_MS / 1000), 1000 * (self::TIMEOUT_MS % 1000)); + stream_socket_shutdown($client, STREAM_SHUT_WR); + $response_lines = []; + while (!feof($client)) { + $response_lines[] = fgets($client); + } + stream_socket_shutdown($client, STREAM_SHUT_RD); + fclose($client); + $response_bytes = implode('', $response_lines); + // This uses the 'phplike' format imitating php's error format. "%s in %s on line %d" + $response = json_decode($response_bytes, true); + if (!is_array($response)) { + self::debugError(sprintf("Invalid response from phan for %s: expected JSON object: %s", $opts->url, $response_bytes)); + return; + } + $status = isset($response['status']) ? $response['status'] : null; + if ($status === 'ok') { + self::dumpJSONIssues($response, $file_mapping, $request); + } else { + self::debugError(sprintf("Invalid response from phan for %s: %s", $opts->url, $response_bytes)); + } + } + + /** + * @param array $response + * @param string[] $file_mapping + * @param array $request + * @return void + */ + private static function dumpJSONIssues(array $response, array $file_mapping, array $request) + { + $did_debug = false; + $lines = []; + // if ($response['issue_count'] > 0) + $issues = $response['issues']; + $format = $request['format']; + if ($format === 'json') { + if (!\is_array($issues)) { + if (\is_string($issues)) { + self::debugError(sprintf("Invalid issues response from phan: %s\n", $issues)); + } else { + self::debugError(sprintf("Invalid type for issues response from phan: %s\n", gettype($issues))); + } + return; + } + if (isset($request['is_user_specified_format'])) { + // The user requested the raw JSON, not what `phan_client` converts it to + echo json_encode($issues) . "\n"; + return; + } + } else { + // When formats other than 'json' are requested, the Phan daemon returns the issues as a raw string. + // (e.g. codeclimate returns a string with JSON separated by "\x00") + if (!\is_string($issues)) { + self::debugError(sprintf("Invalid type for issues response from phan: %s\n", gettype($issues))); + return; + } + echo $issues; + return; + } + foreach ($issues as $issue) { + if (!is_array($issue)) { + self::debugError(sprintf("Invalid type for element of issues response from phan: %s\n", gettype($issues))); + return; + } + if ($issue['type'] !== 'issue') { + continue; + } + $pathInProject = $issue['location']['path']; // relative path + if (!isset($file_mapping[$pathInProject])) { + if (!$did_debug) { + self::debugInfo(sprintf("Unexpected path for issue (expected %s): %s\n", json_encode($file_mapping) ?: 'invalid', json_encode($issue) ?: 'invalid')); + } + $did_debug = true; + continue; + } + $line = $issue['location']['lines']['begin']; + $description = $issue['description']; + $parts = explode(' ', $description, 3); + if (count($parts) === 3 && $parts[1] === $issue['check_name']) { + $description = implode(': ', $parts); + } + if (isset($issue['suggestion'])) { + $description .= ' (' . $issue['suggestion'] . ')'; + } + $lines[] = sprintf("Phan error: %s in %s on line %d\n", $description, $file_mapping[$pathInProject], $line); + } + // https://github.com/neomake/neomake/issues/153 + echo implode('', $lines); + } + + /** + * Returns true if the output stream supports colors + * + * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo + * terminals via named pipes, so we can only check the environment. + * + * Reference: Composer\XdebugHandler\Process::supportsColor + * https://github.com/composer/xdebug-handler + * (This is internal, so it was duplicated in case their API changed) + * + * This duplicates CLI::supportsColor() so that phan_client can run as a standalone file + * + * @param resource $output A valid CLI output stream + * @return bool + * @suppress PhanUndeclaredFunction + */ + public static function supportsColor($output) + { + if ('Hyper' === getenv('TERM_PROGRAM')) { + return true; + } + if (\defined('PHP_WINDOWS_VERSION_BUILD')) { + return (\function_exists('sapi_windows_vt100_support') + && \sapi_windows_vt100_support($output)) + || false !== \getenv('ANSICON') + || 'ON' === \getenv('ConEmuANSI') + || 'xterm' === \getenv('TERM'); + } + + if (\function_exists('stream_isatty')) { + return \stream_isatty($output); + } elseif (\function_exists('posix_isatty')) { + return \posix_isatty($output); + } + + $stat = \fstat($output); + // Check if formatted mode is S_IFCHR + return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; + } + +} + +/** + * This represents the CLI options for Phan + * (and the logic to parse them and generate usage messages) + */ +class PhanPHPLinterOpts +{ + /** @var string tcp:// or unix:// socket URL of the daemon. */ + public $url; + + /** @var list - file list */ + public $file_list = []; + + /** @var string[]|null - optional, maps original files to temporary file path to use as a substitute. */ + public $temporary_file_map = null; + + /** @var bool if true, enable verbose output. */ + public $verbose = false; + + /** @var bool should this client request analysis from the Phan server when the file has syntax errors */ + public $use_fallback_parser; + + /** @var ?string the output mode to use. If null, use the default from the daemon */ + public $output_mode = null; + + /** @var ?bool whether to color the output on the client */ + public $color = null; + + /** + * @var bool should this client print a usage text if an **unexpected** error occurred. + */ + private $print_usage_on_error = true; + + /** + * @param string $msg - optional message + * @param int $exit_code - process exit code. + * @return void - exits with $exit_code + */ + public function usage($msg = '', $exit_code = 0) + { + if (strlen($msg) > 0 || $this->print_usage_on_error) { + global $argv; + if (!empty($msg)) { + echo "$msg\n"; + } + + // TODO: Add an option to autostart the daemon if user also has global configuration to allow it for a given project folder. ($HOME/.phanconfig) + // TODO: Allow changing (adding/removing) issue suppression types for the analysis phase (would not affect the parse phase) + + echo << + Unix socket which a Phan daemon is listening for requests on. + + --daemonize-tcp-port + TCP port which a Phan daemon is listening for JSON requests on, in daemon mode. (E.g. 'default', which is an alias for port 4846) + If no option is specified for the daemon's address, phan_client defaults to connecting on port 4846. + + --use-fallback-parser + Skip the local PHP syntax check. + Use this if the daemon is also executing with --use-fallback-parser, or if the daemon runs a different PHP version from the default. + Useful if you wish to report errors while editing the file, even if the file is currently syntactically invalid. + + -l, --syntax-check + Syntax check, and if the Phan daemon is running, analyze the following file (absolute path or relative to current working directory) + This will only analyze the file if a full phan check (with .phan/config.php) would analyze the file. + + -m, --output-mode + Output mode from 'phan_client' (default), 'text', 'json', 'csv', 'codeclimate', 'checkstyle', or 'pylint' + + -t, --temporary-file-map '{"file.php":"/path/to/tmp/file_copy.php"}' + A json mapping from original path to absolute temporary path (E.g. of a file that is still being edited) + + -f, --flycheck-file '/path/to/tmp/file_copy.php' + A simpler way to specify a file mapping when checking a single files. + Pass this after the only occurrence of --syntax-check. + + -d, --disable-usage-on-error + If this option is set, don't print full usage messages for missing/inaccessible files or inaccessible daemons. + (Continue printing usage messages for invalid combinations of options.) + + -v, --verbose + Whether to emit debugging output of this client. + + --color, --no-color + Whether or not to colorize reported issues. + The default and text output modes are colorized by default, if the terminal supports it. + + -h, --help + This help information + +EOB; + } + exit($exit_code); + } + + const GETOPT_SHORT_OPTIONS = 's:p:l:t:f:m:vhd'; + + const GETOPT_LONG_OPTIONS = [ + 'help', + 'daemonize-socket:', + 'daemonize-tcp-port:', + 'disable-usage-on-error', + 'syntax-check:', + 'temporary-file-map:', + 'use-fallback-parser', + 'flycheck-file:', + 'output-mode:', + 'color', + 'no-color', + 'verbose', + ]; + + /** + * @suppress PhanParamTooManyInternal - `getopt` added an optional third parameter in php 7.1 + * @suppress UnusedSuppression + */ + public function __construct() + { + global $argv; + + // Parse command line args + $optind = 0; + $getopt_reflection = new ReflectionFunction('getopt'); + if ($getopt_reflection->getNumberOfParameters() >= 3) { + // optind support is only in php 7.1+. + // hhvm doesn't expect a third parameter, but reports a version of php 7.1, even in the latest version. + $opts = getopt(self::GETOPT_SHORT_OPTIONS, self::GETOPT_LONG_OPTIONS, $optind); + } else { + $opts = getopt(self::GETOPT_SHORT_OPTIONS, self::GETOPT_LONG_OPTIONS); + } + if (PHP_VERSION_ID >= 70100 && $optind < count($argv)) { + $this->usage(sprintf("Unexpected parameter %s", json_encode($argv[$optind]) ?: var_export($argv[$optind], true))); + } + + // Check for this first, since the option parser may also emit debug output in the future. + if (in_array('-v', $argv, true) || in_array('--verbose', $argv, true)) { + PhanPHPLinter::$verbose = true; + $this->verbose = true; + } + $print_usage_on_error = true; + + if (!is_array($opts)) { + $opts = []; + } + foreach ($opts as $key => $value) { + switch ($key) { + case 's': + case 'daemonize-socket': + $this->checkCanConnectToDaemon('unix'); + if ($this->url !== null) { + $this->usage('Can specify --daemonize-socket or --daemonize-tcp-port only once', 1); + } + // Check if the socket is valid after parsing the file list. + // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal + $socket_dirname = dirname(realpath($value)); + if (!is_string($socket_dirname) || !file_exists($socket_dirname) || !is_dir($socket_dirname)) { + // The client doesn't require that the file exists if the daemon isn't running, but we do require that the folder exists. + $msg = sprintf('Configured to connect to Unix socket server at socket %s, but folder %s does not exist', json_encode($value) ?: 'invalid', json_encode($socket_dirname) ?: 'invalid'); + $this->usage($msg, 1); + } else { + $this->url = sprintf('unix://%s/%s', $socket_dirname, basename($value)); + } + break; + case 'use-fallback-parser': + $this->use_fallback_parser = true; + break; + case 'f': + case 'flycheck-file': + // Add alias, for use in flycheck + if (\is_array($this->temporary_file_map)) { + $this->usage('--flycheck-file should be specified only once.', 1); + } + if (!\is_array($this->file_list) || count($this->file_list) !== 1) { + $this->usage('--flycheck-file should be specified after the first occurrence of -l.', 1); + } + if (!is_string($value)) { + $this->usage('--flycheck-file should be passed a string value', 1); + break; // unreachable + } + $this->temporary_file_map = [$this->file_list[0] => $value]; + break; + case 't': + case 'temporary-file-map': + if (\is_array($this->temporary_file_map)) { + $this->usage('--temporary-file-map should be specified only once.', 1); + } + $mapping = json_decode($value, true); + if (!\is_array($mapping)) { + $this->usage('--temporary-file-map should be a JSON encoded map from source file to temporary file to analyze instead', 1); + break; // unreachable + } + $this->temporary_file_map = $mapping; + + break; + case 'p': + case 'daemonize-tcp-port': + $this->checkCanConnectToDaemon('tcp'); + if (strcasecmp($value, 'default') === 0) { + $port = 4846; + } else { + $port = filter_var($value, FILTER_VALIDATE_INT); + } + if ($port >= 1024 && $port <= 65535) { + $this->url = sprintf('tcp://127.0.0.1:%d', $port); + } else { + $this->usage("daemonize-tcp-port must be the string 'default' or an integer between 1024 and 65535, got '$value'", 1); + } + break; + case 'l': + case 'syntax-check': + $path = $value; + if (!is_string($path)) { + $this->print_usage_on_error = $print_usage_on_error; + $this->usage(sprintf("Error: asked to analyze path %s which is not a string", json_encode($path) ?: 'invalid'), 1); + exit(1); + } + if (!file_exists($path)) { + $this->print_usage_on_error = $print_usage_on_error; + $this->usage(sprintf("Error: asked to analyze file %s which does not exist", json_encode($path) ?: 'invalid'), 1); + exit(1); + } + $this->file_list[] = $path; + break; + case 'h': + case 'help': + $this->usage(); + break; + case 'd': + case 'disable-usage-on-error': + $print_usage_on_error = false; + break; + case 'v': + case 'verbose': + break; // already parsed. + case 'color': + $this->color = true; + break; + case 'no-color': + $this->color = false; + break; + case 'm': + case 'output-mode': + if (!is_string($value) || !in_array($value, ['text', 'json', 'csv', 'codeclimate', 'checkstyle', 'pylint', 'phan_client'], true)) { + $this->usage("Expected --output-mode {text,json,csv,codeclimate,checkstyle,pylint}, but got " . json_encode($value), 1); + break; // unreachable + } + if ($value === 'phan_client') { + // We're requesting the default + break; + } + $this->output_mode = $value; + break; + default: + $this->usage("Unknown option '-$key'", 1); + break; + } + } + try { + self::checkAllArgsUsed($opts, $argv); + } catch (InvalidArgumentException $e) { + $this->usage($e->getMessage(), 1); + } + + if (count($this->file_list) === 0) { + // Invalid invocation, always print this message + $this->usage("This requires at least one file to analyze (with -l path/to/file", 1); + } + if (\is_array($this->temporary_file_map)) { + foreach ($this->temporary_file_map as $original_path => $unused_temporary_path) { + if (!in_array($original_path, $this->file_list, true)) { + $this->usage("Need to specify -l '$original_path' if a mapping is included", 1); + } + } + } + if ($this->url === null) { + $this->url = 'tcp://127.0.0.1:4846'; + } + // In the majority of cases, apply this **after** checking sanity of CLI options + // (without actually starting the analysis). + $this->print_usage_on_error = $print_usage_on_error; + } + + /** + * prints error message if php doesn't support connecting to a daemon with a given protocol. + * @param string $protocol + * @return void + */ + private function checkCanConnectToDaemon($protocol) + { + $opt = $protocol === 'unix' ? '--daemonize-socket' : '--daemonize-tcp-port'; + if (!in_array($protocol, stream_get_transports(), true)) { + $this->usage("The $protocol:///path/to/file schema is not supported on this system, cannot connect to a daemon with $opt", 1); + } + if ($this->url !== null) { + $this->usage('Can specify --daemonize-socket or --daemonize-tcp-port only once', 1); + } + } + + /** + * Deliberately duplicating CLI::checkAllArgsUsed() + * + * @param array $opts + * @param list $argv + * @return void + * @throws InvalidArgumentException + */ + private static function checkAllArgsUsed(array $opts, array &$argv) + { + $pruneargv = []; + foreach ($opts as $opt => $value) { + foreach ($argv as $key => $chunk) { + $regex = '/^' . (isset($opt[1]) ? '--' : '-') . \preg_quote((string) $opt, '/') . '/'; + + if (in_array($chunk, is_array($value) ? $value : [$value], true) + && $argv[$key - 1][0] === '-' + || \preg_match($regex, $chunk) + ) { + $pruneargv[] = $key; + } + } + } + + while (count($pruneargv) > 0) { + $key = \array_pop($pruneargv); + unset($argv[$key]); + } + + foreach ($argv as $arg) { + if ($arg[0] === '-') { + $parts = \explode('=', $arg, 2); + $key = $parts[0]; + $value = isset($parts[1]) ? $parts[1] : ''; // php getopt() treats --processes and --processes= the same way + $key = \preg_replace('/^--?/', '', $key); + if ($value === '') { + if (in_array($key . ':', self::GETOPT_LONG_OPTIONS, true)) { + throw new InvalidArgumentException("Missing required value for '$arg'"); + } + if (strlen($key) === 1 && strlen($parts[0]) === 2) { + // @phan-suppress-next-line PhanParamSuspiciousOrder this is deliberate + if (\strpos(self::GETOPT_SHORT_OPTIONS, "$key:") !== false) { + throw new InvalidArgumentException("Missing required value for '-$key'"); + } + } + } + throw new InvalidArgumentException("Unknown option '$arg'" . self::getFlagSuggestionString($key), 1); + } + } + } + + /** + * Finds potentially misspelled flags and returns them as a string + * + * This will use levenshtein distance, showing the first one or two flags + * which match with a distance of <= 5 + * + * @param string $key Misspelled key to attempt to correct + * @return string + * @internal + */ + public static function getFlagSuggestionString( + $key + ) { + /** + * @param string $s + * @return string + */ + $trim = static function ($s) { + return \rtrim($s, ':'); + }; + /** + * @param string $suggestion + * @return string + */ + $generate_suggestion = static function ($suggestion) { + return (strlen($suggestion) === 1 ? '-' : '--') . $suggestion; + }; + /** + * @param string $suggestion + * @param string ...$other_suggestions + * @return string + */ + $generate_suggestion_text = static function ($suggestion, ...$other_suggestions) use ($generate_suggestion) { + $suggestions = \array_merge([$suggestion], $other_suggestions); + return ' (did you mean ' . \implode(' or ', array_map($generate_suggestion, $suggestions)) . '?)'; + }; + $short_options = \array_filter(array_map($trim, \str_split(self::GETOPT_SHORT_OPTIONS))); + if (strlen($key) === 1) { + $alternate = \ctype_lower($key) ? \strtoupper($key) : \strtolower($key); + if (in_array($alternate, $short_options, true)) { + return $generate_suggestion_text($alternate); + } + return ''; + } elseif ($key === '') { + return ''; + } elseif (strlen($key) > 255) { + // levenshtein refuses to run for longer keys + return ''; + } + // include short options in case a typo is made like -aa instead of -a + $known_flags = \array_merge(self::GETOPT_LONG_OPTIONS, $short_options); + + $known_flags = array_map($trim, $known_flags); + + $similarities = []; + + $key_lower = \strtolower($key); + foreach ($known_flags as $flag) { + if (strlen($flag) === 1 && \stripos($key, $flag) === false) { + // Skip over suggestions of flags that have no common characters + continue; + } + $distance = \levenshtein($key_lower, \strtolower($flag)); + // distance > 5 is too far off to be a typo + // Make sure that if two flags have the same distance, ties are sorted alphabetically + if ($distance > 5) { + continue; + } + if ($key === $flag) { + if (in_array($key . ':', self::GETOPT_LONG_OPTIONS, true)) { + return " (This option is probably missing the required value. Or this option may not apply to a regular Phan analysis, and/or it may be unintentionally unhandled in \Phan\CLI::__construct())"; + } else { + return " (This option may not apply to a regular Phan analysis, and/or it may be unintentionally unhandled in \Phan\CLI::__construct())"; + } + } + $similarities[$flag] = [$distance, "x" . \strtolower($flag), $flag]; + } + + \asort($similarities); // retain keys and sort descending + $similarity_values = \array_values($similarities); + + if (count($similarity_values) >= 2 && ($similarity_values[1][0] <= $similarity_values[0][0] + 1)) { + // If the next-closest suggestion isn't close to as similar as the closest suggestion, just return the closest suggestion + return $generate_suggestion_text($similarity_values[0][2], $similarity_values[1][2]); + } elseif (count($similarity_values) >= 1) { + return $generate_suggestion_text($similarity_values[0][2]); + } + return ''; + } +} +PhanPHPLinter::run(); diff --git a/bundled-libs/phan/phan/prep b/bundled-libs/phan/phan/prep new file mode 100755 index 000000000..d7e6a0a8f --- /dev/null +++ b/bundled-libs/phan/phan/prep @@ -0,0 +1,2 @@ +#!/usr/bin/env php += 8) { + return "\0\0\0\0\0\0\0\0" . \pack('J', $node); + } else { + return "\0\0\0\0\0\0\0\0\0\0\0\0" . \pack('N', $node); + } + } + // This is not a valid array key, give up + return md5((string) $node, true); + } + + /** + * @param Node|string|int|float|null $node + * @return string a 16-byte binary key for the Node which is unlikely to overlap for ordinary code + */ + public static function hash($node): string + { + if (!is_object($node)) { + // hashKey + if (is_string($node)) { + return md5($node, true); + } elseif (is_int($node)) { + if (\PHP_INT_SIZE >= 8) { + return "\0\0\0\0\0\0\0\0" . \pack('J', $node); + } else { + return "\0\0\0\0\0\0\0\0\0\0\0\0" . \pack('N', $node); + } + } elseif (is_float($node)) { + return "\0\0\0\0\0\0\0\1" . \pack('e', $node); + } elseif (is_null($node)) { + return "\0\0\0\0\0\0\0\2\0\0\0\0\0\0\0\0"; + } + // This is not a valid AST, give up + return md5((string) $node, true); + } + // @phan-suppress-next-line PhanUndeclaredProperty + return $node->hash ?? ($node->hash = self::computeHash($node)); + } + + /** + * @param Node $node + * @return string a newly computed 16-byte binary key + */ + private static function computeHash(Node $node): string + { + $str = 'N' . $node->kind . ':' . ($node->flags & 0xfffff); + foreach ($node->children as $key => $child) { + // added in PhanAnnotationAdder + if (\is_string($key) && \strncmp($key, 'phan', 4) === 0) { + continue; + } + $str .= self::hashKey($key); + $str .= self::hash($child); + } + return md5($str, true); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/ASTReverter.php b/bundled-libs/phan/phan/src/Phan/AST/ASTReverter.php new file mode 100644 index 000000000..929b7ecca --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/ASTReverter.php @@ -0,0 +1,556 @@ +foo(self::MY_CONST)") + * 3. Configuration of rendering this. + * + * Similar utilities: + * + * - https://github.com/tpunt/php-ast-reverter is a pretty printer. + * - \Phan\Debug::nodeToString() converts nodes to strings. + */ +class ASTReverter +{ + public const EXEC_NODE_FLAG_NAMES = [ + flags\EXEC_EVAL => 'eval', + flags\EXEC_INCLUDE => 'include', + flags\EXEC_INCLUDE_ONCE => 'include_once', + flags\EXEC_REQUIRE => 'require', + flags\EXEC_REQUIRE_ONCE => 'require_once', + ]; + + /** @var associative-array this contains maps from node kinds to closures to convert node kinds to strings */ + private static $closure_map; + /** @var Closure(Node):string this maps unknown node types to strings */ + private static $noop; + + // TODO: Make this configurable, copy instance properties to static properties. + public function __construct() + { + } + + /** + * Convert $node to a short PHP string representing $node. + * + * This does not work for all node kinds, and may be ambiguous. + * + * @param Node|string|int|float|bool|null|resource $node + */ + public static function toShortString($node): string + { + if (!($node instanceof Node)) { + if ($node === null) { + // use lowercase 'null' instead of 'NULL' + return 'null'; + } + if (\is_string($node)) { + return self::escapeString($node); + } + if (\is_resource($node)) { + return 'resource(' . \get_resource_type($node) . ')'; + } + // TODO: minimal representations for floats, arrays, etc. + return \var_export($node, true); + } + return (self::$closure_map[$node->kind] ?? self::$noop)($node); + } + + /** + * Escapes the inner contents to be suitable for a single-line single or double quoted string + * + * @see https://github.com/nikic/PHP-Parser/tree/master/lib/PhpParser/PrettyPrinter/Standard.php + */ + public static function escapeString(string $string): string + { + if (\preg_match('/([\0-\15\16-\37])/', $string)) { + // Use double quoted strings if this contains newlines, tabs, control characters, etc. + return '"' . self::escapeInnerString($string, '"') . '"'; + } + // Otherwise, use single quotes + return \var_export($string, true); + } + + /** + * Escapes the inner contents to be suitable for a single-line double quoted string + * + * @see https://github.com/nikic/PHP-Parser/tree/master/lib/PhpParser/PrettyPrinter/Standard.php + */ + public static function escapeInnerString(string $string, string $quote = null): string + { + if (null === $quote) { + // For doc strings, don't escape newlines + $escaped = \addcslashes($string, "\t\f\v$\\"); + } else { + $escaped = \addcslashes($string, "\n\r\t\f\v$" . $quote . "\\"); + } + + // Escape other control characters + return \preg_replace_callback('/([\0-\10\16-\37])(?=([0-7]?))/', /** @param list $matches */ static function (array $matches): string { + $oct = \decoct(\ord($matches[1])); + if ($matches[2] !== '') { + // If there is a trailing digit, use the full three character form + return '\\' . \str_pad($oct, 3, '0', \STR_PAD_LEFT); + } + return '\\' . $oct; + }, $escaped); + } + + /** + * Static initializer. + */ + public static function init(): void + { + self::$noop = static function (Node $_): string { + return '(unknown)'; + }; + self::$closure_map = [ + /** + * @suppress PhanAccessClassConstantInternal + */ + ast\AST_TYPE => static function (Node $node): string { + return PostOrderAnalysisVisitor::AST_CAST_FLAGS_LOOKUP[$node->flags]; + }, + /** + * @suppress PhanPartialTypeMismatchArgument + */ + ast\AST_TYPE_UNION => static function (Node $node): string { + return implode('|', \array_map('self::toShortTypeString', $node->children)); + }, + /** + * @suppress PhanTypeMismatchArgumentNullable + */ + ast\AST_NULLABLE_TYPE => static function (Node $node): string { + return '?' . self::toShortTypeString($node->children['type']); + }, + ast\AST_POST_INC => static function (Node $node): string { + return self::formatIncDec('%s++', $node->children['var']); + }, + ast\AST_PRE_INC => static function (Node $node): string { + return self::formatIncDec('++%s', $node->children['var']); + }, + ast\AST_POST_DEC => static function (Node $node): string { + return self::formatIncDec('%s--', $node->children['var']); + }, + ast\AST_PRE_DEC => static function (Node $node): string { + return self::formatIncDec('--%s', $node->children['var']); + }, + ast\AST_ARG_LIST => static function (Node $node): string { + return '(' . implode(', ', \array_map('self::toShortString', $node->children)) . ')'; + }, + ast\AST_NAMED_ARG => static function (Node $node): string { + return $node->children['name'] . ': ' . self::toShortString($node->children['expr']); + }, + ast\AST_PARAM_LIST => static function (Node $node): string { + return '(' . implode(', ', \array_map('self::toShortString', $node->children)) . ')'; + }, + ast\AST_PARAM => static function (Node $node): string { + $str = '$' . $node->children['name']; + if ($node->flags & ast\flags\PARAM_VARIADIC) { + $str = "...$str"; + } + if ($node->flags & ast\flags\PARAM_REF) { + $str = "&$str"; + } + if (isset($node->children['type'])) { + $str = ASTReverter::toShortString($node->children['type']) . ' ' . $str; + } + if (isset($node->children['default'])) { + $str .= ' = ' . ASTReverter::toShortString($node->children['default']); + } + return $str; + }, + ast\AST_EXPR_LIST => static function (Node $node): string { + return implode(', ', \array_map('self::toShortString', $node->children)); + }, + ast\AST_CLASS_CONST => static function (Node $node): string { + return self::toShortString($node->children['class']) . '::' . $node->children['const']; + }, + ast\AST_CLASS_NAME => static function (Node $node): string { + return self::toShortString($node->children['class']) . '::class'; + }, + ast\AST_MAGIC_CONST => static function (Node $node): string { + return UnionTypeVisitor::MAGIC_CONST_NAME_MAP[$node->flags] ?? '(unknown)'; + }, + ast\AST_CONST => static function (Node $node): string { + return self::toShortString($node->children['name']); + }, + ast\AST_VAR => static function (Node $node): string { + $name_node = $node->children['name']; + return '$' . (is_string($name_node) ? $name_node : ('{' . self::toShortString($name_node) . '}')); + }, + ast\AST_DIM => static function (Node $node): string { + $expr_str = self::toShortString($node->children['expr']); + if ($expr_str === '(unknown)') { + return '(unknown)'; + } + + $dim = $node->children['dim']; + if ($dim !== null) { + $dim_str = self::toShortString($dim); + } else { + $dim_str = ''; + } + if ($node->flags & ast\flags\DIM_ALTERNATIVE_SYNTAX) { + return "${expr_str}{{$dim_str}}"; + } + return "${expr_str}[$dim_str]"; + }, + ast\AST_NAME => static function (Node $node): string { + $result = $node->children['name']; + switch ($node->flags) { + case ast\flags\NAME_FQ: + return '\\' . $result; + case ast\flags\NAME_RELATIVE: + return 'namespace\\' . $result; + default: + return (string)$result; + } + }, + ast\AST_NAME_LIST => static function (Node $node): string { + return implode('|', \array_map('self::toShortString', $node->children)); + }, + ast\AST_ARRAY => static function (Node $node): string { + $parts = []; + foreach ($node->children as $elem) { + if (!$elem instanceof Node) { + // Should always either be a Node or null. + $parts[] = ''; + continue; + } + $part = self::toShortString($elem->children['value']); + $key_node = $elem->children['key']; + if ($key_node !== null) { + $part = self::toShortString($key_node) . '=>' . $part; + } + $parts[] = $part; + } + $string = implode(',', $parts); + switch ($node->flags) { + case ast\flags\ARRAY_SYNTAX_SHORT: + case ast\flags\ARRAY_SYNTAX_LONG: + default: + return "[$string]"; + case ast\flags\ARRAY_SYNTAX_LIST: + return "list($string)"; + } + }, + /** @suppress PhanAccessClassConstantInternal */ + ast\AST_BINARY_OP => static function (Node $node): string { + return sprintf( + "(%s %s %s)", + self::toShortString($node->children['left']), + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags] ?? 'unknown', + self::toShortString($node->children['right']) + ); + }, + ast\AST_ASSIGN => static function (Node $node): string { + return sprintf( + "(%s = %s)", + self::toShortString($node->children['var']), + self::toShortString($node->children['expr']) + ); + }, + ast\AST_ASSIGN_REF => static function (Node $node): string { + return sprintf( + "(%s =& %s)", + self::toShortString($node->children['var']), + self::toShortString($node->children['expr']) + ); + }, + /** @suppress PhanAccessClassConstantInternal */ + ast\AST_ASSIGN_OP => static function (Node $node): string { + return sprintf( + "(%s %s= %s)", + self::toShortString($node->children['var']), + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags] ?? 'unknown', + self::toShortString($node->children['expr']) + ); + }, + ast\AST_UNARY_OP => static function (Node $node): string { + $operation_name = PostOrderAnalysisVisitor::NAME_FOR_UNARY_OP[$node->flags] ?? null; + if (!$operation_name) { + return '(unknown)'; + } + $expr = $node->children['expr']; + $expr_text = self::toShortString($expr); + if (($expr->kind ?? null) !== ast\AST_UNARY_OP) { + return $operation_name . $expr_text; + } + return sprintf("%s(%s)", $operation_name, $expr_text); + }, + ast\AST_PROP => static function (Node $node): string { + $prop_node = $node->children['prop']; + return sprintf( + '%s->%s', + self::toShortString($node->children['expr']), + $prop_node instanceof Node ? '{' . self::toShortString($prop_node) . '}' : (string)$prop_node + ); + }, + ast\AST_NULLSAFE_PROP => static function (Node $node): string { + $prop_node = $node->children['prop']; + return sprintf( + '%s?->%s', + self::toShortString($node->children['expr']), + $prop_node instanceof Node ? '{' . self::toShortString($prop_node) . '}' : (string)$prop_node + ); + }, + ast\AST_STATIC_CALL => static function (Node $node): string { + $method_node = $node->children['method']; + return sprintf( + '%s::%s%s', + self::toShortString($node->children['class']), + is_string($method_node) ? $method_node : self::toShortString($method_node), + self::toShortString($node->children['args']) + ); + }, + ast\AST_METHOD_CALL => static function (Node $node): string { + $method_node = $node->children['method']; + return sprintf( + '%s->%s%s', + self::toShortString($node->children['expr']), + is_string($method_node) ? $method_node : self::toShortString($method_node), + self::toShortString($node->children['args']) + ); + }, + ast\AST_NULLSAFE_METHOD_CALL => static function (Node $node): string { + $method_node = $node->children['method']; + return sprintf( + '%s?->%s%s', + self::toShortString($node->children['expr']), + is_string($method_node) ? $method_node : self::toShortString($method_node), + self::toShortString($node->children['args']) + ); + }, + ast\AST_STATIC_PROP => static function (Node $node): string { + $prop_node = $node->children['prop']; + return sprintf( + '%s::$%s', + self::toShortString($node->children['class']), + $prop_node instanceof Node ? '{' . self::toShortString($prop_node) . '}' : (string)$prop_node + ); + }, + ast\AST_INSTANCEOF => static function (Node $node): string { + return sprintf( + '(%s instanceof %s)', + self::toShortString($node->children['expr']), + self::toShortString($node->children['class']) + ); + }, + ast\AST_CAST => static function (Node $node): string { + return sprintf( + '(%s)(%s)', + // @phan-suppress-next-line PhanAccessClassConstantInternal + PostOrderAnalysisVisitor::AST_CAST_FLAGS_LOOKUP[$node->flags] ?? 'unknown', + self::toShortString($node->children['expr']) + ); + }, + ast\AST_CALL => static function (Node $node): string { + return sprintf( + '%s%s', + self::toShortString($node->children['expr']), + self::toShortString($node->children['args']) + ); + }, + ast\AST_NEW => static function (Node $node): string { + // TODO: add parenthesis in case this is used as (new X())->method(), or properties, but only when necessary + return sprintf( + 'new %s%s', + self::toShortString($node->children['class']), + self::toShortString($node->children['args']) + ); + }, + ast\AST_CLONE => static function (Node $node): string { + // clone($x)->someMethod() has surprising precedence, + // so surround `clone $x` with parenthesis. + return sprintf( + '(clone(%s))', + self::toShortString($node->children['expr']) + ); + }, + ast\AST_CONDITIONAL => static function (Node $node): string { + ['cond' => $cond, 'true' => $true, 'false' => $false] = $node->children; + if ($true !== null) { + return sprintf('(%s ? %s : %s)', self::toShortString($cond), self::toShortString($true), self::toShortString($false)); + } + return sprintf('(%s ?: %s)', self::toShortString($cond), self::toShortString($false)); + }, + /** @suppress PhanPossiblyUndeclaredProperty */ + ast\AST_MATCH => static function (Node $node): string { + ['cond' => $cond, 'stmts' => $stmts] = $node->children; + return sprintf('match (%s) {%s}', ASTReverter::toShortString($cond), $stmts->children ? ' ' . ASTReverter::toShortString($stmts) . ' ' : ''); + }, + ast\AST_MATCH_ARM_LIST => static function (Node $node): string { + return implode(', ', \array_map(self::class . '::toShortString', $node->children)); + }, + ast\AST_MATCH_ARM => static function (Node $node): string { + ['cond' => $cond, 'expr' => $expr] = $node->children; + return sprintf('%s => %s', $cond !== null ? ASTReverter::toShortString($cond) : 'default', ASTReverter::toShortString($expr)); + }, + ast\AST_ISSET => static function (Node $node): string { + return sprintf( + 'isset(%s)', + self::toShortString($node->children['var']) + ); + }, + ast\AST_EMPTY => static function (Node $node): string { + return sprintf( + 'empty(%s)', + self::toShortString($node->children['expr']) + ); + }, + ast\AST_PRINT => static function (Node $node): string { + return sprintf( + 'print(%s)', + self::toShortString($node->children['expr']) + ); + }, + ast\AST_ECHO => static function (Node $node): string { + return 'echo ' . ASTReverter::toShortString($node->children['expr']) . ';'; + }, + ast\AST_UNPACK => static function (Node $node): string { + return sprintf( + '...(%s)', + self::toShortString($node->children['expr']) + ); + }, + ast\AST_INCLUDE_OR_EVAL => static function (Node $node): string { + return sprintf( + '%s(%s)', + self::EXEC_NODE_FLAG_NAMES[$node->flags], + self::toShortString($node->children['expr']) + ); + }, + ast\AST_ENCAPS_LIST => static function (Node $node): string { + $parts = []; + foreach ($node->children as $c) { + if ($c instanceof Node) { + $parts[] = '{' . self::toShortString($c) . '}'; + } else { + $parts[] = self::escapeInnerString((string)$c, '"'); + } + } + return '"' . implode('', $parts) . '"'; + }, + ast\AST_SHELL_EXEC => static function (Node $node): string { + $parts = []; + $expr = $node->children['expr']; + if ($expr instanceof Node) { + foreach ($expr->children as $c) { + if ($c instanceof Node) { + $parts[] = '{' . self::toShortString($c) . '}'; + } else { + $parts[] = self::escapeInnerString((string)$c, '`'); + } + } + } else { + $parts[] = self::escapeInnerString((string)$expr, '`'); + } + return '`' . implode('', $parts) . '`'; + }, + // Slightly better short placeholders than (unknown) + ast\AST_CLOSURE => static function (Node $_): string { + return '(function)'; + }, + ast\AST_ARROW_FUNC => static function (Node $_): string { + return '(fn)'; + }, + ast\AST_RETURN => static function (Node $node): string { + return sprintf( + 'return %s;', + self::toShortString($node->children['expr']) + ); + }, + ast\AST_THROW => static function (Node $node): string { + return sprintf( + '(throw %s)', + self::toShortString($node->children['expr']) + ); + }, + ast\AST_FOR => static function (Node $_): string { + return '(for loop)'; + }, + ast\AST_WHILE => static function (Node $_): string { + return '(while loop)'; + }, + ast\AST_DO_WHILE => static function (Node $_): string { + return '(do-while loop)'; + }, + ast\AST_FOREACH => static function (Node $_): string { + return '(foreach loop)'; + }, + ast\AST_IF => static function (Node $_): string { + return '(if statement)'; + }, + ast\AST_IF_ELEM => static function (Node $_): string { + return '(if statement element)'; + }, + ast\AST_TRY => static function (Node $_): string { + return '(try statement)'; + }, + ast\AST_SWITCH => static function (Node $_): string { + return '(switch statement)'; + }, + ast\AST_SWITCH_LIST => static function (Node $_): string { + return '(switch case list)'; + }, + ast\AST_SWITCH_CASE => static function (Node $_): string { + return '(switch case statement)'; + }, + // TODO: AST_SHELL_EXEC, AST_ENCAPS_LIST(in shell_exec or double quotes) + ]; + } + + /** + * Returns the representation of an AST_TYPE, AST_NULLABLE_TYPE, AST_TYPE_UNION, or AST_NAME, as seen in an element signature + */ + public static function toShortTypeString(Node $node): string + { + if ($node->kind === ast\AST_NULLABLE_TYPE) { + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + return '?' . self::toShortTypeString($node->children['type']); + } + if ($node->kind === ast\AST_TYPE) { + return PostOrderAnalysisVisitor::AST_TYPE_FLAGS_LOOKUP[$node->flags]; + } + // Probably AST_NAME + return self::toShortString($node); + } + + + /** + * @param Node|string|int|float $node + */ + private static function formatIncDec(string $format, $node): string + { + $str = self::toShortString($node); + if (!($node instanceof Node && $node->kind === ast\AST_VAR)) { + $str = '(' . $str . ')'; + } + // @phan-suppress-next-line PhanPluginPrintfVariableFormatString + return sprintf($format, $str); + } +} +ASTReverter::init(); diff --git a/bundled-libs/phan/phan/src/Phan/AST/ASTSimplifier.php b/bundled-libs/phan/phan/src/Phan/AST/ASTSimplifier.php new file mode 100644 index 000000000..f5d44152e --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/ASTSimplifier.php @@ -0,0 +1,789 @@ + (Equivalent list of nodes to [$node], possibly a clone with modifications) + */ + private static function apply(Node $node): array + { + switch ($node->kind) { + case ast\AST_FUNC_DECL: + case ast\AST_METHOD: + case ast\AST_CLOSURE: + case ast\AST_CLASS: + case ast\AST_DO_WHILE: + case ast\AST_FOREACH: + return [self::applyToStmts($node)]; + case ast\AST_FOR: + return self::normalizeForStatement($node); + case ast\AST_WHILE: + return self::normalizeWhileStatement($node); + //case ast\AST_BREAK: + //case ast\AST_CONTINUE: + //case ast\AST_RETURN: + //case ast\AST_THROW: + //case ast\AST_EXIT: + default: + return [$node]; + case ast\AST_STMT_LIST: + return [self::applyToStatementList($node)]; + // Conditional blocks: + case ast\AST_IF: + return self::normalizeIfStatement($node); + case ast\AST_TRY: + return [self::normalizeTryStatement($node)]; + } + } + + /** + * @param Node $node - A node which will have its child statements simplified. + * @return Node - The same node, or an equivalent simplified node + */ + private static function applyToStmts(Node $node): Node + { + $stmts = $node->children['stmts']; + // Can be null, a single statement, or (possibly) a scalar instead of a node? + if (!($stmts instanceof Node)) { + return $node; + } + $new_stmts = self::applyToStatementList($stmts); + if ($new_stmts === $stmts) { + return $node; + } + $new_node = clone($node); + $new_node->children['stmts'] = $new_stmts; + return $new_node; + } + + /** + * @param Node $statement_list - The statement list to simplify + * @return Node - an equivalent statement list (Identical, or a clone) + */ + private static function applyToStatementList(Node $statement_list): Node + { + if ($statement_list->kind !== ast\AST_STMT_LIST) { + $statement_list = self::buildStatementList($statement_list->lineno, $statement_list); + } + $new_children = []; + foreach ($statement_list->children as $child_node) { + if ($child_node instanceof Node) { + foreach (self::apply($child_node) as $new_child_node) { + $new_children[] = $new_child_node; + } + } else { + $new_children[] = $child_node; + } + } + [$new_children, $modified] = self::normalizeStatementList($new_children); + if (!$modified && $new_children === $statement_list->children) { + return $statement_list; + } + $clone_node = clone($statement_list); + $clone_node->children = $new_children; + return $clone_node; + } + + /** + * Creates a new node with kind ast\AST_STMT_LIST from a list of 0 or more child nodes. + */ + private static function buildStatementList(int $lineno, Node ...$child_nodes): Node + { + return new Node( + ast\AST_STMT_LIST, + 0, + $child_nodes, + $lineno + ); + } + + /** + * @param list $statements + * @return array{0:list,1:bool} - [New/old list, bool $modified] An equivalent list after simplifying (or the original list) + */ + private static function normalizeStatementList(array $statements): array + { + $modified = false; + $new_statements = []; + foreach ($statements as $stmt) { + $new_statements[] = $stmt; + if (!($stmt instanceof Node)) { + continue; + } + if ($stmt->kind !== ast\AST_IF) { + continue; + } + // Run normalizeIfStatement again. + \array_pop($new_statements); + \array_push($new_statements, ...self::normalizeIfStatement($stmt)); + $modified = $modified || \end($new_statements) !== $stmt; + continue; + } + return [$modified ? $new_statements : $statements, $modified]; + } + + /** + * Replaces the last node in a list with a list of 0 or more nodes + * @param list $nodes + * @param Node ...$new_statements + */ + private static function replaceLastNodeWithNodeList(array &$nodes, Node...$new_statements): void + { + if (\array_pop($nodes) === false) { + throw new AssertionError("Saw an unexpected empty node list"); + } + foreach ($new_statements as $stmt) { + $nodes[] = $stmt; + } + } + + public const NON_SHORT_CIRCUITING_BINARY_OPERATOR_FLAGS = [ + flags\BINARY_BOOL_XOR, + flags\BINARY_IS_IDENTICAL, + flags\BINARY_IS_NOT_IDENTICAL, + flags\BINARY_IS_EQUAL, + flags\BINARY_IS_NOT_EQUAL, + flags\BINARY_IS_SMALLER, + flags\BINARY_IS_SMALLER_OR_EQUAL, + flags\BINARY_IS_GREATER, + flags\BINARY_IS_GREATER_OR_EQUAL, + flags\BINARY_SPACESHIP, + ]; + + /** + * If this returns true, the expression has no side effects, and can safely be reordered. + * (E.g. returns true for `MY_CONST` or `false` in `if (MY_CONST === ($x = y))` + * + * @param Node|string|float|int $node + * @internal the way this behaves may change + * @see ScopeImpactCheckingVisitor::hasPossibleImpact() for a more general check + */ + public static function isExpressionWithoutSideEffects($node): bool + { + if (!($node instanceof Node)) { + return true; + } + switch ($node->kind) { + case ast\AST_CONST: + case ast\AST_MAGIC_CONST: + case ast\AST_NAME: + return true; + case ast\AST_UNARY_OP: + return self::isExpressionWithoutSideEffects($node->children['expr']); + case ast\AST_BINARY_OP: + return self::isExpressionWithoutSideEffects($node->children['left']) && + self::isExpressionWithoutSideEffects($node->children['right']); + case ast\AST_CLASS_CONST: + case ast\AST_CLASS_NAME: + return self::isExpressionWithoutSideEffects($node->children['class']); + default: + return false; + } + } + + /** + * Converts an if statement to one which is easier for phan to analyze + * E.g. repeatedly makes these conversions + * if (A && B) {X} -> if (A) { if (B) {X}} + * if ($var = A) {X} -> $var = A; if ($var) {X} + * @return non-empty-list - One or more nodes created from $original_node. + * Will return [$original_node] if no modifications were made. + */ + private static function normalizeIfStatement(Node $original_node): array + { + $nodes = [$original_node]; + // Repeatedly apply these rules + do { + $old_nodes = $nodes; + $node = $nodes[count($nodes) - 1]; + $node->flags = 0; + $if_cond = $node->children[0]->children['cond']; + if (!($if_cond instanceof Node)) { + break; // No transformation rules apply here. + } + + if ($if_cond->kind === ast\AST_UNARY_OP && + $if_cond->flags === flags\UNARY_BOOL_NOT) { + $cond_node = $if_cond->children['expr']; + if ($cond_node instanceof Node && + $cond_node->kind === ast\AST_UNARY_OP && + $cond_node->flags === flags\UNARY_BOOL_NOT) { + self::replaceLastNodeWithNodeList($nodes, self::applyIfDoubleNegateReduction($node)); + continue; + } + if (count($node->children) === 1) { + self::replaceLastNodeWithNodeList($nodes, self::applyIfNegatedToIfElseReduction($node)); + continue; + } + } + if ($if_cond->kind === ast\AST_BINARY_OP && in_array($if_cond->flags, self::NON_SHORT_CIRCUITING_BINARY_OPERATOR_FLAGS, true)) { + // if (($var = A) === B) {X} -> $var = A; if ($var === B) { X} + $if_cond_children = $if_cond->children; + if (in_array($if_cond_children['left']->kind ?? 0, [ast\AST_ASSIGN, ast\AST_ASSIGN_REF], true) && + ($if_cond_children['left']->children['var']->kind ?? 0) === ast\AST_VAR && + self::isExpressionWithoutSideEffects($if_cond_children['right'])) { + self::replaceLastNodeWithNodeList($nodes, ...self::applyAssignInLeftSideOfBinaryOpReduction($node)); + continue; + } + if (in_array($if_cond_children['right']->kind ?? 0, [ast\AST_ASSIGN, ast\AST_ASSIGN_REF], true) && + ($if_cond_children['right']->children['var']->kind ?? 0) === ast\AST_VAR && + self::isExpressionWithoutSideEffects($if_cond_children['left'])) { + self::replaceLastNodeWithNodeList($nodes, ...self::applyAssignInRightSideOfBinaryOpReduction($node)); + continue; + } + // TODO: If the left-hand side is a constant or class constant or literal, that's safe to rearrange as well + // (But `foo($y = something()) && $x = $y` is not safe to rearrange) + } + if (count($node->children) === 1) { + if ($if_cond->kind === ast\AST_BINARY_OP && + $if_cond->flags === flags\BINARY_BOOL_AND) { + self::replaceLastNodeWithNodeList($nodes, self::applyIfAndReduction($node)); + // if (A && B) {X} -> if (A) { if (B) {X}} + // Do this, unless there is an else statement that can be executed. + continue; + } + } elseif (count($node->children) === 2) { + if ($if_cond->kind === ast\AST_UNARY_OP && + $if_cond->flags === flags\UNARY_BOOL_NOT && + $node->children[1]->children['cond'] === null) { + self::replaceLastNodeWithNodeList($nodes, self::applyIfNegateReduction($node)); + continue; + } + } elseif (count($node->children) >= 3) { + self::replaceLastNodeWithNodeList($nodes, self::applyIfChainReduction($node)); + continue; + } + if ($if_cond->kind === ast\AST_ASSIGN && + ($if_cond->children['var']->kind ?? null) === ast\AST_VAR) { + // if ($var = A) {X} -> $var = A; if ($var) {X} + // do this whether or not there is an else. + // TODO: Could also reduce `if (($var = A) && B) {X} else if (C) {Y} -> $var = A; .... + self::replaceLastNodeWithNodeList($nodes, ...self::applyIfAssignReduction($node)); + continue; + } + } while ($old_nodes !== $nodes); + return $nodes; + } + + /** + * Converts a while statement to one which is easier for phan to analyze + * E.g. repeatedly makes these conversions + * while (A && B) {X} -> while (A) { if (!B) {break;} X} + * while (!!A) {X} -> while (A) { X } + * @return array{0:Node} - An array with a single while statement + * Will return [$original_node] if no modifications were made. + */ + private static function normalizeWhileStatement(Node $original_node): array + { + $node = $original_node; + // Repeatedly apply these rules + while (true) { + $while_cond = $node->children['cond']; + if (!($while_cond instanceof Node)) { + break; // No transformation rules apply here. + } + + if ($while_cond->kind === ast\AST_UNARY_OP && + $while_cond->flags === flags\UNARY_BOOL_NOT) { + $cond_node = $while_cond->children['expr']; + if ($cond_node instanceof Node && + $cond_node->kind === ast\AST_UNARY_OP && + $cond_node->flags === flags\UNARY_BOOL_NOT) { + $node = self::applyWhileDoubleNegateReduction($node); + continue; + } + break; + } + if ($while_cond->kind === ast\AST_BINARY_OP && + $while_cond->flags === flags\BINARY_BOOL_AND) { + // TODO: Also support `and` operator. + $node = self::applyWhileAndReduction($node); + // while (A && B) {X} -> while (A) { if (!B) {break;} X} + // Do this, unless there is an else statement that can be executed. + continue; + } + break; + } + + return [$node]; + } + + /** + * Converts a for statement to one which is easier for phan to analyze + * E.g. repeatedly makes these conversions + * for (init; !!cond; loop) -> for (init; cond; loop) + * @return array{0:Node} - An array with a single for statement. + * Will return [$node] if no modifications were made. + */ + private static function normalizeForStatement(Node $node): array + { + // Repeatedly apply these rules + while (true) { + $for_cond_list = $node->children['cond']; + if (!($for_cond_list instanceof Node)) { + break; // No transformation rules apply here. + } + + $for_cond = \end($for_cond_list->children); + + if (!($for_cond instanceof Node)) { + break; + } + if ($for_cond->kind === ast\AST_UNARY_OP && + $for_cond->flags === flags\UNARY_BOOL_NOT) { + $cond_node = $for_cond->children['expr']; + if ($cond_node instanceof Node && + $cond_node->kind === ast\AST_UNARY_OP && + $cond_node->flags === flags\UNARY_BOOL_NOT) { + $node = self::applyForDoubleNegateReduction($node); + continue; + } + } + break; + } + + return [$node]; + } + + /** + * if (($var = A) === B) {X} -> $var = A; if ($var === B) { X } + * + * @return array{0:Node,1:Node} + * @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller. + */ + private static function applyAssignInLeftSideOfBinaryOpReduction(Node $node): array + { + $inner_assign_statement = $node->children[0]->children['cond']->children['left']; + if (!($inner_assign_statement instanceof Node)) { + throw new AssertionError('Expected $inner_assign_statement instanceof Node'); + } + $inner_assign_var = $inner_assign_statement->children['var']; + + if ($inner_assign_var->kind !== ast\AST_VAR) { + throw new AssertionError('Expected $inner_assign_var->kind === ast\AST_VAR'); + } + + $new_node_elem = clone($node->children[0]); + $new_node_elem->children['cond']->children['left'] = $inner_assign_var; + $new_node_elem->flags = 0; + $new_node = clone($node); + $new_node->children[0] = $new_node_elem; + $new_node->lineno = $new_node_elem->lineno; + $new_node->flags = 0; + return [$inner_assign_statement, $new_node]; + } + + /** + * if (B === ($var = A)) {X} -> $var = A; if (B === $var) { X } + * + * @return array{0:Node,1:Node} + * @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller. + */ + private static function applyAssignInRightSideOfBinaryOpReduction(Node $node): array + { + $inner_assign_statement = $node->children[0]->children['cond']->children['right']; + $inner_assign_var = $inner_assign_statement->children['var']; + + $new_node_elem = clone($node->children[0]); + $new_node_elem->children['cond']->children['right'] = $inner_assign_var; + $new_node_elem->flags = 0; + $new_node = clone($node); + $new_node->children[0] = $new_node_elem; + $new_node->lineno = $new_node_elem->lineno; + $new_node->flags = 0; + return [$inner_assign_statement, $new_node]; + } + + /** + * Creates a new node with kind ast\AST_IF from two branches + */ + private static function buildIfNode(Node $l, Node $r): Node + { + return new Node( + ast\AST_IF, + 0, + [$l, $r], + $l->lineno + ); + } + + /** + * maps if (A) {X} elseif (B) {Y} else {Z} -> if (A) {Y} else { if (B) {Y} else {Z}} + */ + private static function applyIfChainReduction(Node $node): Node + { + $children = $node->children; // Copy of array of Nodes of type IF_ELEM + if (count($children) <= 2) { + return $node; + } + while (count($children) > 2) { + $r = array_pop($children); + $l = array_pop($children); + if (!($l instanceof Node && $r instanceof Node)) { + throw new AssertionError("Expected to have AST_IF_ELEM nodes"); + } + $l->children['stmts']->flags = 0; + $r->children['stmts']->flags = 0; + $inner_if_node = self::buildIfNode($l, $r); + $new_r = new Node( + ast\AST_IF_ELEM, + 0, + [ + 'cond' => null, + 'stmts' => self::buildStatementList($inner_if_node->lineno, ...(self::normalizeIfStatement($inner_if_node))), + ], + 0 + ); + $children[] = $new_r; + } + // $children is an array of 2 nodes of type IF_ELEM + return new Node(ast\AST_IF, 0, $children, $node->lineno); + } + + /** + * Converts if (A && B) {X}` -> `if (A) { if (B){X}}` + * @return Node simplified node logically equivalent to $node, with kind ast\AST_IF. + * @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller. + */ + private static function applyIfAndReduction(Node $node): Node + { + if (count($node->children) !== 1) { + throw new AssertionError('Expected an if statement with no else/elseif statements'); + } + $inner_node_elem = clone($node->children[0]); // AST_IF_ELEM + $inner_node_elem->children['cond'] = $inner_node_elem->children['cond']->children['right']; + $inner_node_elem->flags = 0; + $inner_node_lineno = $inner_node_elem->lineno; + + // Normalize code such as `if (A && (B && C)) {...}` recursively. + $inner_node_stmts = self::normalizeIfStatement(new Node( + ast\AST_IF, + 0, + [$inner_node_elem], + $inner_node_lineno + )); + + $inner_node_stmt_list = new Node(ast\AST_STMT_LIST, 0, $inner_node_stmts, $inner_node_lineno); + $outer_node_elem = clone($node->children[0]); // AST_IF_ELEM + $outer_node_elem->children['cond'] = $node->children[0]->children['cond']->children['left']; + $outer_node_elem->children['stmts'] = $inner_node_stmt_list; + $outer_node_elem->flags = 0; + return new Node( + ast\AST_IF, + 0, + [$outer_node_elem], + $node->lineno + ); + } + + /** + * Converts `while (A && B) {X}` -> `while (A) { if (!B) { break;} X}` + * @return Node simplified node logically equivalent to $node, with kind ast\AST_IF. + */ + private static function applyWhileAndReduction(Node $node): Node + { + $cond_node = $node->children['cond']; + $right_node = $cond_node->children['right']; + $lineno = $right_node->lineno ?? $cond_node->lineno; + $conditional_break_elem = self::makeBreakWithNegatedConditional($right_node, $lineno); + + return new Node( + ast\AST_WHILE, + 0, + [ + 'cond' => $cond_node->children['left'], + 'stmts' => new Node( + ast\AST_STMT_LIST, + 0, + array_merge([$conditional_break_elem], $node->children['stmts']->children), + $lineno + ), + ], + $node->lineno + ); + } + + /** + * Creates a Node for `if (!COND) { break; }` + * @param Node|string|int|float $cond_node + */ + private static function makeBreakWithNegatedConditional($cond_node, int $lineno): Node + { + $break_if_elem = new Node( + ast\AST_IF_ELEM, + 0, + [ + 'cond' => new Node( + ast\AST_UNARY_OP, + flags\UNARY_BOOL_NOT, + ['expr' => $cond_node], + $lineno + ), + 'stmts' => new Node( + ast\AST_STMT_LIST, + 0, + [new Node(ast\AST_BREAK, 0, ['depth' => null], $lineno)], + $lineno + ), + ], + $lineno + ); + return new Node( + ast\AST_IF, + 0, + [$break_if_elem], + $lineno + ); + } + + /** + * Converts if ($x = A) {Y} -> $x = A; if ($x) {Y} + * This allows analyzing variables set in if blocks outside of the `if` block + * @return array{0:Node,1:Node} [$outer_assign_statement, $new_node] + * @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller. + */ + private static function applyIfAssignReduction(Node $node): array + { + $outer_assign_statement = $node->children[0]->children['cond']; + if (!($outer_assign_statement instanceof Node)) { + throw new AssertionError('Expected condition of first if statement (with assignment as condition) to be a Node'); + } + $new_node_elem = clone($node->children[0]); + $new_node_elem->children['cond'] = $new_node_elem->children['cond']->children['var']; + $new_node_elem->flags = 0; + $new_node = clone($node); + $new_node->children[0] = $new_node_elem; + $new_node->lineno = $new_node_elem->lineno; + $new_node->flags = 0; + return [$outer_assign_statement, $new_node]; + } + + /** + * Converts if (!x) {Y} else {Z} -> if (x) {Z} else {Y} + * This improves Phan's analysis for cases such as `if (!is_string($x))`. + * @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller. + */ + private static function applyIfNegateReduction(Node $node): Node + { + if (!( + count($node->children) === 2 && + $node->children[0]->children['cond']->flags === flags\UNARY_BOOL_NOT && + $node->children[1]->children['cond'] === null + )) { + throw new AssertionError('Failed precondition of ' . __METHOD__); + } + $new_node = clone($node); + $new_node->children = [clone($new_node->children[1]), clone($new_node->children[0])]; + $new_node->children[0]->children['cond'] = $node->children[0]->children['cond']->children['expr']; + $new_node->children[1]->children['cond'] = null; + $new_node->flags = 0; + // @phan-suppress-next-line PhanUndeclaredProperty used by EmptyStatementListPlugin + $new_node->is_simplified = true; + return $new_node; + } + + /** + * Converts if (!!(x)) {Y} -> if (x) {Y} + * This improves Phan's analysis for cases such as `if (!!x)` + * @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller. + */ + private static function applyIfDoubleNegateReduction(Node $node): Node + { + if (!( + $node->children[0]->children['cond']->flags === flags\UNARY_BOOL_NOT && + $node->children[0]->children['cond']->children['expr']->flags === flags\UNARY_BOOL_NOT + )) { + throw new AssertionError('Failed precondition of ' . __METHOD__); + } + + $new_cond = $node->children[0]->children['cond']->children['expr']->children['expr']; + $new_node = clone($node); + $new_node->flags = 0; + $new_node->children[0] = clone($node->children[0]); + $new_node->children[0]->flags = 0; + $new_node->children[0]->children['cond'] = $new_cond; + + return $new_node; + } + + /** + * Converts while (!!(x)) {Y} -> if (x) {Y} + * This improves Phan's analysis for cases such as `if (!!x)` + */ + private static function applyWhileDoubleNegateReduction(Node $node): Node + { + if (!( + $node->children['cond']->flags === flags\UNARY_BOOL_NOT && + $node->children['cond']->children['expr']->flags === flags\UNARY_BOOL_NOT + )) { + throw new AssertionError('Failed precondition of ' . __METHOD__); + } + + return new Node( + ast\AST_WHILE, + 0, + [ + 'cond' => $node->children['cond']->children['expr']->children['expr'], + 'stmts' => $node->children['stmts'] + ], + $node->lineno + ); + } + + /** + * Converts for (INIT; !!(x); LOOP) {Y} -> if (INIT; x; LOOP) {Y} + * This improves Phan's analysis for cases such as `if (!!x)` + */ + private static function applyForDoubleNegateReduction(Node $node): Node + { + $children = $node->children; + $cond_node_list = $children['cond']->children; + $cond_node = array_pop($cond_node_list); + if (!( + $cond_node->flags === flags\UNARY_BOOL_NOT && + $cond_node->children['expr']->flags === flags\UNARY_BOOL_NOT + )) { + throw new AssertionError('Failed precondition of ' . __METHOD__); + } + $cond_node_list[] = $cond_node->children['expr']->children['expr']; + + $children['cond'] = new ast\Node( + ast\AST_EXPR_LIST, + 0, + $cond_node_list, + $children['cond']->lineno + ); + + return new Node( + ast\AST_FOR, + 0, + $children, + $node->lineno + ); + } + + private static function applyIfNegatedToIfElseReduction(Node $node): Node + { + if (count($node->children) !== 1) { + throw new AssertionError("Expected one child node"); + } + $if_elem = $node->children[0]; + if ($if_elem->children['cond']->flags !== flags\UNARY_BOOL_NOT) { + throw new AssertionError("Expected condition to begin with unary boolean negation operator"); + } + $lineno = $if_elem->lineno; + $new_else_elem = new Node( + ast\AST_IF_ELEM, + 0, + [ + 'cond' => null, + 'stmts' => $if_elem->children['stmts'], + ], + $lineno + ); + $new_if_elem = new Node( + ast\AST_IF_ELEM, + 0, + [ + 'cond' => $if_elem->children['cond']->children['expr'], + 'stmts' => new Node(ast\AST_STMT_LIST, 0, [], $if_elem->lineno), + ], + $lineno + ); + return new Node( + ast\AST_IF, + 0, + [$new_if_elem, $new_else_elem], + $node->lineno + ); + } + + /** + * Recurses on a list of 0 or more catch statements. (as in try/catch) + * Returns an equivalent list of catch AST nodes (or the original if no changes were made) + */ + private static function normalizeCatchesList(Node $catches): Node + { + $list = $catches->children; + $new_list = array_map( + static function (Node $node): Node { + return self::applyToStmts($node); + }, + // @phan-suppress-next-line PhanPartialTypeMismatchArgument should be impossible to be float + $list + ); + if ($new_list === $list) { + return $catches; + } + $new_catches = clone($catches); + $new_catches->children = $new_list; + $new_catches->flags = 0; + return $new_catches; + } + + /** + * Recurses on a try/catch/finally node, applying simplifications(catch/finally are optional) + * Returns an equivalent try/catch/finally node (or the original if no changes were made) + */ + private static function normalizeTryStatement(Node $node): Node + { + $try = $node->children['try']; + $catches = $node->children['catches']; + $finally = $node->children['finally'] ?? null; + $new_try = self::applyToStatementList($try); + $new_catches = $catches ? self::normalizeCatchesList($catches) : $catches; + $new_finally = $finally ? self::applyToStatementList($finally) : $finally; + if ($new_try === $try && $new_catches === $catches && $new_finally === $finally) { + return $node; + } + $new_node = clone($node); + $new_node->children['try'] = $new_try; + $new_node->children['catches'] = $new_catches; + $new_node->children['finally'] = $new_finally; + $new_node->flags = 0; + return $new_node; + } + + /** + * Returns a Node that represents $node after all of the AST simplification steps. + * + * $node is not modified. This will reuse descendant nodes that didn't change. + */ + public static function applyStatic(Node $node): Node + { + $rewriter = new self(); + $nodes = $rewriter->apply($node); + if (count($nodes) !== 1) { + throw new AssertionError("Expected applying simplifier to a statement list would return an array with one statement list"); + } + return $nodes[0]; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/AnalysisVisitor.php b/bundled-libs/phan/phan/src/Phan/AST/AnalysisVisitor.php new file mode 100644 index 000000000..a3c549d51 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/AnalysisVisitor.php @@ -0,0 +1,125 @@ +context = $context; + $this->code_base = $code_base; + } + + /** + * @param string $issue_type + * The type of issue to emit such as Issue::ParentlessClass + * + * @param int $lineno + * The line number where the issue was found + * + * @param int|string|FQSEN|UnionType|Type ...$parameters + * Template parameters for the issue's error message + * + * @see PluginAwarePostAnalysisVisitor::emitPluginIssue if you are using this from a plugin. + */ + protected function emitIssue( + string $issue_type, + int $lineno, + ...$parameters + ): void { + Issue::maybeEmitWithParameters( + $this->code_base, + $this->context, + $issue_type, + $lineno, + $parameters + ); + } + + /** + * @param string $issue_type + * The type of issue to emit such as Issue::ParentlessClass + * + * @param int $lineno + * The line number where the issue was found + * + * @param list $parameters + * Template parameters for the issue's error message + * + * @param ?Suggestion $suggestion + * A suggestion (may be null) + */ + protected function emitIssueWithSuggestion( + string $issue_type, + int $lineno, + array $parameters, + ?Suggestion $suggestion + ): void { + Issue::maybeEmitWithParameters( + $this->code_base, + $this->context, + $issue_type, + $lineno, + $parameters, + $suggestion + ); + } + + /** + * Check if an issue type (different from the one being emitted) should be suppressed. + * + * This is useful for ensuring that TypeMismatchProperty also suppresses PhanPossiblyNullTypeMismatchProperty, + * for example. + */ + protected function shouldSuppressIssue(string $issue_type, int $lineno): bool + { + return Issue::shouldSuppressIssue( + $this->code_base, + $this->context, + $issue_type, + $lineno, + [] + ); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/ArrowFunc.php b/bundled-libs/phan/phan/src/Phan/AST/ArrowFunc.php new file mode 100644 index 000000000..79a71fd25 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/ArrowFunc.php @@ -0,0 +1,121 @@ + maps variable names to the first Node where the variable was used.*/ + private $uses = []; + + private function __construct() + { + } + + /** + * Returns the set of variables used by the arrow func $n + * + * @param Node $n a Node with kind ast\AST_ARROW_FUNC + * @return associative-array + */ + public static function getUses(Node $n): array + { + if ($n->kind !== ast\AST_ARROW_FUNC) { + throw new InvalidArgumentException("Expected node kind AST_ARROW_FUNC but got " . ast\get_kind_name($n->kind)); + } + // @phan-suppress-next-line PhanUndeclaredProperty + return $n->phan_arrow_uses ?? $n->phan_arrow_uses = (new self())->computeUses($n); + } + + /** + * @return array + */ + private function computeUses(Node $n): array + { + $stmts = $n->children['stmts']; + if ($stmts instanceof Node) { // should always be a node + $this->buildUses($stmts); + // Iterate over the AST_PARAM nodes and remove their variables. + // They are variables used within the function, but are not uses from the outer scope. + foreach ($n->children['params']->children ?? [] as $param) { + $name = $param->children['name'] ?? null; + if (\is_string($name)) { + unset($this->uses[$name]); + } + } + } + return $this->uses; + } + + /** + * @param int|string $name the name of the variable being used by this arrow func. + * may need to handle `${'0'}`? + */ + private function recordUse($name, Node $n): void + { + $this->uses[$name] = $this->uses[$name] ?? $n; + } + + private function buildUses(Node $n): void + { + switch ($n->kind) { + case ast\AST_VAR: + $name = $n->children['name']; + if (is_string($name)) { + $this->recordUse($name, $n); + return; + } + break; + case ast\AST_ARROW_FUNC: + foreach (self::getUses($n) as $name => $child_node) { + $this->recordUse($name, $child_node); + } + return; + case ast\AST_CLOSURE: + foreach ($n->children['uses']->children ?? [] as $child_node) { + if (!$child_node instanceof Node) { + continue; + } + $name = $child_node->children['name']; + if (is_string($name)) { + $this->recordUse($name, $child_node); + } + } + return; + case ast\AST_CLASS: + foreach ($n->children['args']->children ?? [] as $child_node) { + if ($child_node instanceof Node) { + $this->buildUses($child_node); + } + } + return; + } + foreach ($n->children as $child_node) { + if ($child_node instanceof Node) { + $this->buildUses($child_node); + } + } + } + + /** + * Record that variable $variable_name exists in the outer scope of the arrow function with node $n + */ + public static function recordVariableExistsInOuterScope(Node $n, string $variable_name): void + { + if ($n->kind !== ast\AST_ARROW_FUNC) { + throw new InvalidArgumentException("Expected node kind AST_ARROW_FUNC but got " . ast\get_kind_name($n->kind)); + } + // @phan-suppress-next-line PhanUndeclaredProperty + $n->phan_arrow_inherited_vars[$variable_name] = true; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/ContextNode.php b/bundled-libs/phan/phan/src/Phan/AST/ContextNode.php new file mode 100644 index 000000000..1c6afcdb7 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/ContextNode.php @@ -0,0 +1,2681 @@ +node */ + private $context; + + /** @var Node|array|bool|string|float|int|bool|null the node which we're requesting information about. */ + private $node; + + /** + * @param CodeBase $code_base The code base within which we're operating + * @param Context $context The context in which we are requesting information about the Node. + * @param Node|array|string|float|int|bool|null $node the node which we're requesting information about. + */ + public function __construct( + CodeBase $code_base, + Context $context, + $node + ) { + $this->code_base = $code_base; + $this->context = $context; + $this->node = $node; + } + + /** + * Get a list of fully qualified names from a node + * + * @return list + * @throws FQSENException if the node has invalid names + * @suppress PhanUnreferencedPublicMethod this used to be used + */ + public function getQualifiedNameList(): array + { + if (!($this->node instanceof Node)) { + return []; + } + + $union_type = UnionType::empty(); + foreach ($this->node->children as $name_node) { + $union_type = $union_type->withUnionType(UnionTypeVisitor::unionTypeFromClassNode( + $this->code_base, + $this->context, + $name_node + )); + } + return \array_map('strval', $union_type->getTypeSet()); + } + + /** + * Get a fully qualified name from a node + * @throws FQSENException if the node is invalid + * @internal TODO: Stop using this + */ + public function getQualifiedName(): string + { + return UnionTypeVisitor::unionTypeFromClassNode( + $this->code_base, + $this->context, + $this->node + )->__toString(); + } + + /** + * Gets the list of possible FQSENs for a trait. + * NOTE: does not validate that it is really used on a trait + * @return list + * @throws FQSENException + */ + public function getTraitFQSENList(): array + { + if (!($this->node instanceof Node)) { + return []; + } + + /** + * @param Node|int|string|float|null $name_node + * @throws FQSENException + */ + $result = []; + foreach ($this->node->children as $name_node) { + $trait_fqsen = (new ContextNode( + $this->code_base, + $this->context, + $name_node + ))->getTraitFQSEN([]); + if ($trait_fqsen) { + // Should never be null but check anyway + // TODO warn + $result[] = $trait_fqsen; + } + } + return $result; + } + + /** + * Gets the FQSEN for a trait. + * NOTE: does not validate that it is really used on a trait + * @param array $adaptations_map + * @return ?FullyQualifiedClassName (If this returns null, the caller is responsible for emitting an issue or falling back) + * @throws FQSENException hopefully impossible + */ + public function getTraitFQSEN(array $adaptations_map): ?FullyQualifiedClassName + { + // TODO: In a subsequent PR, try to make trait analysis work when $adaptations_map has multiple possible traits. + $trait_fqsen_string = $this->getQualifiedName(); + if ($trait_fqsen_string === '') { + if (\count($adaptations_map) === 1) { + // @phan-suppress-next-line PhanPossiblyNonClassMethodCall + return \reset($adaptations_map)->getTraitFQSEN(); + } else { + return null; + } + } + return FullyQualifiedClassName::fromStringInContext( + $trait_fqsen_string, + $this->context + ); + } + + /** + * Get a list of traits adaptations from a node of kind ast\AST_TRAIT_ADAPTATIONS + * (with fully qualified names and `as`/`instead` info) + * + * @param list $trait_fqsen_list TODO: use this for sanity check + * + * @return array maps the lowercase trait fqsen to the corresponding adaptations. + * + * @throws UnanalyzableException (should be caught and emitted as an issue) + */ + public function getTraitAdaptationsMap(array $trait_fqsen_list): array + { + if (!($this->node instanceof Node)) { + return []; + } + + // NOTE: This fetches fully qualified names more than needed, + // but this isn't optimized, since traits aren't frequently used in classes. + + $adaptations_map = []; + foreach ($trait_fqsen_list as $trait_fqsen) { + $adaptations_map[\strtolower($trait_fqsen->__toString())] = new TraitAdaptations($trait_fqsen); + } + + foreach ($this->node->children as $adaptation_node) { + if (!$adaptation_node instanceof Node) { + throw new AssertionError('Expected adaptation_node to be Node'); + } + if ($adaptation_node->kind === ast\AST_TRAIT_ALIAS) { + $this->handleTraitAlias($adaptations_map, $adaptation_node); + } elseif ($adaptation_node->kind === ast\AST_TRAIT_PRECEDENCE) { + $this->handleTraitPrecedence($adaptations_map, $adaptation_node); + } else { + throw new AssertionError("Unknown adaptation node kind " . $adaptation_node->kind); + } + } + return $adaptations_map; + } + + /** + * Handles a node of kind ast\AST_TRAIT_ALIAS, modifying the corresponding TraitAdaptations instance + * @param array $adaptations_map + * @param Node $adaptation_node + */ + private function handleTraitAlias(array $adaptations_map, Node $adaptation_node): void + { + $trait_method_node = $adaptation_node->children['method']; + if (!$trait_method_node instanceof Node) { + throw new AssertionError("Expected node for trait alias"); + } + $trait_original_class_name_node = $trait_method_node->children['class']; + $trait_original_method_name = $trait_method_node->children['method']; + $trait_new_method_name = $adaptation_node->children['alias'] ?? $trait_original_method_name; + if (!\is_string($trait_original_method_name)) { + $this->emitIssue( + Issue::InvalidTraitUse, + $trait_original_class_name_node->lineno ?? 0, + "Expected original method name of a trait use to be a string" + ); + return; + } + if (!\is_string($trait_new_method_name)) { + $this->emitIssue( + Issue::InvalidTraitUse, + $trait_original_class_name_node->lineno ?? 0, + "Expected new method name of a trait use to be a string" + ); + return; + } + try { + $trait_fqsen = (new ContextNode( + $this->code_base, + $this->context, + $trait_original_class_name_node + ))->getTraitFQSEN($adaptations_map); + } catch (FQSENException $e) { + $this->emitIssue( + Issue::InvalidTraitUse, + $trait_original_class_name_node->lineno ?? 0, + $e->getMessage() + ); + return; + } + if ($trait_fqsen === null) { + // TODO: try to analyze this rare special case instead of giving up in a subsequent PR? + // E.g. `use A, B{foo as bar}` is valid PHP, but hard to analyze. + $this->emitIssue( + Issue::AmbiguousTraitAliasSource, + $trait_method_node->lineno ?? 0, + $trait_new_method_name, + $trait_original_method_name, + '[' . implode(', ', \array_map(static function (TraitAdaptations $t): string { + return (string) $t->getTraitFQSEN(); + }, $adaptations_map)) . ']' + ); + return; + } + + $fqsen_key = \strtolower($trait_fqsen->__toString()); + + $adaptations_info = $adaptations_map[$fqsen_key] ?? null; + if ($adaptations_info === null) { + // This will probably correspond to a PHP fatal error, but keep going anyway. + $this->emitIssue( + Issue::RequiredTraitNotAdded, + $trait_original_class_name_node->lineno ?? 0, + $trait_fqsen->__toString() + ); + return; + } + // TODO: Could check for duplicate alias method occurrences, but `php -l` would do that for you in some cases + $adaptations_info->alias_methods[$trait_new_method_name] = new TraitAliasSource($trait_original_method_name, $adaptation_node->lineno ?? 0, $adaptation_node->flags ?? 0); + // Handle `use MyTrait { myMethod as private; }` by skipping the original method. + // TODO: Do this a cleaner way. + if (strcasecmp($trait_new_method_name, $trait_original_method_name) === 0) { + $adaptations_info->hidden_methods[\strtolower($trait_original_method_name)] = true; + } + } + + /** + * @param string|int|float|bool|Type|UnionType|FQSEN ...$parameters + * Template parameters for the issue's error message. + * If these are objects, they should define __toString() + */ + private function emitIssue( + string $issue_type, + int $lineno, + ...$parameters + ): void { + Issue::maybeEmit( + $this->code_base, + $this->context, + $issue_type, + $lineno, + ...$parameters + ); + } + + /** + * Handles a node of kind ast\AST_TRAIT_PRECEDENCE, modifying the corresponding TraitAdaptations instance + * @param array $adaptations_map + * @param Node $adaptation_node + * @throws UnanalyzableException (should be caught and emitted as an issue) + */ + private function handleTraitPrecedence(array $adaptations_map, Node $adaptation_node): void + { + // TODO: Should also verify that the original method exists, in a future PR? + $trait_method_node = $adaptation_node->children['method']; + if (!$trait_method_node instanceof Node) { + throw new AssertionError("Expected node for trait use"); + } + // $trait_chosen_class_name_node = $trait_method_node->children['class']; + $trait_chosen_method_name = $trait_method_node->children['method']; + $trait_chosen_class_name_node = $trait_method_node->children['class']; + if (!is_string($trait_chosen_method_name)) { + $this->emitIssue( + Issue::InvalidTraitUse, + $trait_method_node->lineno ?? 0, + "Expected the insteadof method's name to be a string" + ); + return; + } + + try { + $trait_chosen_fqsen = (new ContextNode( + $this->code_base, + $this->context, + $trait_chosen_class_name_node + ))->getTraitFQSEN($adaptations_map); + } catch (FQSENException $e) { + $this->emitIssue( + Issue::InvalidTraitUse, + $trait_method_node->lineno ?? 0, + $e->getMessage() + ); + return; + } + + + if (!$trait_chosen_fqsen) { + throw new UnanalyzableException( + $trait_chosen_class_name_node, + "This shouldn't happen. Could not determine trait fqsen for trait with higher precedence for method $trait_chosen_method_name" + ); + } + + if (($adaptations_map[\strtolower($trait_chosen_fqsen->__toString())] ?? null) === null) { + // This will probably correspond to a PHP fatal error, but keep going anyway. + $this->emitIssue( + Issue::RequiredTraitNotAdded, + $trait_chosen_class_name_node->lineno ?? 0, + $trait_chosen_fqsen->__toString() + ); + } + + // This is the class which will have the method hidden + foreach ($adaptation_node->children['insteadof']->children as $trait_insteadof_class_name) { + try { + $trait_insteadof_fqsen = (new ContextNode( + $this->code_base, + $this->context, + $trait_insteadof_class_name + ))->getTraitFQSEN($adaptations_map); + } catch (\Exception $_) { + $trait_insteadof_fqsen = null; + } + if (!$trait_insteadof_fqsen) { + throw new UnanalyzableException( + $trait_insteadof_class_name, + "This shouldn't happen. Could not determine trait fqsen for trait with lower precedence for method $trait_chosen_method_name" + ); + } + + $fqsen_key = \strtolower($trait_insteadof_fqsen->__toString()); + + $adaptations_info = $adaptations_map[$fqsen_key] ?? null; + if ($adaptations_info === null) { + // TODO: Make this into an issue type + $this->emitIssue( + Issue::RequiredTraitNotAdded, + $trait_insteadof_class_name->lineno ?? 0, + $trait_insteadof_fqsen->__toString() + ); + continue; + } + $adaptations_info->hidden_methods[strtolower($trait_chosen_method_name)] = true; + } + } + + /** + * @return string + * A variable name associated with the given node + * + * TODO: Deprecate this and use more precise ways to locate the desired element + * TODO: Distinguish between the empty string and the lack of a name + */ + public function getVariableName(): string + { + if (!($this->node instanceof Node)) { + return (string)$this->node; + } + + $node = $this->node; + + while (($node instanceof Node) + && ($node->kind !== ast\AST_VAR) + && ($node->kind !== ast\AST_STATIC) + && ($node->kind !== ast\AST_MAGIC_CONST) + ) { + $node = \reset($node->children); + } + + if (!($node instanceof Node)) { + return (string)$node; + } + + $name_node = $node->children['name'] ?? ''; + if ($name_node === '') { + return ''; + } + + if ($name_node instanceof Node) { + // This is nonsense. Give up, but check if it's a type other than int/string. + // (e.g. to catch typos such as $$this->foo = bar;) + try { + $name_node_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $name_node, true); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + return ''; + } + static $int_or_string_type; + if ($int_or_string_type === null) { + $int_or_string_type = UnionType::fromFullyQualifiedPHPDocString('int|string|null'); + } + if (!$name_node_type->canCastToUnionType($int_or_string_type)) { + $this->emitIssue(Issue::TypeSuspiciousIndirectVariable, $name_node->lineno ?? 0, (string)$name_node_type); + } + + // return empty string on failure. + return (string)$name_node_type->asSingleScalarValueOrNull(); + } + + return (string)$name_node; + } + + // Constants for getClassList() API + public const CLASS_LIST_ACCEPT_ANY = 0; + public const CLASS_LIST_ACCEPT_OBJECT = 1; + public const CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME = 2; + + /** + * @return array{0:UnionType,1:Clazz[]} + * @throws CodeBaseException if $ignore_missing_classes == false + */ + public function getClassListInner(bool $ignore_missing_classes): array + { + $node = $this->node; + if (!($node instanceof Node)) { + if (\is_string($node)) { + return [LiteralStringType::instanceForValue($node, false)->asRealUnionType(), []]; + } + return [UnionType::empty(), []]; + } + $context = $this->context; + $node_id = \spl_object_id($node); + + $cached_result = $context->getCachedClassListOfNode($node_id); + if ($cached_result) { + // About 25% of requests are cache hits + return $cached_result; + } + $code_base = $this->code_base; + try { + $union_type = UnionTypeVisitor::unionTypeFromClassNode( + $code_base, + $context, + $node + ); + } catch (FQSENException $e) { + $this->emitIssue( + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike, + $this->node->lineno ?? $context->getLineNumberStart(), + $e->getFQSEN() + ); + $union_type = UnionType::empty(); + } + if ($union_type->isEmpty()) { + $result = [$union_type, []]; + $context->setCachedClassListOfNode($node_id, $result); + return $result; + } + + $class_list = []; + try { + foreach ($union_type->asClassList( + $code_base, + $context + ) as $clazz) { + $class_list[] = $clazz; + } + $result = [$union_type, $class_list]; + $context->setCachedClassListOfNode($node_id, $result); + return $result; + } catch (CodeBaseException $e) { + if ($ignore_missing_classes) { + // swallow it + // TODO: Is it appropriate to return class_list + return [$union_type, $class_list]; + } + + throw $e; + } + } + /** + * @param bool $ignore_missing_classes + * If set to true, missing classes will be ignored and + * exceptions will be inhibited + * + * @param int $expected_type_categories + * If set to CLASS_LIST_ACCEPT_ANY, this will not warn. + * If set to CLASS_LIST_ACCEPT_OBJECT, this will warn if the inferred type is exclusively non-object types. This will not add classes based on LiteralStringType + * If set to CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME, this will warn if the inferred type is exclusively non-object and non-string types. + * + * @param ?string $custom_issue_type + * If this exists, emit the given issue type (passing in the class's union type as format arg) instead of the default issue type. + * The issue type passed in must have exactly one template string parameter (e.g. {CLASS}, {TYPE}) + * + * @return list + * A list of classes representing the non-native types + * associated with the given node + * + * @throws CodeBaseException + * An exception is thrown if a non-native type does not have + * an associated class + * + * @throws IssueException + * An exception is thrown if fetching the requested class name + * would trigger an issue (e.g. Issue::ContextNotObject) + */ + public function getClassList( + bool $ignore_missing_classes = false, + int $expected_type_categories = self::CLASS_LIST_ACCEPT_ANY, + string $custom_issue_type = null, + bool $warn_if_wrong_type = true + ): array { + [$union_type, $class_list] = $this->getClassListInner($ignore_missing_classes); + if ($union_type->isEmpty()) { + return []; + } + + // TODO: Should this check that count($class_list) > 0 instead? Or just always check? + if (\count($class_list) === 0) { + if (!$union_type->hasTypeMatchingCallback(function (Type $type) use ($expected_type_categories): bool { + if ($this->node instanceof Node) { + if ($this->node->kind === ast\AST_NAME) { + return $type->isObjectWithKnownFQSEN(); + } + if ($this->node->kind === ast\AST_TYPE) { + return $this->node->flags !== ast\flags\TYPE_STATIC; + } + } + return $type->isObject() || ($type instanceof MixedType) || ($expected_type_categories !== self::CLASS_LIST_ACCEPT_OBJECT && $type instanceof StringType); + })) { + if ($warn_if_wrong_type) { + if ($custom_issue_type === Issue::TypeExpectedObjectPropAccess) { + if ($union_type->isType(NullType::instance(false))) { + $custom_issue_type = Issue::TypeExpectedObjectPropAccessButGotNull; + } + } + $this->emitIssue( + $custom_issue_type ?? ($expected_type_categories !== self::CLASS_LIST_ACCEPT_OBJECT ? Issue::TypeExpectedObjectOrClassName : Issue::TypeExpectedObject), + $this->node->lineno ?? $this->context->getLineNumberStart(), + ASTReverter::toShortString($this->node), + (string)$union_type->asNonLiteralType() + ); + } + } elseif ($expected_type_categories !== self::CLASS_LIST_ACCEPT_OBJECT) { + foreach ($union_type->getTypeSet() as $type) { + if ($type instanceof LiteralStringType) { + $type_value = $type->getValue(); + try { + $fqsen = FullyQualifiedClassName::fromFullyQualifiedString($type_value); + if ($this->code_base->hasClassWithFQSEN($fqsen)) { + $class_list[] = $this->code_base->getClassByFQSEN($fqsen); + } elseif ($warn_if_wrong_type) { + $this->emitIssue( + Issue::UndeclaredClass, + $this->node->lineno ?? $this->context->getLineNumberStart(), + (string)$fqsen + ); + } + } catch (FQSENException $e) { + if ($warn_if_wrong_type) { + $this->emitIssue( + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike, + $this->node->lineno ?? $this->context->getLineNumberStart(), + $e->getFQSEN() + ); + } + } + } + } + } + } + + return $class_list; + } + + /** + * @param Node|string $method_name + * Either then name of the method or a node that + * produces the name of the method. + * + * @param bool $is_static + * Set to true if this is a static method call + * + * @param bool $is_direct @phan-mandatory-param + * Set to true if this is directly invoking the method (guaranteed not to be special syntax) + * + * @param bool $is_new_expression + * Set to true if this is (new (expr)()) + * + * @return Method + * A method with the given name on the class referenced + * from the given node + * + * @throws NodeException + * An exception is thrown if we can't understand the node + * + * @throws CodeBaseException + * An exception is thrown if we can't find the given + * method + * + * @throws IssueException + */ + public function getMethod( + $method_name, + bool $is_static, + bool $is_direct = false, + bool $is_new_expression = false + ): Method { + + if ($method_name instanceof Node) { + $method_name_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $method_name + ); + foreach ($method_name_type->getTypeSet() as $type) { + if ($type instanceof LiteralStringType) { + // TODO: Warn about nullable? + return $this->getMethod($type->getValue(), $is_static, $is_direct, $is_new_expression); + } + } + // The method_name turned out to be a variable. + // There isn't much we can do to figure out what + // it's referring to. + throw new NodeException( + $method_name, + "Unexpected method node" + ); + } + + if (!\is_string($method_name)) { + throw new AssertionError("Method name must be a string. Found non-string in context."); + } + + $node = $this->node; + if (!($node instanceof Node)) { + throw new AssertionError('$node must be a node'); + } + + try { + // Fetch the list of valid classes, and warn about any undefined classes. + // (We have more specific issue types such as PhanNonClassMethodCall below, don't emit PhanTypeExpected*) + $class_list = (new ContextNode( + $this->code_base, + $this->context, + $node->children['expr'] + ?? $node->children['class'] + ))->getClassList( + false, + $is_static || $is_new_expression ? self::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME : self::CLASS_LIST_ACCEPT_OBJECT, + null, + $is_new_expression // emit warnings about the class if this is for `new $className` + ); + } catch (CodeBaseException $exception) { + $exception_fqsen = $exception->getFQSEN(); + throw new IssueException( + Issue::fromType(Issue::UndeclaredClassMethod)( + $this->context->getFile(), + $node->lineno, + [$method_name, (string)$exception_fqsen], + ($exception_fqsen instanceof FullyQualifiedClassName + ? IssueFixSuggester::suggestSimilarClassForMethod($this->code_base, $this->context, $exception_fqsen, $method_name, $is_static) + : null) + ) + ); + } + + // If there were no classes on the left-type, figure + // out what we were trying to call the method on + // and send out an error. + if (\count($class_list) === 0) { + try { + $union_type = UnionTypeVisitor::unionTypeFromClassNode( + $this->code_base, + $this->context, + $node->children['expr'] + ?? $node->children['class'] + ); + } catch (FQSENException $e) { + throw new IssueException( + Issue::fromType($e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike)( + $this->context->getFile(), + $node->lineno, + [$e->getFQSEN()] + ) + ); + } + + if ($union_type->isDefinitelyUndefined() + || (!$union_type->isEmpty() + && $union_type->isNativeType() + && !$union_type->hasTypeMatchingCallback(static function (Type $type): bool { + return !$type->isNullableLabeled() && ($type instanceof MixedType || $type instanceof ObjectType); + }) + // reject `$stringVar->method()` but not `$stringVar::method()` and not (`new $stringVar()` + && !(($is_static || $is_new_expression) && $union_type->hasNonNullStringType()) + && !( + Config::get_null_casts_as_any_type() + && $union_type->hasType(NullType::instance(false)) + )) + ) { + throw new IssueException( + Issue::fromType(Issue::NonClassMethodCall)( + $this->context->getFile(), + $node->lineno, + [ $method_name, (string)$union_type ] + ) + ); + } + + throw new NodeException( + $node, + "Can't figure out method call for $method_name" + ); + } + $class_without_method = null; + $method = null; + $call_method = null; + + // Hunt to see if any of them have the method we're + // looking for + foreach ($class_list as $class) { + if ($class->hasMethodWithName($this->code_base, $method_name, $is_direct)) { + if ($method) { + // TODO: Could favor the most generic subclass in a union type + continue; + } + $method = $class->getMethodByName($this->code_base, $method_name); + if ($method->hasTemplateType()) { + try { + $method = $method->resolveTemplateType( + $this->code_base, + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr'] ?? $node->children['class']) + ); + } catch (RecursionDepthException $_) { + } + } + } elseif (!$is_static && $class->allowsCallingUndeclaredInstanceMethod($this->code_base)) { + $call_method = $class->getCallMethod($this->code_base); + } elseif ($is_static && $class->allowsCallingUndeclaredStaticMethod($this->code_base)) { + $call_method = $class->getCallStaticMethod($this->code_base); + } else { + $class_without_method = $class->getFQSEN(); + } + } + if (!$method || ($is_direct && $method->isFakeConstructor())) { + $method = $call_method; + } + if ($method) { + if ($class_without_method && Config::get_strict_method_checking()) { + $this->emitIssue( + Issue::PossiblyUndeclaredMethod, + $node->lineno, + $method_name, + implode('|', \array_map(static function (Clazz $class): string { + return $class->getFQSEN()->__toString(); + }, $class_list)), + $class_without_method + ); + } + return $method; + } + + $first_class = $class_list[0]; + + // Figure out an FQSEN for the method we couldn't find + $method_fqsen = FullyQualifiedMethodName::make( + $first_class->getFQSEN(), + $method_name + ); + + if ($is_static) { + throw new IssueException( + Issue::fromType(Issue::UndeclaredStaticMethod)( + $this->context->getFile(), + $node->lineno, + [ (string)$method_fqsen ], + IssueFixSuggester::suggestSimilarMethod($this->code_base, $this->context, $first_class, $method_name, $is_static) + ) + ); + } + + throw new IssueException( + Issue::fromType(Issue::UndeclaredMethod)( + $this->context->getFile(), + $node->lineno, + [ (string)$method_fqsen ], + IssueFixSuggester::suggestSimilarMethod($this->code_base, $this->context, $first_class, $method_name, $is_static) + ) + ); + } + + /** + * Yields a list of FunctionInterface objects for the 'expr' of an AST_CALL. + * @return iterable + */ + public function getFunctionFromNode(bool $return_placeholder_for_undefined = false): iterable + { + $expression = $this->node; + if (!($expression instanceof Node)) { + if (!\is_string($expression)) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeInvalidCallable, + $this->context->getLineNumberStart(), + (string)$expression + ); + } + // TODO: this might need to account for 'myFunction'() + return []; + } + if ($expression->kind === ast\AST_NAME) { + $name = $expression->children['name']; + try { + return [ + $this->getFunction($name, false, $return_placeholder_for_undefined), + ]; + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (FQSENException $exception) { + Issue::maybeEmit( + $this->code_base, + $this->context, + $exception instanceof EmptyFQSENException ? Issue::EmptyFQSENInCallable : Issue::InvalidFQSENInCallable, + $expression->lineno, + $exception->getFQSEN() + ); + } + return []; + } + // The least common case: A dynamic function call such as $x(), (self::$x)(), etc. + return $this->getFunctionLikeFromDynamicExpression(); + } + + /** + * Yields a list of FunctionInterface objects for the 'expr' of an AST_CALL. + * Precondition: expr->kind !== ast\AST_NAME + * + * @return \Generator + */ + private function getFunctionLikeFromDynamicExpression(): \Generator + { + $code_base = $this->code_base; + $context = $this->context; + $expression = $this->node; + $union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expression)->withStaticResolvedInContext($context); + if ($union_type->isEmpty()) { + return; + } + + $has_type = false; + foreach ($union_type->getTypeSet() as $type) { + $func = $type->asFunctionInterfaceOrNull($code_base, $context); + if ($func) { + yield $func; + $has_type = true; + } + } + if (!$has_type) { + if (!$union_type->hasPossiblyCallableType()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeInvalidCallable, + $expression->lineno ?? $context->getLineNumberStart(), + $union_type + ); + return; + } + } + if (Config::get_strict_method_checking() && $union_type->containsDefiniteNonCallableType()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypePossiblyInvalidCallable, + $expression->lineno ?? $context->getLineNumberStart(), + $union_type + ); + } + } + + /** + * @throws IssueException for PhanUndeclaredFunction to be caught and reported by the caller + */ + private function returnStubOrThrowUndeclaredFunctionIssueException( + FullyQualifiedFunctionName $function_fqsen, + bool $suggest_in_global_namespace, + FullyQualifiedFunctionName $namespaced_function_fqsen = null, + bool $return_placeholder_for_undefined = false + ): Func { + if ($return_placeholder_for_undefined) { + $functions = $this->code_base->getPlaceholdersForUndeclaredFunction($function_fqsen); + Issue::maybeEmitWithParameters( + $this->code_base, + $this->context, + Issue::UndeclaredFunction, + $this->node->lineno ?? $this->context->getLineNumberStart(), + [ "$function_fqsen()" ], + IssueFixSuggester::suggestSimilarGlobalFunction($this->code_base, $this->context, $namespaced_function_fqsen ?? $function_fqsen, $suggest_in_global_namespace) + ); + if ($functions) { + return $functions[0]; + } + } + throw new IssueException( + Issue::fromType(Issue::UndeclaredFunction)( + $this->context->getFile(), + $this->node->lineno ?? $this->context->getLineNumberStart(), + [ "$function_fqsen()" ], + IssueFixSuggester::suggestSimilarGlobalFunction($this->code_base, $this->context, $namespaced_function_fqsen ?? $function_fqsen, $suggest_in_global_namespace) + ) + ); + } + + /** + * @param string $function_name + * The name of the function we'd like to look up + * + * @param bool $is_function_declaration + * This must be set to true if we're getting a function + * that is being declared and false if we're getting a + * function being called. + * + * @param bool $return_placeholder_for_undefined + * When this is true, Phan will create a placeholder + * for undefined functions so that argument counts and + * types can be checked. + * + * @return FunctionInterface + * A method with the given name in the given context + * + * @throws IssueException + * An exception is thrown if we can't find the given + * function + * + * @throws FQSENException + * An exception is thrown if the FQSEN being requested + * was determined but was invalid/empty + */ + public function getFunction( + string $function_name, + bool $is_function_declaration = false, + bool $return_placeholder_for_undefined = false + ): FunctionInterface { + + $node = $this->node; + if (!($node instanceof Node)) { + throw new AssertionError('$this->node must be a node'); + } + $code_base = $this->code_base; + $context = $this->context; + $namespace = $context->getNamespace(); + $flags = $node->flags; + // TODO: support namespace aliases for functions + if ($is_function_declaration) { + $function_fqsen = FullyQualifiedFunctionName::make($namespace, $function_name); + if ($code_base->hasFunctionWithFQSEN($function_fqsen)) { + return $code_base->getFunctionByFQSEN($function_fqsen); + } + } elseif (($flags & ast\flags\NAME_RELATIVE) !== 0) { + // For relative functions (e.g. namespace\foo()) + $function_fqsen = FullyQualifiedFunctionName::make($namespace, $function_name); + if (!$code_base->hasFunctionWithFQSEN($function_fqsen)) { + return $this->returnStubOrThrowUndeclaredFunctionIssueException($function_fqsen, false, null, $return_placeholder_for_undefined); + } + return $code_base->getFunctionByFQSEN($function_fqsen); + } else { + if (($flags & ast\flags\NAME_NOT_FQ) !== 0) { + if ($context->hasNamespaceMapFor(\ast\flags\USE_FUNCTION, $function_name)) { + // If we already have `use function function_name;` + $function_fqsen = $context->getNamespaceMapFor(\ast\flags\USE_FUNCTION, $function_name); + if (!($function_fqsen instanceof FullyQualifiedFunctionName)) { + throw new AssertionError("Expected to fetch a fully qualified function name for this namespace use"); + } + + // Make sure the method we're calling actually exists + if (!$code_base->hasFunctionWithFQSEN($function_fqsen)) { + // The FQSEN from 'use MyNS\function_name;' was the only possible fqsen for that function. + return $this->returnStubOrThrowUndeclaredFunctionIssueException($function_fqsen, false, null, $return_placeholder_for_undefined); + } + + return $code_base->getFunctionByFQSEN($function_fqsen); + } + // For relative and non-fully qualified functions (e.g. namespace\foo(), foo()) + $function_fqsen = FullyQualifiedFunctionName::make($namespace, $function_name); + + if ($code_base->hasFunctionWithFQSEN($function_fqsen)) { + return $code_base->getFunctionByFQSEN($function_fqsen); + } + if ($namespace === '' || \strpos($function_name, '\\') !== false) { + return $this->returnStubOrThrowUndeclaredFunctionIssueException($function_fqsen, false, null, $return_placeholder_for_undefined); + } + // If it doesn't exist in the local namespace, try it + // in the global namespace + } + $function_fqsen = + FullyQualifiedFunctionName::make( + '', + $function_name + ); + } + + // Make sure the method we're calling actually exists + if (!$code_base->hasFunctionWithFQSEN($function_fqsen)) { + $not_fully_qualified = (bool)($flags & ast\flags\NAME_NOT_FQ); + return $this->returnStubOrThrowUndeclaredFunctionIssueException( + $function_fqsen, + $not_fully_qualified, + $not_fully_qualified ? FullyQualifiedFunctionName::make($namespace, $function_name) : $function_fqsen, + $return_placeholder_for_undefined + ); + } + + return $code_base->getFunctionByFQSEN($function_fqsen); + } + + /** + * @return Variable + * A variable in scope. + * + * @throws NodeException + * An exception is thrown if we can't understand the node + * + * @throws IssueException + * An IssueException is thrown if the variable doesn't + * exist + */ + public function getVariable(): Variable + { + $node = $this->node; + if (!($node instanceof Node)) { + throw new AssertionError('$this->node must be a node'); + } + + // Get the name of the variable + $variable_name = $this->getVariableName(); + + if ($variable_name === '') { + throw new NodeException( + $node, + "Variable name not found" + ); + } + + // Check to see if the variable exists in this scope + if (!$this->context->getScope()->hasVariableWithName($variable_name)) { + if (Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())) { + // We return a clone of the global or superglobal variable + // that can't be used to influence the type of that superglobal in other files. + return new Variable( + $this->context, + $variable_name, + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name), + 0 + ); + } + + throw new IssueException( + Issue::fromType(Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name))( + $this->context->getFile(), + $node->lineno, + [ $variable_name ], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name) + ) + ); + } + + return $this->context->getScope()->getVariableByName( + $variable_name + ); + } + + /** + * @return Variable + * A variable in scope + * + * @throws NodeException + * An exception is thrown if we can't understand the node + * + * @throws IssueException + * An IssueException is thrown if the variable doesn't + * exist + */ + public function getVariableStrict(): Variable + { + $node = $this->node; + if (!($node instanceof Node)) { + throw new AssertionError('$this->node must be a node'); + } + + if ($node->kind === ast\AST_VAR) { + $variable_name = $node->children['name']; + + if (!is_string($variable_name)) { + throw new NodeException( + $node, + "Variable name not found" + ); + } + + // Check to see if the variable exists in this scope + $scope = $this->context->getScope(); + if (!$scope->hasVariableWithName($variable_name)) { + if (Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())) { + // We return a clone of the global or superglobal variable + // that can't be used to influence the type of that superglobal in other files. + return new Variable( + $this->context, + $variable_name, + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name), + 0 + ); + } + + throw new IssueException( + Issue::fromType(Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name))( + $this->context->getFile(), + $node->lineno, + [ $variable_name ], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name) + ) + ); + } + + return $scope->getVariableByName( + $variable_name + ); + } + throw new NodeException($node, 'Not a variable node'); + } + + /** + * @return Variable + * A variable in scope or a new variable + * + * @throws NodeException + * An exception is thrown if we can't understand the node + * + * @unused + * @suppress PhanUnreferencedPublicMethod + * @see self::getOrCreateVariableForReferenceParameter() - That is probably what you want instead. + */ + public function getOrCreateVariable(): Variable + { + try { + return $this->getVariable(); + } catch (IssueException $_) { + // Swallow it + } + + $node = $this->node; + if (!($node instanceof Node)) { + throw new AssertionError('$this->node must be a node'); + } + + // Create a new variable + $variable = Variable::fromNodeInContext( + $node, + $this->context, + $this->code_base, + false + ); + + $this->context->addScopeVariable($variable); + + return $variable; + } + + /** + * @param Parameter $parameter the parameter types inferred from combination of real and union type + * + * @param ?Parameter $real_parameter the real parameter type from the type signature + * + * @return Variable + * A variable in scope for the argument to that reference parameter, or a new variable + * + * @throws NodeException + * An exception is thrown if we can't understand the node + */ + public function getOrCreateVariableForReferenceParameter(Parameter $parameter, ?Parameter $real_parameter): Variable + { + // Return the original variable if it existed + try { + $variable = $this->getVariable(); + $union_type = $variable->getUnionType(); + if ($union_type->isPossiblyUndefined()) { + $variable->setUnionType($union_type->convertUndefinedToNullable()); + } + return $variable; + } catch (IssueException $_) { + // Swallow exceptions fetching the variable + } + // Create a new variable, and set its union type to null if that wouldn't create false positives. + + $node = $this->node; + if (!($node instanceof Node)) { + throw new AssertionError('$this->node must be a node'); + } + + // Create a new variable + $variable = Variable::fromNodeInContext( + $node, + $this->context, + $this->code_base, + false + ); + if ($parameter->getReferenceType() === Parameter::REFERENCE_READ_WRITE || + ($real_parameter && !$real_parameter->getNonVariadicUnionType()->containsNullableOrIsEmpty())) { + static $null_type = null; + if ($null_type === null) { + $null_type = NullType::instance(false)->asPHPDocUnionType(); + } + // If this is a variable that is both read and written, + // then set the previously undefined variable type to null instead so we can type check it + // (e.g. arguments to array_shift()) + // + // Also, if this has a real type signature that would make PHP throw a TypeError when passed null, then set this to null so the type checker will emit a warning (#1344) + // + // (TODO: read/writeable is currently only possible to annotate for internal functions in FunctionSignatureMap.php), + + // TODO: How should this handle variadic references? + $variable->setUnionType($null_type); + } + + $this->context->addScopeVariable($variable); + + return $variable; + } + + /** + * @param bool $is_static + * True if we're looking for a static property, + * false if we're looking for an instance property. + * + * @return Property + * Phan's representation of a property declaration. + * + * @throws NodeException + * An exception is thrown if we can't understand the node + * + * @throws IssueException + * An exception is thrown if we can't find the given + * class or if we don't have access to the property (its + * private or protected) + * or if the property is static and missing. + * + * @throws UnanalyzableException + * An exception is thrown if we hit a construct in which + * we can't determine if the property exists or not + */ + public function getProperty( + bool $is_static, + bool $is_known_assignment = false + ): Property { + $node = $this->node; + + if (!($node instanceof Node)) { + throw new AssertionError('$this->node must be a node'); + } + + $property_name = $node->children['prop']; + + // Give up for things like C::$prop_name + if (!\is_string($property_name)) { + if ($property_name instanceof Node) { + $property_name = UnionTypeVisitor::anyStringLiteralForNode($this->code_base, $this->context, $property_name); + } else { + $property_name = (string)$property_name; + } + if (!\is_string($property_name)) { + throw $this->createExceptionForInvalidPropertyName($node, $is_static); + } + } + + $class_fqsen = null; + + try { + $expected_type_categories = $is_static ? self::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME : self::CLASS_LIST_ACCEPT_OBJECT; + $expected_issue = $is_static ? Issue::TypeExpectedObjectStaticPropAccess : Issue::TypeExpectedObjectPropAccess; + $class_list = (new ContextNode( + $this->code_base, + $this->context, + $node->children['expr'] ?? + $node->children['class'] + ))->getClassList(false, $expected_type_categories, $expected_issue); + } catch (CodeBaseException $exception) { + $exception_fqsen = $exception->getFQSEN(); + if ($exception_fqsen instanceof FullyQualifiedClassName) { + throw new IssueException( + Issue::fromType($is_static ? Issue::UndeclaredClassStaticProperty : Issue::UndeclaredClassProperty)( + $this->context->getFile(), + $node->lineno, + [ $property_name, $exception_fqsen ], + IssueFixSuggester::suggestSimilarClassForGenericFQSEN($this->code_base, $this->context, $exception_fqsen) + ) + ); + } + // TODO: Is this ever used? The undeclared property issues should instead be caused by the hasPropertyWithFQSEN checks below. + if ($is_static) { + throw new IssueException( + Issue::fromType(Issue::UndeclaredStaticProperty)( + $this->context->getFile(), + $node->lineno, + [ $property_name, (string)$exception->getFQSEN() ] + ) + ); + } else { + throw new IssueException( + Issue::fromType(Issue::UndeclaredProperty)( + $this->context->getFile(), + $node->lineno, + [ "{$exception->getFQSEN()}->$property_name" ] + ) + ); + } + } + + $class_without_property = null; + $property = null; + foreach ($class_list as $class) { + $class_fqsen = $class->getFQSEN(); + + // Keep hunting if this class doesn't have the given + // property + if (!$class->hasPropertyWithName( + $this->code_base, + $property_name + )) { + // (if fetching an instance property) + // If there's a getter on properties then all + // bets are off. However, @phan-forbid-undeclared-magic-properties + // will make this method analyze the code as if all properties were declared or had @property annotations. + if (!$is_static && $class->hasGetMethod($this->code_base) && !$class->getForbidUndeclaredMagicProperties($this->code_base)) { + throw new UnanalyzableMagicPropertyException( + $node, + $class, + $property_name, + "Can't determine if property {$property_name} exists in class {$class->getFQSEN()} with __get defined" + ); + } + + $class_without_property = $class; + continue; + } + if ($property) { + continue; + } + + $property = $class->getPropertyByNameInContext( + $this->code_base, + $property_name, + $this->context, + $is_static, + $node, + $is_known_assignment + ); + + if ($property->isDeprecated()) { + $this->emitIssue( + Issue::DeprecatedProperty, + $node->lineno, + $property->getRepresentationForIssue(), + $property->getFileRef()->getFile(), + $property->getFileRef()->getLineNumberStart(), + $property->getDeprecationReason() + ); + } + + if ($property->isNSInternal($this->code_base) + && !$property->isNSInternalAccessFromContext( + $this->code_base, + $this->context + ) + ) { + $this->emitIssue( + Issue::AccessPropertyInternal, + $node->lineno, + $property->getRepresentationForIssue(), + $property->getElementNamespace() ?: '\\', + $property->getFileRef()->getFile(), + $property->getFileRef()->getLineNumberStart(), + $this->context->getNamespace() ?: '\\' + ); + } + } + if (!$is_static && Config::get_strict_object_checking() && + !($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF)) { + $union_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + $invalid = UnionType::empty(); + foreach ($union_type->getTypeSet() as $type) { + if (!$type->isPossiblyObject()) { + $invalid = $invalid->withType($type); + } elseif ($type->isNullableLabeled()) { + $invalid = $invalid->withType(NullType::instance(false)); + } + } + if (!$invalid->isEmpty()) { + if ($node->flags & PhanAnnotationAdder::FLAG_IGNORE_NULLABLE) { + $invalid = $invalid->nonNullableClone(); + } + if (!$invalid->isEmpty()) { + foreach ($invalid->getTypeset() as $type) { + if (!$type->isNullableLabeled()) { + continue; + } + $this->emitIssue( + Issue::PossiblyUndeclaredProperty, + $node->lineno, + $property_name, + $union_type, + $invalid + ); + if ($property) { + return $property; + } + break; + } + } + } + } + if ($property) { + if ($class_without_property && Config::get_strict_object_checking() && + !($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF)) { + $this->emitIssue( + Issue::PossiblyUndeclaredProperty, + $node->lineno, + $property_name, + UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] ?? $node->children['class'] + ), + $class_without_property->getFQSEN() + ); + } + return $property; + } + + // Since we didn't find the property on any of the + // possible classes, check for classes with dynamic + // properties + if (!$is_static) { + foreach ($class_list as $class) { + if (Config::getValue('allow_missing_properties') + || $class->hasDynamicProperties($this->code_base) + ) { + return $class->getPropertyByNameInContext( + $this->code_base, + $property_name, + $this->context, + $is_static, + $node, + $is_known_assignment + ); + } + } + } + + /* + $std_class_fqsen = + FullyQualifiedClassName::getStdClassFQSEN(); + + // If missing properties are cool, create it on + // the first class we found + if (!$is_static && ($class_fqsen && ($class_fqsen === $std_class_fqsen)) + || Config::getValue('allow_missing_properties') + ) { + if (count($class_list) > 0) { + $class = $class_list[0]; + return $class->getPropertyByNameInContext( + $this->code_base, + $property_name, + $this->context, + $is_static, + $node + ); + } + } + */ + + // If the class isn't found, we'll get the message elsewhere + if ($class_fqsen) { + $suggestion = null; + if (isset($class)) { + $suggestion = IssueFixSuggester::suggestSimilarProperty($this->code_base, $this->context, $class, $property_name, $is_static); + } + + if ($is_static) { + throw new IssueException( + Issue::fromType(Issue::UndeclaredStaticProperty)( + $this->context->getFile(), + $node->lineno, + [ $property_name, (string)$class_fqsen ], + $suggestion + ) + ); + } else { + throw new IssueException( + Issue::fromType(Issue::UndeclaredProperty)( + $this->context->getFile(), + $node->lineno, + [ "$class_fqsen->$property_name" ], + $suggestion + ) + ); + } + } + + throw new NodeException( + $node, + "Cannot figure out property from {$this->context}" + ); + } + + /** + * @return NodeException|IssueException + */ + private function createExceptionForInvalidPropertyName(Node $node, bool $is_static): Exception + { + $property_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['prop']); + if ($property_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) { + // If we know it can be a string, throw a NodeException instead of a specific issue + return new NodeException( + $node, + "Cannot figure out property name" + ); + } + return new IssueException( + Issue::fromType($is_static ? Issue::TypeInvalidStaticPropertyName : Issue::TypeInvalidPropertyName)( + $this->context->getFile(), + $node->lineno, + [$property_type] + ) + ); + } + + /** + * @return Property + * A declared property or a newly created dynamic property. + * + * @throws NodeException + * An exception is thrown if we can't understand the node + * + * @throws UnanalyzableException + * An exception is thrown if we can't find the given + * class + * + * @throws CodeBaseException + * An exception is thrown if we can't find the given + * class + * + * @throws IssueException + * An exception is thrown if $is_static, but the property doesn't exist. + */ + public function getOrCreateProperty( + string $property_name, + bool $is_static + ): Property { + + try { + return $this->getProperty($is_static); + } catch (IssueException $exception) { + if ($is_static) { + throw $exception; + } + // TODO: log types of IssueException that aren't for undeclared properties? + // (in another PR) + + // For instance properties, ignore it, + // because we'll create our own property + // @phan-suppress-next-line PhanPluginDuplicateCatchStatementBody + } catch (UnanalyzableException $exception) { + if ($is_static) { + throw $exception; + } + // For instance properties, ignore it, + // because we'll create our own property + } + + $node = $this->node; + if (!($node instanceof Node)) { + throw new AssertionError('$this->node must be a node'); + } + + try { + $expected_type_categories = $is_static ? self::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME : self::CLASS_LIST_ACCEPT_OBJECT; + $expected_issue = $is_static ? Issue::TypeExpectedObjectStaticPropAccess : Issue::TypeExpectedObjectPropAccess; + $class_list = (new ContextNode( + $this->code_base, + $this->context, + $node->children['expr'] ?? null + ))->getClassList(false, $expected_type_categories, $expected_issue); + } catch (CodeBaseException $exception) { + throw new IssueException( + Issue::fromType(Issue::UndeclaredClassReference)( + $this->context->getFile(), + $node->lineno, + [ $exception->getFQSEN() ] + ) + ); + } + + $class = \reset($class_list); + + if (!($class instanceof Clazz)) { + // empty list + throw new UnanalyzableException( + $node, + "Could not get class name from node" + ); + } + + $flags = 0; + if ($node->kind === ast\AST_STATIC_PROP) { + $flags |= ast\flags\MODIFIER_STATIC; + } + + $property_fqsen = FullyQualifiedPropertyName::make( + $class->getFQSEN(), + $property_name + ); + + // Otherwise, we'll create it + $property = new Property( + $this->context, + $property_name, + UnionType::empty(), + $flags, + $property_fqsen, + UnionType::empty() + ); + + $class->addProperty($this->code_base, $property, None::instance()); + + return $property; + } + + /** + * @return GlobalConstant + * Get the (non-class) constant associated with this node + * in this context + * + * @throws IssueException + * should be emitted by the caller if caught. + */ + public function getConst(): GlobalConstant + { + $node = $this->node; + if (!$node instanceof Node) { + throw new AssertionError('$node must be a node'); + } + + if ($node->kind !== ast\AST_CONST) { + throw new AssertionError("Node must be of type ast\AST_CONST"); + } + + $constant_name = $node->children['name']->children['name'] ?? null; + if (!\is_string($constant_name)) { + throw new AssertionError("Can't determine constant name"); + } + + $code_base = $this->code_base; + + $constant_name_lower = \strtolower($constant_name); + if ($constant_name_lower === 'true' || $constant_name_lower === 'false' || $constant_name_lower === 'null') { + return $code_base->getGlobalConstantByFQSEN( + // @phan-suppress-next-line PhanThrowTypeMismatchForCall + FullyQualifiedGlobalConstantName::fromFullyQualifiedString( + $constant_name_lower + ) + ); + } + + $context = $this->context; + $flags = $node->children['name']->flags ?? 0; + try { + if (($flags & ast\flags\NAME_RELATIVE) !== 0) { + $fqsen = FullyQualifiedGlobalConstantName::make($context->getNamespace(), $constant_name); + } elseif (($flags & ast\flags\NAME_NOT_FQ) !== 0) { + if ($context->hasNamespaceMapFor(\ast\flags\USE_CONST, $constant_name)) { + // If we already have `use const CONST_NAME;` + $fqsen = $context->getNamespaceMapFor(\ast\flags\USE_CONST, $constant_name); + if (!($fqsen instanceof FullyQualifiedGlobalConstantName)) { + throw new AssertionError("expected to fetch a fully qualified const name for this namespace use"); + } + + // the fqsen from 'use myns\const_name;' was the only possible fqsen for that const. + } else { + $fqsen = FullyQualifiedGlobalConstantName::make( + $context->getNamespace(), + $constant_name + ); + + if (!$code_base->hasGlobalConstantWithFQSEN($fqsen)) { + if (\strpos($constant_name, '\\') !== false) { + $this->throwUndeclaredGlobalConstantIssueException($code_base, $context, $fqsen); + } + // @phan-suppress-next-line PhanAccessClassConstantInternal + $constant_exists_variable = $context->getScope()->getVariableByNameOrNull(ConditionVisitor::CONSTANT_EXISTS_PREFIX . \ltrim($fqsen->__toString(), '\\')); + if ($constant_exists_variable && + !$constant_exists_variable->getUnionType()->isPossiblyUndefined() && + $constant_exists_variable->getFileRef()->getFile() === $context->getFile()) { + return $this->createPlaceholderGlobalConstant($fqsen); + } + $fqsen = FullyQualifiedGlobalConstantName::fromFullyQualifiedString( + $constant_name + ); + } + } + } else { + // This is a fully qualified constant + $fqsen = FullyQualifiedGlobalConstantName::fromFullyQualifiedString( + $constant_name + ); + } + } catch (FQSENException $e) { + throw new AssertionError("Impossible FQSENException: " . $e->getMessage()); + } + // This is either a fully qualified constant, + // or a relative constant for which nothing was found in the namespace + + if (!$code_base->hasGlobalConstantWithFQSEN($fqsen)) { + // @phan-suppress-next-line PhanAccessClassConstantInternal + $constant_exists_variable = $context->getScope()->getVariableByNameOrNull(ConditionVisitor::CONSTANT_EXISTS_PREFIX . \ltrim($fqsen->__toString(), '\\')); + if ($constant_exists_variable && !$constant_exists_variable->getUnionType()->isPossiblyUndefined() && $constant_exists_variable->getFileRef()->getFile() === $context->getFile()) { + return $this->createPlaceholderGlobalConstant($fqsen); + } + $this->throwUndeclaredGlobalConstantIssueException($code_base, $context, $fqsen); + } + + $constant = $code_base->getGlobalConstantByFQSEN($fqsen); + + if ($constant->isNSInternal($code_base) + && !$constant->isNSInternalAccessFromContext( + $code_base, + $context + ) + ) { + // TODO: Refactor and also check namespaced constants + $this->emitIssue( + Issue::AccessConstantInternal, + $node->lineno, + (string)$constant->getFQSEN(), + $constant->getElementNamespace(), + $constant->getFileRef()->getFile(), + $constant->getFileRef()->getLineNumberStart(), + $context->getNamespace() + ); + } + + return $constant; + } + + private function createPlaceholderGlobalConstant( + FullyQualifiedGlobalConstantName $fqsen + ): GlobalConstant { + return new GlobalConstant( + $this->context, + $fqsen->getName(), + // This can't be an object. + UnionType::fromFullyQualifiedRealString('?array|?bool|?float|?int|?resource|?string'), + 0, + $fqsen + ); + } + + /** + * @throws IssueException + */ + private function throwUndeclaredGlobalConstantIssueException(CodeBase $code_base, Context $context, FullyQualifiedGlobalConstantName $fqsen): void + { + throw new IssueException( + Issue::fromType(Issue::UndeclaredConstant)( + $this->context->getFile(), + $this->node->lineno ?? $context->getLineNumberStart(), + [ $fqsen ], + IssueFixSuggester::suggestSimilarGlobalConstant($code_base, $context, $fqsen) + ) + ); + } + + /** + * @return ClassConstant + * Get the class constant associated with this node + * in this context + * + * @throws NodeException + * An exception is thrown if we can't understand the node + * + * @throws CodeBaseException + * An exception is thrown if we can't find the given + * class + * + * @throws UnanalyzableException + * An exception is thrown if we hit a construct in which + * we can't determine if the property exists or not + * + * @throws IssueException + * An exception is thrown if an issue is found while getting + * the list of possible classes. + */ + public function getClassConst(): ClassConstant + { + $node = $this->node; + if (!($node instanceof Node)) { + throw new AssertionError('$this->node must be a node'); + } + + $constant_name = $node->children['const']; + if (!\strcasecmp($constant_name, 'class')) { + $constant_name = 'class'; + } + + $class_fqsen = null; + + try { + $class_list = (new ContextNode( + $this->code_base, + $this->context, + $node->children['class'] + ))->getClassList(false, self::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME); + } catch (CodeBaseException $exception) { + $exception_fqsen = $exception->getFQSEN(); + if ($constant_name === 'class') { + throw new IssueException( + Issue::fromType(Issue::UndeclaredClassReference)( + $this->context->getFile(), + $node->lineno, + [(string)$exception_fqsen], + IssueFixSuggester::suggestSimilarClassForGenericFQSEN($this->code_base, $this->context, $exception_fqsen) + ) + ); + } + throw new IssueException( + Issue::fromType(Issue::UndeclaredClassConstant)( + $this->context->getFile(), + $node->lineno, + [$constant_name, (string)$exception_fqsen], + IssueFixSuggester::suggestSimilarClassForGenericFQSEN($this->code_base, $this->context, $exception_fqsen) + ) + ); + } + + foreach ($class_list as $class) { + // Remember the last analyzed class for the next issue message + $class_fqsen = $class->getFQSEN(); + + // Check to see if the class has the constant + if (!$class->hasConstantWithName( + $this->code_base, + $constant_name + )) { + continue; + } + + $constant = $class->getConstantByNameInContext( + $this->code_base, + $constant_name, + $this->context + ); + + if ($constant->isNSInternal($this->code_base) + && !$constant->isNSInternalAccessFromContext( + $this->code_base, + $this->context + ) + ) { + $this->emitIssue( + Issue::AccessClassConstantInternal, + $node->lineno, + (string)$constant->getFQSEN(), + $constant->getFileRef()->getFile(), + $constant->getFileRef()->getLineNumberStart() + ); + } + if ($constant->isDeprecated()) { + $this->emitIssue( + Issue::DeprecatedClassConstant, + $node->lineno, + (string)$constant->getFQSEN(), + $constant->getFileRef()->getFile(), + $constant->getFileRef()->getLineNumberStart(), + $constant->getDeprecationReason() + ); + } + + return $constant; + } + + // If no class is found, we'll emit the error elsewhere + if ($class_fqsen) { + $class_constant_fqsen = FullyQualifiedClassConstantName::make($class_fqsen, $constant_name); + throw new IssueException( + Issue::fromType(Issue::UndeclaredConstantOfClass)( + $this->context->getFile(), + $node->lineno, + [ "$class_fqsen::$constant_name" ], + IssueFixSuggester::suggestSimilarClassConstant($this->code_base, $this->context, $class_constant_fqsen) + ) + ); + } + + throw new NodeException( + $node, + "Can't figure out constant {$constant_name} in node" + ); + } + + /** + * @return string + * A unique and stable name for an anonymous class + */ + public function getUnqualifiedNameForAnonymousClass(): string + { + if (!($this->node instanceof Node)) { + throw new AssertionError('$this->node must be a node'); + } + + if (!($this->node->flags & ast\flags\CLASS_ANONYMOUS)) { + throw new AssertionError('Node must be an anonymous class node'); + } + + $class_name = 'anonymous_class_' + . \substr(\md5( + $this->context->getFile() . $this->context->getLineNumberStart() + ), 0, 8); + + return $class_name; + } + + /** + * @throws CodeBaseException if the closure could not be found + */ + public function getClosure(): Func + { + $closure_fqsen = + FullyQualifiedFunctionName::fromClosureInContext( + $this->context, + $this->node + ); + + if (!$this->code_base->hasFunctionWithFQSEN($closure_fqsen)) { + throw new CodeBaseException( + $closure_fqsen, + "Could not find closure $closure_fqsen" + ); + } + + return $this->code_base->getFunctionByFQSEN($closure_fqsen); + } + + /** + * Perform some backwards compatibility checks on a node. + * This ignores union types, and can be run in the parse phase. + * (It often should, because outside quick mode, it may be run multiple times per node) + * + * TODO: This is repetitive, move these checks into ParseVisitor? + * @suppress PhanPossiblyUndeclaredProperty + */ + public function analyzeBackwardCompatibility(): void + { + if (!Config::get_backward_compatibility_checks()) { + return; + } + + if (!($this->node instanceof Node) || !($this->node->children['expr'] ?? false)) { + return; + } + + $kind = $this->node->kind; + if (\in_array($kind, [ast\AST_STATIC_CALL, ast\AST_METHOD_CALL, ast\AST_NULLSAFE_METHOD_CALL], true)) { + return; + } + + $llnode = $this->node; + + if ($kind !== ast\AST_DIM) { + if (!($this->node->children['expr'] instanceof Node)) { + return; + } + + if ($this->node->children['expr']->kind !== ast\AST_DIM) { + (new ContextNode( + $this->code_base, + $this->context, + $this->node->children['expr'] + ))->analyzeBackwardCompatibility(); + return; + } + + $temp = $this->node->children['expr']->children['expr']; + $llnode = $this->node->children['expr']; + $lnode = $temp; + } else { + $temp = $this->node->children['expr']; + $lnode = $temp; + } + + // Strings can have DIMs, it turns out. + if (!($temp instanceof Node)) { + return; + } + + if (!($temp->kind === ast\AST_PROP + || $temp->kind === ast\AST_STATIC_PROP + )) { + return; + } + + while ($temp instanceof Node + && ($temp->kind === ast\AST_PROP + || $temp->kind === ast\AST_STATIC_PROP) + ) { + $llnode = $lnode; + $lnode = $temp; + + // Lets just hope the 0th is the expression + // we want + $temp = \array_values($temp->children)[0]; + } + + if (!($temp instanceof Node)) { + return; + } + + // Foo::$bar['baz'](); is a problem + // Foo::$bar['baz'] is not + if ($lnode->kind === ast\AST_STATIC_PROP + && $kind !== ast\AST_CALL + ) { + return; + } + + // $this->$bar['baz']; is a problem + // $this->bar['baz'] is not + if ($lnode->kind === ast\AST_PROP + && !($lnode->children['prop'] instanceof Node) + && !($llnode->children['prop'] instanceof Node) + ) { + return; + } + + if (( + ( + $lnode->children['prop'] instanceof Node + && $lnode->children['prop']->kind === ast\AST_VAR + ) + || + ( + ($lnode->children['class'] ?? null) instanceof Node + && ( + $lnode->children['class']->kind === ast\AST_VAR + || $lnode->children['class']->kind === ast\AST_NAME + ) + ) + || + ( + ($lnode->children['expr'] ?? null) instanceof Node + && ( + $lnode->children['expr']->kind === ast\AST_VAR + || $lnode->children['expr']->kind === ast\AST_NAME + ) + ) + ) + && + ( + $temp->kind === ast\AST_VAR + || $temp->kind === ast\AST_NAME + ) + ) { + $cache_entry = FileCache::getOrReadEntry($this->context->getFile()); + $line = $cache_entry->getLine($this->node->lineno) ?? ''; + unset($cache_entry); + if (strpos($line, '}[') === false + && strpos($line, ']}') === false + && strpos($line, '>{') === false + ) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::CompatiblePHP7, + $this->node->lineno + ); + } + } + } + + /** + * @throws IssueException if the list of possible classes couldn't be determined. + */ + public function resolveClassNameInContext(): ?FullyQualifiedClassName + { + // A function argument to resolve into an FQSEN + $arg = $this->node; + + try { + if (\is_string($arg)) { + // Class_alias treats arguments as fully qualified strings. + return FullyQualifiedClassName::fromFullyQualifiedString($arg); + } + if ($arg instanceof Node + && $arg->kind === ast\AST_CLASS_NAME) { + $class_type = UnionTypeVisitor::unionTypeFromClassNode( + $this->code_base, + $this->context, + $arg->children['class'] + ); + + // If we find a class definition, then return it. There should be 0 or 1. + // (Expressions such as 'int::class' are syntactically valid, but would have 0 results). + foreach ($class_type->asClassFQSENList($this->context) as $class_fqsen) { + return $class_fqsen; + } + } + + $class_name = $this->getEquivalentPHPScalarValue(); + // TODO: Emit + if (\is_string($class_name)) { + return FullyQualifiedClassName::fromFullyQualifiedString($class_name); + } + } catch (FQSENException $e) { + throw new IssueException( + Issue::fromType($e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike)( + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike, + $this->node->lineno ?? $this->context->getLineNumberStart(), + [$e->getFQSEN()] + ) + ); + } + + return null; + } + + // Flags for getEquivalentPHPValue + + // Should this attempt to resolve arrays? + public const RESOLVE_ARRAYS = (1 << 0); + // Should this attempt to resolve array keys? + public const RESOLVE_ARRAY_KEYS = (1 << 1); + // Should this attempt to resolve array values? + public const RESOLVE_ARRAY_VALUES = (1 << 2); + // Should this attempt to resolve accesses to constants? + public const RESOLVE_CONSTANTS = (1 << 3); + // If resolving array keys fails, should this use a placeholder? + public const RESOLVE_KEYS_USE_FALLBACK_PLACEHOLDER = (1 << 4); + // Skip unknown keys + public const RESOLVE_KEYS_SKIP_UNKNOWN_KEYS = (1 << 5); + // Resolve unary and binary operations. + public const RESOLVE_OPS = (1 << 6); + // Resolve calls to is_int, is_null, isset/empty, etc. + public const RESOLVE_TYPE_CHECKS = (1 << 6); + // Resolve variables, but only if this was defined as a constant AST. + // This currently only supports static variables. + // When disabled, all variables will be resolved. + public const RESOLVE_ONLY_CONSTANT_VARS = (1 << 7); + + public const RESOLVE_DEFAULT = + self::RESOLVE_ARRAYS | + self::RESOLVE_ARRAY_KEYS | + self::RESOLVE_ARRAY_VALUES | + self::RESOLVE_CONSTANTS | + self::RESOLVE_KEYS_USE_FALLBACK_PLACEHOLDER; + + public const RESOLVE_SCALAR_DEFAULT = + self::RESOLVE_CONSTANTS | + self::RESOLVE_KEYS_USE_FALLBACK_PLACEHOLDER; + + /** + * @param int $flags - See self::RESOLVE_* + * @return ?array - returns an array if elements could be resolved. + */ + private function getEquivalentPHPArrayElements(Node $node, int $flags): ?array + { + $elements = []; + foreach ($node->children as $child_node) { + if (!($child_node instanceof Node)) { + self::warnAboutEmptyArrayElements($this->code_base, $this->context, $node); + continue; + } + $key_node = ($flags & self::RESOLVE_ARRAY_KEYS) !== 0 ? $child_node->children['key'] : null; + $value_node = $child_node->children['value']; + if (self::RESOLVE_ARRAY_VALUES) { + $value_node = $this->getEquivalentPHPValueForNode($value_node, $flags); + } + // NOTE: this has some overlap with DuplicateKeyPlugin + if ($key_node === null) { + $elements[] = $value_node; + } elseif (\is_scalar($key_node)) { + $elements[$key_node] = $value_node; // Check for float? + } else { + $key = $this->getEquivalentPHPValueForNode($key_node, $flags); + if (\is_scalar($key)) { + $elements[$key] = $value_node; + } else { + if (($flags & self::RESOLVE_KEYS_USE_FALLBACK_PLACEHOLDER) !== 0) { + $elements[] = $value_node; + } else { + // TODO: Alternate strategies? + return null; + } + } + } + } + return $elements; + } + + /** + * @param Node $node a node of kind AST_ARRAY + * @suppress PhanUndeclaredProperty this adds a dynamic property + */ + public static function warnAboutEmptyArrayElements(CodeBase $code_base, Context $context, Node $node): void + { + if (isset($node->didWarnAboutEmptyArrayElements)) { + return; + } + $node->didWarnAboutEmptyArrayElements = true; + + $lineno = $node->lineno; + foreach ($node->children as $child_node) { + if (!$child_node instanceof Node) { + // Emit the line number of the nearest Node before this empty element + Issue::maybeEmit( + $code_base, + $context, + Issue::SyntaxError, + $lineno, + "Cannot use empty array elements in arrays" + ); + continue; + } + // Update the line number of the nearest Node + $lineno = $child_node->lineno; + } + } + /** + * This converts an AST node in context to the value it represents. + * This is useful for plugins, etc, and will gradually improve. + * + * @see self::getEquivalentPHPValue() + * + * @param Node|float|int|string $node + * @return Node|string[]|int[]|float[]|string|float|int|bool|resource|null - + * If this could be resolved and we're certain of the value, this gets a raw PHP value for $node. + * Otherwise, this returns $node. + */ + public function getEquivalentPHPValueForNode($node, int $flags) + { + if (!($node instanceof Node)) { + return $node; + } + $kind = $node->kind; + switch ($kind) { + case ast\AST_ARRAY: + if (($flags & self::RESOLVE_ARRAYS) === 0) { + return $node; + } + $elements = $this->getEquivalentPHPArrayElements($node, $flags); + if ($elements === null) { + // Attempted to resolve elements but failed at one or more elements. + return $node; + } + return $elements; + case ast\AST_CONST: + $name = $node->children['name']->children['name'] ?? null; + if (\is_string($name)) { + switch (\strtolower($name)) { + case 'false': + return false; + case 'true': + return true; + case 'null': + return null; + } + } + if (($flags & self::RESOLVE_CONSTANTS) === 0) { + return $node; + } + try { + $constant = (new ContextNode($this->code_base, $this->context, $node))->getConst(); + } catch (Exception $_) { + // Is there a need to catch IssueException as well? + return $node; + } + // TODO: Recurse, but don't try to resolve constants again + $new_node = $constant->getNodeForValue(); + if (is_object($new_node)) { + // Avoid infinite recursion, only resolve once + $new_node = (new ContextNode($this->code_base, $constant->getContext(), $new_node))->getEquivalentPHPValueForNode($new_node, $flags & ~self::RESOLVE_CONSTANTS); + } + return $new_node; + case ast\AST_CLASS_CONST: + if (($flags & self::RESOLVE_CONSTANTS) === 0) { + return $node; + } + try { + $constant = (new ContextNode($this->code_base, $this->context, $node))->getClassConst(); + } catch (\Exception $_) { + return $node; + } + // TODO: Recurse, but don't try to resolve constants again + $new_node = $constant->getNodeForValue(); + if (is_object($new_node)) { + // Avoid infinite recursion, only resolve once + $new_node = (new ContextNode($this->code_base, $constant->getContext(), $new_node))->getEquivalentPHPValueForNode($new_node, $flags & ~self::RESOLVE_CONSTANTS); + } + return $new_node; + case ast\AST_CLASS_NAME: + try { + return UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node, false)->asSingleScalarValueOrNull() ?? $node; + } catch (\Exception $_) { + return $node; + } + case ast\AST_MAGIC_CONST: + // TODO: Look into eliminating this + return $this->getValueForMagicConstByNode($node); + case ast\AST_BINARY_OP: + if ($flags & self::RESOLVE_OPS) { + return $this->getValueForBinaryOp($node, $flags); + } + break; + case ast\AST_UNARY_OP: + if ($flags & self::RESOLVE_OPS) { + return $this->getValueForUnaryOp($node, $flags); + } + break; + case ast\AST_EMPTY: + if ($flags & self::RESOLVE_TYPE_CHECKS) { + return $this->getValueForEmptyCheck($node, $flags); + } + break; + case ast\AST_ISSET: + if ($flags & self::RESOLVE_TYPE_CHECKS) { + // fprintf(STDERR, "Computing isset for %s\n", \Phan\Debug::nodeToString($node)); + return $this->getValueForIssetCheck($node, $flags); + } + break; + case ast\AST_CALL: + if ($flags & self::RESOLVE_TYPE_CHECKS) { + // fprintf(STDERR, "Computing isset for %s\n", \Phan\Debug::nodeToString($node)); + return $this->getValueForCall($node, $flags); + } + break; + case ast\AST_VAR: + if ($flags & self::RESOLVE_ONLY_CONSTANT_VARS) { + if (!$this->isVarWithConstantDefinition($node)) { + return $node; + } + // fall through. + } + break; + default: + if ($flags & self::RESOLVE_ONLY_CONSTANT_VARS) { + // Don't resolve other node kinds not in this list. + return $node; + } + break; + } + $node_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node + ); + $value = $node_type->asValueOrNullOrSelf(); + if (\is_object($value)) { + return $node; + } + return $value; + } + + /** + * Check if this variable is one which Phan has inferred to be likely + * to have a definition that was constant at this point in the codebase. + * (This is a heuristic) + */ + public function isVarWithConstantDefinition(Node $node): bool + { + if ($node->kind !== ast\AST_VAR) { + return false; + } + $name = $node->children['name']; + if (!is_string($name)) { + return false; + } + $scope = $this->context->getScope(); + if ($scope->hasVariableWithName($name)) { + return ($scope->getVariableByName($name)->getPhanFlags() & \Phan\Language\Element\Flags::IS_CONSTANT_DEFINITION) !== 0; + } + return false; + } + + /** + * @return Node|string[]|int[]|float[]|string|float|int|bool|null - + * If this could be resolved and we're certain of the value, + * this gets a raw PHP value for the binary operation represented by $node. + * Otherwise, this returns $node. + */ + private function getValueForBinaryOp(Node $node, int $flags) + { + $left_value = $this->getEquivalentPHPValueForNode($node->children['left'], $flags); + if ($left_value instanceof Node) { + return $node; + } + $right_value = $this->getEquivalentPHPValueForNode($node->children['right'], $flags); + if ($right_value instanceof Node) { + return $node; + } + try { + return InferValue::computeBinaryOpResult($left_value, $right_value, $node->flags); + } catch (Error $e) { + self::handleErrorInOperation($node, $e); + return $node; + } + } + + private function handleErrorInOperation(Node $node, Error $e): void + { + $this->emitIssue( + Issue::TypeErrorInOperation, + $node->lineno, + ASTReverter::toShortString($node), + $e->getMessage() + ); + } + + /** + * @return Node|string[]|int[]|float[]|string|float|int|bool|null - + * If this could be resolved and we're certain of the value, + * then this gets a raw PHP value for the unary operation represented by $node. + * Otherwise, this returns $node. + */ + private function getValueForUnaryOp(Node $node, int $flags) + { + $operand_value = $this->getEquivalentPHPValueForNode($node->children['expr'], $flags); + // fprintf(STDERR, "Computing unary op for %s : operand = %s\n", \Phan\Debug::nodeToString($node), json_encode($operand_value)); + if ($operand_value instanceof Node) { + return $node; + } + + try { + return InferValue::computeUnaryOpResult($operand_value, $node->flags); + } catch (Error $e) { + self::handleErrorInOperation($node, $e); + return $node; + } + } + + /** + * @param Node $node a node of kind AST_EMPTY + * @return Node|bool + * If this could be resolved and we're certain of the value, this gets a raw PHP boolean for $node. + * Otherwise, this returns $node. + */ + private function getValueForEmptyCheck(Node $node, int $flags) + { + $expr_value = $this->getEquivalentPHPValueForNode($node->children['expr'], $flags); + if ($expr_value instanceof Node) { + return $node; + } + + return !$expr_value; + } + + /** + * @param Node $node a node of kind AST_ISSET + * @return Node|bool + * If this could be resolved and we're certain of the result returned by isset, + * this gets a raw PHP boolean for $node. + * Otherwise, this returns $node. + */ + private function getValueForIssetCheck(Node $node, int $flags) + { + $var_value = $this->getEquivalentPHPValueForNode($node->children['var'], $flags); + if ($var_value instanceof Node) { + return $node; + } + + return $var_value !== null; + } + + // Type checks that can act on a single argument + private const TYPE_CHECK_SET = [ + 'is_array' => true, + 'is_bool' => true, + 'is_callable' => true, + 'is_double' => true, + 'is_float' => true, + 'is_int' => true, + 'is_integer' => true, + 'is_iterable' => true, + 'is_long' => true, + 'is_null' => true, + 'is_numeric' => true, + 'is_object' => true, + 'is_real' => true, + 'is_resource' => true, + 'is_scalar' => true, + 'is_string' => true, + ]; + + /** + * @param Node $node a node of kind AST_CALL + * @return Node|bool + * If this could be resolved and we're certain of the return value of the call, + * this gets a raw result for $node (currently limited to booleans, e.g. is_string($var). + * Otherwise, this returns $node. + */ + private function getValueForCall(Node $node, int $flags) + { + $arg_list = $node->children['args']->children; + if (\count($arg_list) !== 1) { + return $node; + } + $raw_function_name = ConditionVisitorUtil::getFunctionName($node); + if (!is_string($raw_function_name)) { + return $node; + } + $raw_function_name = \strtolower($raw_function_name); + if (!isset(self::TYPE_CHECK_SET[$raw_function_name])) { + return $node; + } + $arg_value = $this->getEquivalentPHPValueForNode($arg_list[0], $flags); + if ($arg_value instanceof Node) { + return $node; + } + + // Given some known function name and the resolved value of the argument to the function, + // evaluate what the result is. + // e.g. `is_null($someStaticValue)` + return $raw_function_name($arg_value); + } + + /** + * @return array|string|int|float|bool|null|Node the value of the corresponding PHP constant, + * or the original node if that could not be determined + * @suppress PhanUnreferencedPublicMethod + */ + public function getValueForMagicConst() + { + $node = $this->node; + if (!($node instanceof Node && $node->kind === ast\AST_MAGIC_CONST)) { + throw new AssertionError(__METHOD__ . ' expected AST_MAGIC_CONST'); + } + return $this->getValueForMagicConstByNode($node); + } + + /** + * @return array|string|int|float|bool|null|Node the value of the corresponding PHP magic constant (e.g. __FILE__), + * or the original node if that could not be determined + */ + public function getValueForMagicConstByNode(Node $node) + { + $result = (new UnionTypeVisitor($this->code_base, $this->context))->visitMagicConst($node)->asSingleScalarValueOrNullOrSelf(); + return is_object($result) ? $node : $result; + } + + /** + * This converts an AST node in context to the value it represents. + * This is useful for plugins, etc, and will gradually improve. + * + * This does not create new object instances. + * + * @return Node|string[]|int[]|float[]|string|float|int|bool|resource|null - + * If this could be resolved and we're certain of the value, this gets an equivalent definition. + * Otherwise, this returns $node. + */ + public function getEquivalentPHPValue(int $flags = self::RESOLVE_DEFAULT) + { + return $this->getEquivalentPHPValueForNode($this->node, $flags); + } + + /** + * @return Node|string[]|int[]|float[]|string|float|int|bool|resource|null - + * If this could be resolved and we're certain of the value, this gets an equivalent definition. + * Otherwise, this returns $node. + */ + public function getEquivalentPHPValueForControlFlowAnalysis() + { + return $this->getEquivalentPHPValueForNode( + $this->node, + self::RESOLVE_ARRAYS | + self::RESOLVE_ARRAY_KEYS | + self::RESOLVE_ARRAY_VALUES | + self::RESOLVE_OPS | + self::RESOLVE_ONLY_CONSTANT_VARS | + self::RESOLVE_TYPE_CHECKS + ); + } + + /** + * This converts an AST node (of any kind) in context to the value it represents. + * This is useful for plugins, etc, and will gradually improve. + * + * This does not create new object instances. + * + * @return Node|string|float|int|bool|null - + * If this could be resolved and we're certain of the value, this gets an equivalent definition. + * Otherwise, this returns $node. If this would be an array, this returns $node. + * + * @suppress PhanPartialTypeMismatchReturn the flags prevent this from returning an array + */ + public function getEquivalentPHPScalarValue() + { + return $this->getEquivalentPHPValueForNode($this->node, self::RESOLVE_SCALAR_DEFAULT); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/FallbackUnionTypeVisitor.php b/bundled-libs/phan/phan/src/Phan/AST/FallbackUnionTypeVisitor.php new file mode 100644 index 000000000..3ab6fbe9a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/FallbackUnionTypeVisitor.php @@ -0,0 +1,870 @@ +code_base = $code_base; + $this->context = $context; + } + + /** + * @param CodeBase $code_base + * The code base within which we're operating + * + * @param Context $context + * The context of the parser at the node for which we'd + * like to determine a type + * + * @param int|float|string|Node $node + * The node which is having the type be determined + * + * @return UnionType + * The conservatively chosen UnionType associated with the given node + * in the given Context within the given CodeBase + */ + public static function unionTypeFromNode( + CodeBase $code_base, + Context $context, + $node + ): UnionType { + if ($node instanceof Node) { + return (new self($code_base, $context))->__invoke($node); + } + return Type::fromObject($node)->asRealUnionType(); + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node (@phan-unused-param) + * An AST node we'd like to determine the UnionType + * for + * + * @return UnionType + * The set of types associated with the given node + */ + public function visit(Node $node): UnionType + { + return UnionType::empty(); + } + + // Real types aren't certain, since these don't throw even if the expression + // being incremented is an object or array. + // + // TODO: Check if union type is sane (string/int) + // public function visitPostInc(Node $node) : UnionType + // public function visitPostDec(Node $node) : UnionType + // public function visitPreInc(Node $node) : UnionType + // public function visitPreDec(Node $node) : UnionType + + /** + * Visit a node with kind `\ast\AST_CLONE` + * + * @param Node $node @unused-param + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitClone(Node $node): UnionType + { + return ObjectType::instance(false)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_EMPTY` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitEmpty(Node $node): UnionType + { + return BoolType::instance(false)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_ISSET` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitIsset(Node $node): UnionType + { + return BoolType::instance(false)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_INCLUDE_OR_EVAL` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitIncludeOrEval(Node $node): UnionType + { + // require() can return arbitrary objects. Lets just + // say that we don't know what it is and move on + return UnionType::empty(); + } + + /** + * Visit a node with kind `\ast\AST_SHELL_EXEC` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitShellExec(Node $node): UnionType + { + return StringType::instance(true)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_CONDITIONAL` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitConditional(Node $node): UnionType + { + $cond_node = $node->children['cond']; + $cond_truthiness = UnionTypeVisitor::checkCondUnconditionalTruthiness($cond_node); + // For the shorthand $a ?: $b, the cond node will be the truthy value. + // Note: an ast node will never be null(can be unset), it will be a const AST node with the name null. + $true_node = $node->children['true'] ?? $cond_node; + + // Rarely, a conditional will always be true or always be false. + if ($cond_truthiness !== null) { + // TODO: Add no-op checks in another PR, if they don't already exist for conditional. + if ($cond_truthiness) { + // The condition is unconditionally true + return self::unionTypeFromNode( + $this->code_base, + $this->context, + $true_node + ); + } else { + // The condition is unconditionally false + + // Add the type for the 'false' side + return self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['false'] + ); + } + } + + // Postcondition: This is (cond_expr) ? (true_expr) : (false_expr) + + $true_type = self::unionTypeFromNode( + $this->code_base, + $this->context, + $true_node + ); + + $false_type = self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['false'] + ); + + // Add the type for the 'true' side to the 'false' side + $union_type = $true_type->withUnionType($false_type); + + // If one side has an unknown type but the other doesn't + // we can't let the unseen type get erased. Unfortunately, + // we need to add 'mixed' in so that we know it could be + // anything at all. + // + // See Issue #104 + if ($true_type->isEmpty() xor $false_type->isEmpty()) { + $union_type = $union_type->withType( + MixedType::instance(false) + ); + } + + return $union_type; + } + + /** + * Visit a node with kind `\ast\AST_ARRAY` + * + * @param Node $node @unused-param + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitArray(Node $node): UnionType + { + // TODO: More precise + return ArrayType::instance(false)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_BINARY_OP` (or `\ast\AST_ASSIGN_OP`) + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitBinaryOp(Node $node): UnionType + { + switch ($node->flags) { + case flags\BINARY_ADD: + return UnionType::fromFullyQualifiedRealString('int|float|array'); + case flags\BINARY_BITWISE_AND: + case flags\BINARY_BITWISE_OR: + case flags\BINARY_BITWISE_XOR: + return UnionType::fromFullyQualifiedRealString('int|string'); + case flags\BINARY_BOOL_XOR: + case flags\BINARY_IS_EQUAL: + case flags\BINARY_IS_IDENTICAL: + case flags\BINARY_IS_NOT_EQUAL: + case flags\BINARY_IS_NOT_IDENTICAL: + case flags\BINARY_IS_SMALLER: + case flags\BINARY_IS_SMALLER_OR_EQUAL: + case flags\BINARY_BOOL_AND: + case flags\BINARY_BOOL_OR: + case flags\BINARY_IS_GREATER: + case flags\BINARY_IS_GREATER_OR_EQUAL: + return UnionType::fromFullyQualifiedRealString('bool'); + case flags\BINARY_CONCAT: + return UnionType::fromFullyQualifiedRealString('string'); + case flags\BINARY_DIV: + case flags\BINARY_MUL: + case flags\BINARY_POW: + case flags\BINARY_SUB: + return UnionType::fromFullyQualifiedRealString('int|float'); + case flags\BINARY_MOD: + case flags\BINARY_SHIFT_LEFT: + case flags\BINARY_SHIFT_RIGHT: + return UnionType::fromFullyQualifiedRealString('int'); + case flags\BINARY_SPACESHIP: + return UnionType::fromFullyQualifiedRealString('-1|0|1'); + case flags\BINARY_COALESCE: + return $this->analyzeCoalesce($node); + } + return UnionType::empty(); + } + + /** + * @param Node $node a node of kind ast\AST_ASSIGN_OP or ast\AST_BINARY_OP with flags ast\flags\BINARY_COALESCE + */ + private function analyzeCoalesce(Node $node): UnionType + { + $left = self::unionTypeFromNode($this->code_base, $this->context, $node->children['left'] ?? $node->children['var']); + if ($left->isEmpty()) { + return UnionType::empty(); + } + $right = self::unionTypeFromNode($this->code_base, $this->context, $node->children['right'] ?? $node->children['expr']); + if ($right->isEmpty()) { + return UnionType::empty(); + } + return $left->nonNullableClone()->withUnionType($right); + } + + /** + * Visit a node with kind `\ast\AST_ASSIGN_OP` (E.g. $x .= 'suffix') + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitAssignOp(Node $node): UnionType + { + // TODO: Refactor if this depends on $node->children in the future. + return $this->visitBinaryOp($node); + } + + /** + * Visit a node with kind `\ast\AST_CAST` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws NodeException if the flags are a value we aren't expecting + * @suppress PhanThrowTypeMismatchForCall + */ + public function visitCast(Node $node): UnionType + { + // This calls unionTypeFromNode to trigger any warnings + // TODO: Check if the cast would throw an error at runtime, based on the type (e.g. casting object to string/int) + + // RedundantConditionCallPlugin contains unrelated checks of whether this is redundant. + switch ($node->flags) { + case \ast\flags\TYPE_NULL: + return NullType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_BOOL: + return BoolType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_LONG: + return IntType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_DOUBLE: + return FloatType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_STRING: + return StringType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_ARRAY: + return ArrayType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_OBJECT: + // TODO: Handle values that are already objects + return Type::fromFullyQualifiedString('\stdClass')->asRealUnionType(); + case \ast\flags\TYPE_STATIC: + return StaticType::instance(false)->asRealUnionType(); + default: + throw new NodeException( + $node, + 'Unknown type (' . $node->flags . ') in cast' + ); + } + } + + /** + * Visit a node with kind `\ast\AST_NEW` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitNew(Node $node): UnionType + { + static $object_type; + if ($object_type === null) { + $object_type = ObjectType::instance(false)->asRealUnionType(); + } + $class_node = $node->children['class']; + if (!($class_node instanceof Node)) { + return $object_type; + } + return $this->visitClassNameNode($class_node) ?? $object_type; + } + + /** + * Visit a node with kind `\ast\AST_INSTANCEOF` + * + * @param Node $node @unused-param + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitInstanceOf(Node $node): UnionType + { + return BoolType::instance(false)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_CLOSURE` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitClosure(Node $node): UnionType + { + // The type of a closure is the fqsen pointing + // at its definition + $closure_fqsen = + FullyQualifiedFunctionName::fromClosureInContext( + $this->context, + $node + ); + + if ($this->code_base->hasFunctionWithFQSEN($closure_fqsen)) { + $func = $this->code_base->getFunctionByFQSEN($closure_fqsen); + } else { + $func = null; + } + + return ClosureType::instanceWithClosureFQSEN( + $closure_fqsen, + $func + )->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_ARROW_FUNC` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitArrowFunc(Node $node): UnionType + { + return $this->visitClosure($node); + } + + /** + * Visit a node with kind `\ast\AST_ENCAPS_LIST` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitEncapsList(Node $node): UnionType + { + return StringType::instance(false)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_CONST` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitConst(Node $node): UnionType + { + // Figure out the name of the constant if it's + // a string. + $constant_name = $node->children['name']->children['name'] ?? ''; + + // If the constant is referring to the current + // class, return that as a type + if (Type::isSelfTypeString($constant_name) || Type::isStaticTypeString($constant_name)) { + return Type::fromStringInContext($constant_name, $this->context, Type::FROM_NODE)->asRealUnionType(); + } + + try { + $constant = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getConst(); + } catch (Exception $_) { + return UnionType::empty(); + } + + return $constant->getUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_CLASS_CONST` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitClassConst(Node $node): UnionType + { + $class_node = $node->children['class']; + if (!$class_node instanceof Node || $class_node->kind !== ast\AST_NAME) { + // ignore nonsense like (0)::class, and dynamic accesses such as $var::CLASS + return UnionType::empty(); + } + try { + $constant = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getClassConst(); + $union_type = $constant->getUnionType(); + if (\strcasecmp($class_node->children['name'], 'static') === 0) { + if ($this->context->isInClassScope() && $this->context->getClassInScope($this->code_base)->isFinal()) { + // static::X should be treated like self::X in a final class. + return $union_type; + } + return $union_type->eraseRealTypeSet(); + } + return $union_type; + } catch (NodeException $_) { + // ignore, this should warn elsewhere + } + + return UnionType::empty(); + } + + // TODO: Support AST_STATIC_PROP + + /** + * Visit a node with kind `\ast\AST_CALL` + * + * @param Node $node + * A node of the type indicated by the function name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitCall(Node $node): UnionType + { + $expression = $node->children['expr']; + if (!($expression instanceof Node && $expression->kind === ast\AST_NAME)) { + // Give up on closures, callables + return UnionType::empty(); + } + try { + $function_list_generator = (new ContextNode( + $this->code_base, + $this->context, + $expression + ))->getFunctionFromNode(true); + + $possible_types = null; + foreach ($function_list_generator as $function) { + $function->analyzeReturnTypes($this->code_base); // For daemon/server mode, call this to consistently ensure accurate return types. + + // NOTE: Deliberately do not use the closure for $function->hasDependentReturnType(). + // Most plugins expect the context to have variables, which this won't provide. + $function_types = $function->getUnionType(); + if ($possible_types instanceof UnionType) { + $possible_types = $possible_types->withUnionType($function_types); + } else { + $possible_types = $function_types; + } + } + + return $possible_types ?? UnionType::empty(); + } catch (Exception $_) { + return UnionType::empty(); + } + } + + /** + * Visit a node with kind `\ast\AST_STATIC_CALL` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitStaticCall(Node $node): UnionType + { + ['class' => $class_node, 'method' => $method_name] = $node->children; + if (!\is_string($method_name) || !($class_node instanceof Node) || $class_node->kind !== ast\AST_NAME) { + // Give up on dynamic calls + return UnionType::empty(); + } + try { + $possible_types = null; + foreach (UnionTypeVisitor::classListFromNodeAndContext($this->code_base, $this->context, $class_node) as $class) { + if (!$class->hasMethodWithName($this->code_base, $method_name, true)) { + return UnionType::empty(); + } + $method = $class->getMethodByName($this->code_base, $method_name); + $method_types = $method->getUnionType(); + if ($possible_types instanceof UnionType) { + $possible_types = $possible_types->withUnionType($method_types); + } else { + $possible_types = $method_types; + } + } + return $possible_types ?? UnionType::empty(); + } catch (Exception $_) { + return UnionType::empty(); + } + } + + /** + * Visit a node with kind `\ast\AST_METHOD_CALL`. + * + * Conservatively try to infer the returned union type of calls such + * as $this->someMethod(...) + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitMethodCall(Node $node): UnionType + { + ['expr' => $expr_node, 'method' => $method_name] = $node->children; + if (!\is_string($method_name) || !($expr_node instanceof Node) || $expr_node->kind !== ast\AST_VAR) { + // Give up on dynamic calls + return UnionType::empty(); + } + // Only attempt to handle $this->method() + // Don't attempt to handle possibility of closures being rebound. + if ($expr_node->children['name'] !== 'this') { + return UnionType::empty(); + } + if (!$this->context->isInClassScope()) { + return UnionType::empty(); + } + try { + $class = $this->context->getClassInScope($this->code_base); + if (!$class->hasMethodWithName($this->code_base, $method_name, true)) { + return UnionType::empty(); + } + $method = $class->getMethodByName($this->code_base, $method_name); + return $method->getUnionType(); + } catch (Exception $_) { + return UnionType::empty(); + } + } + + /** + * Visit a node with kind `\ast\AST_NULLSAFE_METHOD_CALL`. + * + * Conservatively try to infer the returned union type of calls such + * as $this?->someMethod(...) + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitNullsafeMethodCall(Node $node): UnionType + { + return $this->visitMethodCall($node)->nullableClone(); + } + + /** + * Visit a node with kind `\ast\AST_ASSIGN` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitAssign(Node $node): UnionType + { + // XXX typed properties/references will change the type of the result from the right hand side + return self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + } + + /** + * Visit a node with kind `\ast\AST_UNARY_OP` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitUnaryOp(Node $node): UnionType + { + // Shortcut some easy operators + $flags = $node->flags; + if ($flags === \ast\flags\UNARY_BOOL_NOT) { + return BoolType::instance(false)->asRealUnionType(); + } + + if ($flags === \ast\flags\UNARY_MINUS || $flags === ast\flags\UNARY_PLUS) { + return UnionType::fromFullyQualifiedRealString('int|float'); + } elseif ($flags === \ast\flags\UNARY_BITWISE_NOT) { + return UnionType::fromFullyQualifiedRealString('int|string'); + } + // UNARY_SILENCE + return self::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']); + } + + /** + * `print($str)` always returns 1. + * See https://secure.php.net/manual/en/function.print.php#refsect1-function.print-returnvalues + * @param Node $node @phan-unused-param + */ + public function visitPrint(Node $node): UnionType + { + return LiteralIntType::instanceForValue(1, false)->asRealUnionType(); + } + + /** + * @param Node $node + * A node holding a class name + * + * @return ?UnionType + * The set of types that are possibly produced by the + * given node + */ + private function visitClassNameNode(Node $node): ?UnionType + { + // Things of the form `new $className()`, `new $obj()`, `new (foo())()`, etc. + if ($node->kind !== \ast\AST_NAME) { + return null; + } + + // Get the name of the class + $class_name = $node->children['name']; + + // If this is a straight-forward class name, recurse into the + // class node and get its type + if (Type::isStaticTypeString($class_name)) { + return StaticType::instance(false)->asRealUnionType(); + } + if (!Type::isSelfTypeString($class_name)) { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + return UnionTypeVisitor::unionTypeFromClassNode( + $this->code_base, + $this->context, + $node + ); + } + + // This node references `self` or `static` + if (!$this->context->isInClassScope()) { + return UnionType::empty(); + } + + // Reference to a parent class + if ($class_name === 'parent') { + $class = $this->context->getClassInScope( + $this->code_base + ); + + $parent_type_option = $class->getParentTypeOption(); + if (!$parent_type_option->isDefined()) { + return UnionType::empty(); + } + + return $parent_type_option->get()->asRealUnionType(); + } + + return $this->context->getClassFQSEN()->asType()->asRealUnionType(); + } + + // TODO: visitVar for $this is Object or the current class. +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/InferPureSnippetVisitor.php b/bundled-libs/phan/phan/src/Phan/AST/InferPureSnippetVisitor.php new file mode 100644 index 000000000..393dc86cd --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/InferPureSnippetVisitor.php @@ -0,0 +1,174 @@ +sideEffectFreeMethod() + * + * @param Node|int|string|float|null $node + */ + public static function isSideEffectFreeSnippet(CodeBase $code_base, Context $context, $node): bool + { + if (!$node instanceof Node) { + return true; + } + try { + (new self($code_base, $context))->__invoke($node); + return true; + } catch (NodeException $_) { + return false; + } + } + + public function visitReturn(Node $node): void + { + throw new NodeException($node); + } + + // visitThrow throws already + + // TODO(optional): Bother tracking actual loop/switch depth + public function visitBreak(Node $node): void + { + if ($node->children['depth'] > 1) { + throw new NodeException($node); + } + } + + public function visitContinue(Node $node): void + { + if ($node->children['depth'] > 1) { + throw new NodeException($node); + } + } + + public function visitYield(Node $node): void + { + throw new NodeException($node); + } + + public function visitYieldFrom(Node $node): void + { + throw new NodeException($node); + } + + // TODO(optional) track actual goto labels + public function visitGoto(Node $node): void + { + throw new NodeException($node); + } + + // NOTE: Checks of assignment, increment or decrement are deferred to --unused-variable-detection + + public function visitUnset(Node $node): void + { + throw new NodeException($node); + } + + protected function getClassForVariable(Node $expr): Clazz + { + if ($expr->kind !== ast\AST_VAR) { + // TODO: Support static properties, (new X()), other expressions with inferable types + throw new NodeException($expr, 'expected simple variable'); + } + $var_name = $expr->children['name']; + if (!is_string($var_name)) { + throw new NodeException($expr, 'variable name is not a string'); + } + if ($var_name !== 'this') { + $variable = $this->context->getScope()->getVariableByNameOrNull($var_name); + if (!$variable) { + throw new NodeException($expr, 'unknown variable'); + } + + $union_type = $variable->getUnionType()->asNormalizedTypes(); + $known_fqsen = null; + foreach ($union_type->getTypeSet() as $type) { + if (!$type->isObjectWithKnownFQSEN()) { + continue; + } + $fqsen = $type->asFQSEN(); + if ($known_fqsen && $known_fqsen !== $fqsen) { + throw new NodeException($expr, 'unknown class'); + } + $known_fqsen = $fqsen; + } + if (!$known_fqsen instanceof FullyQualifiedClassName) { + throw new NodeException($expr, 'unknown class'); + } + if (!$this->code_base->hasClassWithFQSEN($known_fqsen)) { + throw new NodeException($expr, 'unknown class'); + } + return $this->code_base->getClassByFQSEN($known_fqsen); + } + if (!$this->context->isInClassScope()) { + throw new NodeException($expr, 'Not in class scope'); + } + return $this->context->getClassInScope($this->code_base); + } + + + /** + * @param Node $node the node of the call, with 'args' + * @override + */ + protected function checkCalledFunction(Node $node, FunctionInterface $method): void + { + if ($method->isPure()) { + // avoid false positives - throw when calling void methods that were marked as free of side effects. + if ($method->isPHPInternal() || (($method instanceof Method && $method->isAbstract()) || $method->hasReturn() || $method->hasYield())) { + return; + } + } + $label = self::getLabelForFunction($method); + + $value = (UseReturnValuePlugin::HARDCODED_FQSENS[$label] ?? false); + if ($value === true) { + return; + } elseif ($value === UseReturnValuePlugin::SPECIAL_CASE) { + if (UseReturnValueVisitor::doesSpecialCaseHaveSideEffects($label, $node)) { + // infer that var_export($x, true) is pure but not var_export($x) + throw new NodeException($node, $label); + } + return; + } + throw new NodeException($node, $label); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/InferPureVisitor.php b/bundled-libs/phan/phan/src/Phan/AST/InferPureVisitor.php new file mode 100644 index 000000000..01df8d7ca --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/InferPureVisitor.php @@ -0,0 +1,712 @@ + + */ + protected $unresolved_status_dependencies = []; + + /** + * A graph that tracks information about whether functions are pure, and how they depend on other functions. + * + * @var ?PureMethodGraph + */ + protected $pure_method_graph; + + public function __construct(CodeBase $code_base, Context $context, string $function_fqsen_label, ?PureMethodGraph $graph = null) + { + $this->code_base = $code_base; + $this->context = $context; + $this->function_fqsen_label = $function_fqsen_label; + $this->pure_method_graph = $graph; + } + + /** + * Generate a visitor from a function or method. + * This will be used for checking if the method is pure. + */ + public static function fromFunction(CodeBase $code_base, FunctionInterface $func, ?PureMethodGraph $graph): InferPureVisitor + { + return new self( + $code_base, + $func->getContext(), + self::getLabelForFunction($func), + $graph + ); + } + + /** + * Returns the label UseReturnValuePlugin will use to look up whether this functions/methods is pure. + */ + public function getLabel(): string + { + return $this->function_fqsen_label; + } + + /** + * Returns an array of functions/methods with unknown pure status. + * If any of those functions are impure, then this function is impure. + * + * @return array + */ + public function getUnresolvedStatusDependencies(): array + { + return $this->unresolved_status_dependencies; + } + + /** + * Returns the label UseReturnValuePlugin will use to look up whether functions/methods are pure. + */ + public static function getLabelForFunction(FunctionInterface $func): string + { + return \strtolower(\ltrim($func->getFQSEN()->__toString(), '\\')); + } + + // visitAssignRef + // visitThrow + // visitEcho + // visitPrint + // visitIncludeOrExec + public function visit(Node $node): void + { + throw new NodeException($node); + } + + public function visitVar(Node $node): void + { + if (!\is_scalar($node->children['name'])) { + throw new NodeException($node); + } + } + + /** + * @unused-param $node + * @override + */ + public function visitClassName(Node $node): void + { + } + + /** + * @unused-param $node + * @override + */ + public function visitMagicConst(Node $node): void + { + } + + /** + * @unused-param $node + * @override + */ + public function visitConst(Node $node): void + { + } + + /** + * @unused-param $node + * @override + */ + public function visitEmpty(Node $node): void + { + $this->maybeInvoke($node->children['expr']); + } + + /** @override */ + public function visitIsset(Node $node): void + { + $this->maybeInvoke($node->children['var']); + } + + /** + * @unused-param $node + * @override + */ + public function visitContinue(Node $node): void + { + } + + /** + * @unused-param $node + * @override + */ + public function visitBreak(Node $node): void + { + } + + /** @override */ + public function visitClassConst(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + public function visitStatic(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + public function visitArray(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + public function visitArrayElem(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + public function visitEncapsList(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + public function visitInstanceof(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + public function visitPreInc(Node $node): void + { + $this->checkPureIncDec($node); + } + + public function visitPreDec(Node $node): void + { + $this->checkPureIncDec($node); + } + + public function visitPostInc(Node $node): void + { + $this->checkPureIncDec($node); + } + + public function visitPostDec(Node $node): void + { + $this->checkPureIncDec($node); + } + + protected function checkPureIncDec(Node $node): void + { + $var = $node->children['var']; + if (!$var instanceof Node) { + throw new NodeException($node); + } + if ($var->kind !== ast\AST_VAR) { + throw new NodeException($var); + } + $this->visitVar($var); + } + + /** + * @param Node|string|int|float|null $node + */ + final protected function maybeInvoke($node): void + { + if ($node instanceof Node) { + $this->__invoke($node); + } + } + + public function visitBinaryOp(Node $node): void + { + $this->maybeInvoke($node->children['left']); + $this->maybeInvoke($node->children['right']); + } + + public function visitUnaryOp(Node $node): void + { + $this->maybeInvoke($node->children['expr']); + } + + public function visitDim(Node $node): void + { + $this->maybeInvoke($node->children['expr']); + $this->maybeInvoke($node->children['dim']); + } + + public function visitNullsafeProp(Node $node): void + { + $this->visitProp($node); + } + + public function visitProp(Node $node): void + { + ['expr' => $expr, 'prop' => $prop] = $node->children; + if (!$expr instanceof Node) { + throw new NodeException($node); + } + $this->__invoke($expr); + if ($prop instanceof Node) { + throw new NodeException($prop); + } + } + + /** @override */ + public function visitStmtList(Node $node): void + { + foreach ($node->children as $stmt) { + if ($stmt instanceof Node) { + $this->__invoke($stmt); + } + } + } + + /** @override */ + public function visitStaticProp(Node $node): void + { + ['class' => $class, 'prop' => $prop] = $node->children; + if (!$class instanceof Node) { + throw new NodeException($node); + } + $this->__invoke($class); + if ($prop instanceof Node) { + throw new NodeException($prop); + } + } + + final protected function maybeInvokeAllChildNodes(Node $node): void + { + foreach ($node->children as $c) { + if ($c instanceof Node) { + $this->__invoke($c); + } + } + } + + /** @override */ + public function visitCast(Node $node): void + { + $this->maybeInvoke($node->children['expr']); + } + + /** @override */ + public function visitConditional(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitWhile(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitDoWhile(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitFor(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitForeach(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitIf(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitIfElem(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitSwitch(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitSwitchList(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitSwitchCase(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitMatch(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitMatchArmList(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitMatchArm(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** @override */ + public function visitExprList(Node $node): void + { + $this->maybeInvokeAllChildNodes($node); + } + + /** + * @unused-param $node + * @override + */ + public function visitGoto(Node $node): void + { + } + + /** + * @unused-param $node + * @override + */ + public function visitLabel(Node $node): void + { + } + + /** @override */ + public function visitAssignOp(Node $node): void + { + $this->visitAssign($node); + } + + /** @override */ + public function visitAssign(Node $node): void + { + ['var' => $var, 'expr' => $expr] = $node->children; + if (!$var instanceof Node) { + throw new NodeException($node); + } + $this->checkVarKindOfAssign($var); + $this->__invoke($var); + if ($expr instanceof Node) { + $this->__invoke($expr); + } + } + + private function checkVarKindOfAssign(Node $var): void + { + if ($var->kind === ast\AST_VAR) { + return; + } elseif ($var->kind === ast\AST_PROP) { + // Functions that assign to properties aren't pure, + // unless assigning to $this->prop in a constructor. + if (\preg_match('/::__construct$/iD', $this->function_fqsen_label)) { + $name = $var->children['expr']; + if ($name instanceof Node && $name->kind === ast\AST_VAR && $name->children['name'] === 'this') { + return; + } + } + } + throw new NodeException($var); + } + + public function visitNew(Node $node): void + { + $name_node = $node->children['class']; + if (!($name_node instanceof Node && $name_node->kind === ast\AST_NAME)) { + throw new NodeException($node); + } + $this->visitArgList($node->children['args']); + try { + $class_list = (new ContextNode($this->code_base, $this->context, $name_node))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME); + } catch (Exception $_) { + throw new NodeException($name_node); + } + if (!$class_list) { + throw new NodeException($name_node, 'no class found'); + } + foreach ($class_list as $class) { + if ($class->isPHPInternal()) { + // TODO build a list of internal classes where result of new() is often unused. + continue; + } + if (!$class->hasMethodWithName($this->code_base, '__construct', true)) { + throw new NodeException($name_node, 'no __construct found'); + } + $this->checkCalledFunction($node, $class->getMethodByName($this->code_base, '__construct')); + } + } + + /** @override */ + public function visitReturn(Node $node): void + { + $expr_node = $node->children['expr']; + if ($expr_node instanceof Node) { + $this->__invoke($expr_node); + } + } + + /** @override */ + public function visitYield(Node $node): void + { + $this->maybeInvoke($node->children['key']); + $this->maybeInvoke($node->children['value']); + } + + /** @override */ + public function visitYieldFrom(Node $node): void + { + $this->maybeInvoke($node->children['expr']); + } + + /** + * @unused-param $node + * @override + */ + public function visitName(Node $node): void + { + // do nothing + } + + /** @override */ + public function visitCall(Node $node): void + { + $expr = $node->children['expr']; + if (!$expr instanceof Node) { + throw new NodeException($node); + } + if ($expr->kind !== ast\AST_NAME) { + // XXX this is deliberately a limited subset of what full analysis would do, + // so this can't infer locally set closures, etc. + throw new NodeException($expr); + } + $found_function = false; + try { + $function_list_generator = (new ContextNode( + $this->code_base, + $this->context, + $expr + ))->getFunctionFromNode(true); + + foreach ($function_list_generator as $function) { + $this->checkCalledFunction($node, $function); + $found_function = true; + } + } catch (CodeBaseException $_) { + // ignore it. + } + if (!$found_function) { + throw new NodeException($expr, 'not a function'); + } + $this->visitArgList($node->children['args']); + } + + public function visitStaticCall(Node $node): void + { + $method = $node->children['method']; + if (!\is_string($method)) { + throw new NodeException($node); + } + $class = $node->children['class']; + if (!($class instanceof Node)) { + throw new NodeException($node); + } + if ($class->kind !== ast\AST_NAME) { + throw new NodeException($class, 'not a name'); + } + try { + $union_type = UnionTypeVisitor::unionTypeFromClassNode( + $this->code_base, + $this->context, + $class + ); + } catch (Exception $_) { + throw new NodeException($class, 'could not get type'); + } + if ($union_type->typeCount() !== 1) { + throw new NodeException($class); + } + $type = $union_type->getTypeSet()[0]; + if (!$type->isObjectWithKnownFQSEN()) { + throw new NodeException($class); + } + $class_fqsen = $type->asFQSEN(); + if (!($class_fqsen instanceof FullyQualifiedClassName)) { + throw new NodeException($class); + } + if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) { + throw new NodeException($class); + } + try { + $class = $this->code_base->getClassByFQSEN($class_fqsen); + } catch (Exception $_) { + throw new NodeException($node); + } + if (!$class->hasMethodWithName($this->code_base, $method, true)) { + throw new NodeException($node, 'no method'); + } + + $this->checkCalledFunction($node, $class->getMethodByName($this->code_base, $method)); + $this->visitArgList($node->children['args']); + } + + public function visitNullsafeMethodCall(Node $node): void + { + $this->visitMethodCall($node); + } + + public function visitMethodCall(Node $node): void + { + $method_name = $node->children['method']; + if (!\is_string($method_name)) { + throw new NodeException($node); + } + $expr = $node->children['expr']; + if (!$expr instanceof Node) { + throw new NodeException($node); + } + $class = $this->getClassForVariable($expr); + if (!$class->hasMethodWithName($this->code_base, $method_name, true)) { + throw new NodeException($expr, 'does not have method'); + } + $this->checkCalledFunction($node, $class->getMethodByName($this->code_base, $method_name)); + + $this->visitArgList($node->children['args']); + } + + protected function getClassForVariable(Node $expr): Clazz + { + if (!$this->context->isInClassScope()) { + // We don't track variables in UseReturnValuePlugin + throw new NodeException($expr, 'method call seen outside class scope'); + } + if ($expr->kind !== ast\AST_VAR) { + throw new NodeException($expr, 'expected simple variable'); + } + + $var_name = $expr->children['name']; + if (!is_string($var_name)) { + // TODO: Support static properties, (new X()), other expressions with inferable types + throw new NodeException($expr, 'variable name is not a string'); + } + if ($var_name !== 'this') { + throw new NodeException($expr, 'not $this'); + } + if (!$this->context->isInClassScope()) { + throw new NodeException($expr, 'Not in class scope'); + } + return $this->context->getClassInScope($this->code_base); + } + + /** + * @param Node $node the node of the call, with 'args' + */ + protected function checkCalledFunction(Node $node, FunctionInterface $method): void + { + if ($method->isPure()) { + return; + } + $label = self::getLabelForFunction($method); + + $value = (UseReturnValuePlugin::HARDCODED_FQSENS[$label] ?? false); + if ($value === true) { + return; + } elseif ($value === UseReturnValuePlugin::SPECIAL_CASE) { + if (UseReturnValueVisitor::doesSpecialCaseHaveSideEffects($label, $node)) { + // infer that var_export($x, true) is pure but not var_export($x) + throw new NodeException($node, $label); + } + return; + } + if ($method->isPHPInternal()) { + // Something such as printf that isn't pure. Or something that isn't in the HARDCODED_FQSENS. + throw new NodeException($node, 'internal method is not pure'); + } + if ($label === $this->function_fqsen_label) { + return; + } + if ($this->pure_method_graph) { + $this->unresolved_status_dependencies[$label] = $method; + return; + } + throw new NodeException($node, $label); + } + + public function visitClosure(Node $node): void + { + $closure_fqsen = FullyQualifiedFunctionName::fromClosureInContext( + (clone($this->context))->withLineNumberStart($node->lineno), + $node + ); + if (!$this->code_base->hasFunctionWithFQSEN($closure_fqsen)) { + throw new NodeException($node, "Failed lookup of closure_fqsen"); + } + $this->checkCalledFunction($node, $this->code_base->getFunctionByFQSEN($closure_fqsen)); + } + + public function visitArrowFunc(Node $node): void + { + $this->visitClosure($node); + } + + public function visitArgList(Node $node): void + { + foreach ($node->children as $x) { + if ($x instanceof Node) { + $this->__invoke($x); + } + } + } + + public function visitNamedArg(Node $node): void + { + $expr = $node->children['expr']; + if ($expr instanceof Node) { + $this->__invoke($expr); + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/InferValue.php b/bundled-libs/phan/phan/src/Phan/AST/InferValue.php new file mode 100644 index 000000000..e2adaf38a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/InferValue.php @@ -0,0 +1,116 @@ + $right; + case flags\BINARY_IS_GREATER_OR_EQUAL: + return $left >= $right; + case flags\BINARY_SPACESHIP: + return $left <=> $right; + case flags\BINARY_COALESCE: + return $left ?? $right; + default: + return new Node(); + } + }); + } + + /** + * Compute result of a unary operator for a PhanPluginBothLiteralsBinaryOp warning + * @param int|array|string|float|bool|null $operand operand of operation + * @param int $flags the flags on the node + * @return Node|array|int|string|float|bool|null + * Node is returned to indicate that the result could not be computed + * @throws Error that should be handled by caller, e.g. for `+[]`. + */ + public static function computeUnaryOpResult($operand, int $flags) + { + // Don't make errors in the analyzed code crash Phan (e.g. converting arrays to strings). + return self::evalSuppressingErrors(/** @return Node|array|int|string|float|bool|null */ static function () use ($operand, $flags) { + switch ($flags) { + case flags\UNARY_BOOL_NOT: + return !$operand; + case flags\UNARY_BITWISE_NOT: + // TODO: Check for acting on arrays + return ~$operand; + case flags\UNARY_MINUS: + return -$operand; + case flags\UNARY_PLUS: + return +$operand; + case flags\UNARY_SILENCE: + return $operand; + default: + return new Node(); + } + }); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/Parser.php b/bundled-libs/phan/phan/src/Phan/AST/Parser.php new file mode 100644 index 000000000..3703280b5 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/Parser.php @@ -0,0 +1,612 @@ + */ + private static $cache = null; + + /** + * Creates a cache if Phan is configured to use caching in the current phase. + * + * @return ?Cache + */ + private static function maybeGetCache(CodeBase $code_base): ?Cache + { + if ($code_base->getExpectChangesToFileContents()) { + return null; + } + if (!Config::getValue('cache_polyfill_asts')) { + return null; + } + return self::getCache(); + } + + /** + * @return Cache + * @suppress PhanPartialTypeMismatchReturn + */ + private static function getCache(): Cache + { + return self::$cache ?? self::$cache = self::makeNewCache(); + } + + /** + * @return DiskCache + */ + private static function makeNewCache(): DiskCache + { + $igbinary_version = \phpversion('igbinary') ?: ''; + $use_igbinary = \version_compare($igbinary_version, '2.0.5') >= 0; + + $user = \getenv('USERNAME') ?: \getenv('USER'); + $directory = \sys_get_temp_dir() . '/phan'; + if (StringUtil::isNonZeroLengthString($user)) { + $directory .= "-$user"; + } + return new DiskCache($directory, '-ast', ParseResult::class, $use_igbinary); + } + + /** + * Parses the code with the native parser or the polyfill. + * If $suppress_parse_errors is false, this also emits SyntaxError. + * + * @param CodeBase $code_base + * @param Context $context + * @param ?Request $request (A daemon mode request if in daemon mode. May affect the parser used for $file_path) + * @param string $file_path file path for error reporting + * @param string $file_contents file contents to pass to parser. This may deliberately differ from what is currently on disk (e.g. for the language server mode or daemon mode) + * @param bool $suppress_parse_errors (If true, don't emit SyntaxError) + * @throws ParseError + * @throws CompileError (possible in php 7.3) + * @throws ParseException + */ + public static function parseCode( + CodeBase $code_base, + Context $context, + ?Request $request, + string $file_path, + string $file_contents, + bool $suppress_parse_errors + ): Node { + try { + // This will choose the parser to use based on the config and $file_path + // (For "Go To Definition", one of the files will have a slower parser which records the requested AST node) + + if (self::shouldUsePolyfill($file_path, $request)) { + // This helper method has its own exception handling. + // It may throw a ParseException, which is unintentionally not caught here. + return self::parseCodePolyfill($code_base, $context, $file_path, $file_contents, $suppress_parse_errors, $request); + } + return self::parseCodeHandlingDeprecation($code_base, $context, $file_contents, $file_path); + } catch (CompileError | ParseError $native_parse_error) { + return self::handleParseError($code_base, $context, $file_path, $file_contents, $suppress_parse_errors, $native_parse_error, $request); + } + } + + + private static function parseCodeHandlingDeprecation(CodeBase $code_base, Context $context, string $file_contents, string $file_path): Node + { + global $__no_echo_phan_errors; + // Suppress errors such as "declare(encoding=...) ignored because Zend multibyte feature is turned off by settings" (#1076) + // E_COMPILE_WARNING can't be caught by a PHP error handler, + // the errors are printed to stderr by default (can't be captured), + // and those errors might mess up language servers, etc. if ever printed to stdout + $original_error_reporting = error_reporting(); + error_reporting($original_error_reporting & ~\E_COMPILE_WARNING); + $__no_echo_phan_errors = static function (int $errno, string $errstr, string $unused_errfile, int $errline) use ($code_base, $context): bool { + if ($errno === \E_DEPRECATED && \preg_match('/Version.*is deprecated/i', $errstr)) { + return false; + } + // Catch errors such as E_DEPRECATED in php 7.4 for the (real) cast. + Issue::maybeEmit( + $code_base, + $context, + Issue::CompatibleSyntaxNotice, + $errline, + $errstr + ); + // Return true to prevent printing to stderr + return true; + }; + try { + error_clear_last(); + $root_node = \ast\parse_code( + $file_contents, + Config::AST_VERSION, + $file_path + ); + $error = error_get_last(); + if ($error && $error['type'] === \E_COMPILE_WARNING) { + Issue::maybeEmit( + $code_base, + $context, + Issue::SyntaxCompileWarning, + $error['line'], + $error['message'] + ); + } + return $root_node; + } finally { + $__no_echo_phan_errors = false; + error_reporting($original_error_reporting); + } + } + + /** + * Handles ParseError|CompileError from the native parser. + * This will return a Node or re-throw an error, depending on the configuration and parameters. + * + * @param CodeBase $code_base + * @param Context $context + * @param string $file_path file path for error reporting + * @param string $file_contents file contents to pass to parser. May be overridden to ignore what is currently on disk. + * @param ParseError|CompileError $native_parse_error (can be CompileError in 7.3+, will be ParseError in most cases) + * @param ?Request $request used to check if caching should be enabled to save time. + * @throws ParseError most of the time + * @throws CompileError in PHP 7.3+ + */ + public static function handleParseError( + CodeBase $code_base, + Context $context, + string $file_path, + string $file_contents, + bool $suppress_parse_errors, + Error $native_parse_error, + ?Request $request = null + ): Node { + if ($file_path !== 'internal') { + if (!$suppress_parse_errors) { + self::emitSyntaxErrorForNativeParseError($code_base, $context, $file_path, new FileCacheEntry($file_contents), $native_parse_error, $request); + } + if (!Config::getValue('use_fallback_parser')) { + // By default, don't try to re-parse files with syntax errors. + throw $native_parse_error; + } + } + + // If there's a parse error in a file that's excluded from analysis, give up on parsing it. + // Users might not see the parse error, and ignoring it (e.g. acting as though a file in vendor/ or ext/ + // that can't be parsed has class and function definitions) + // may lead to users not noticing bugs. + if (Phan::isExcludedAnalysisFile($file_path)) { + throw $native_parse_error; + } + // But if the user would see the syntax error, go ahead and retry. + + if ($request) { + $converter = new CachingTolerantASTConverter(); + } else { + $converter = new TolerantASTConverter(); + } + $converter->setPHPVersionId(Config::get_closest_target_php_version_id()); + $errors = []; + try { + $node = $converter->parseCodeAsPHPAST($file_contents, Config::AST_VERSION, $errors); + } catch (\Exception $_) { + // Generic fallback. TODO: log. + throw $native_parse_error; + } + // TODO: loop over $errors? + return $node; + } + + /** + * Emit PhanSyntaxError for ParseError|CompileError from the native parser. + * + * @param CodeBase $code_base + * @param Context $context + * @param string $file_path file path for error reporting + * @param FileCacheEntry $file_cache_entry for file contents that were passed to the polyfill parser. May be overridden to ignore what is currently on disk. + * @param ParseError|CompileError $native_parse_error (can be CompileError in 7.3+, will be ParseError in most cases) + */ + public static function emitSyntaxErrorForNativeParseError( + CodeBase $code_base, + Context $context, + string $file_path, + FileCacheEntry $file_cache_entry, + Error $native_parse_error, + ?Request $request = null + ): void { + // Try to get the raw diagnostics by reference. + // For efficiency, reuse the last result if this was called multiple times in a row. + $line = $native_parse_error->getLine(); + $message = $native_parse_error->getMessage(); + $diagnostic_error_column = self::guessErrorColumnUsingTokens($file_cache_entry, $native_parse_error) ?: + self::guessErrorColumnUsingPolyfill($code_base, $context, $file_path, $file_cache_entry, $native_parse_error, $request); + + Issue::maybeEmitWithParameters( + $code_base, + $context, + Issue::SyntaxError, + $line, + [$message], + null, + $diagnostic_error_column + ); + } + + /** + * Returns the 1-based error column, or 0 if unknown. + * + * This will return the corresponding unexpected token only when there's exactly one token with that value on the line with the error. + */ + private static function guessErrorColumnUsingTokens( + FileCacheEntry $file_cache_entry, + Error $native_parse_error + ): int { + if (!\function_exists('token_get_all')) { + return 0; + } + $message = $native_parse_error->getMessage(); + $prefix = "unexpected (?:token )?('(?:.+)'|\"(?:.+)\")"; + if (!\preg_match("/$prefix \((T_\w+)\)/", $message, $matches)) { + if (!\preg_match("/$prefix, expecting/", $message, $matches)) { + if (!\preg_match("/$prefix$/D", $message, $matches)) { + return 0; + } + } + } + $token_name = $matches[2] ?? null; + if (\is_string($token_name)) { + if (!\defined($token_name)) { + return 0; + } + $token_kind = \constant($token_name); + } else { + $token_kind = null; + } + $token_str = \substr($matches[1], 1, -1); + $tokens = \token_get_all($file_cache_entry->getContents()); + $candidates = []; + $desired_line = $native_parse_error->getLine(); + foreach ($tokens as $i => $token) { + if (!\is_array($token)) { + if ($token_str === $token) { + $candidates[] = $i; + } + continue; + } + $line = $token[2]; + if ($line < $desired_line) { + continue; + } elseif ($line > $desired_line) { + break; + } + if ($token_kind !== $token[0]) { + continue; + } + if ($token_str !== $token[1]) { + continue; + } + $candidates[] = $i; + } + if (\count($candidates) !== 1) { + return 0; + } + return self::computeColumnForTokenAtIndex($tokens, $candidates[0], $desired_line); + } + + /** + * @param list $tokens + * @return int the 1-based line number, or 0 on failure + */ + private static function computeColumnForTokenAtIndex(array $tokens, int $i, int $desired_line): int + { + if ($i <= 0) { + return 1; + } + $column = 0; + for ($j = $i - 1; $j >= 0; $j--) { + $token = $tokens[$j]; + if (!\is_array($token)) { + $column += \strlen($token); + continue; + } + $token_str = $token[1]; + if ($token[2] >= $desired_line) { + $column += \strlen($token_str); + continue; + } + $last_newline = \strrpos($token_str, "\n"); + if ($last_newline !== false) { + $column += \strlen($token_str) - $last_newline; + } + break; + } + return $column; + } + + /** + * Returns the 1-based error column, or 0 if unknown. + */ + private static function guessErrorColumnUsingPolyfill( + CodeBase $code_base, + Context $context, + string $file_path, + FileCacheEntry $file_cache_entry, + Error $native_parse_error, + ?Request $request + ): int { + $file_contents = $file_cache_entry->getContents(); + static $last_file_contents = null; + static $errors = []; + + if ($last_file_contents !== $file_contents) { + // Create a brand new reference group + $new_errors = []; + $errors =& $new_errors; + try { + self::parseCodePolyfill($code_base, $context, $file_path, $file_contents, true, $request, $errors); + } catch (Throwable $_) { + // ignore this exception + } + } + // If the polyfill parser emits the first error on the same line as the native parser, + // mention the column that the polyfill parser found for the error. + $diagnostic = $errors[0] ?? null; + // $diagnostic_error_column is either 0 or the column of the error determined by the polyfill parser + if (!$diagnostic) { + return 0; + } + // Using FilePositionMap is much faster than substr_count to count lines if you have more than one diagnostic to report (e.g. a string has an unmatched quote). + $file_position_map = $file_cache_entry->getFilePositionMap(); + $start = (int) $diagnostic->start; + $diagnostic_error_start_line = $file_position_map->getLineNumberForOffset($start); + if ($diagnostic_error_start_line > $native_parse_error->getLine()) { + return 0; + } + // If the current character is whitespace, keep searching forward for the next non-whitespace character + $file_length = \strlen($file_contents); + while ($start + 1 < $file_length && \ctype_space($file_contents[$start])) { + $start++; + } + $diagnostic_error_start_line = $file_position_map->getLineNumberForOffset($start); + if ($diagnostic_error_start_line !== $native_parse_error->getLine()) { + return 0; + } + return $start - (\strrpos($file_contents, "\n", $start - \strlen($file_contents) - 1) ?: 0); + } + + /** Set an arbitrary limit on the number of warnings for the polyfill diagnostics to prevent excessively large errors for unmatched string quotes, etc. */ + private const MAX_POLYFILL_WARNINGS = 1000; + + /** + * Parses the code with the polyfill. If $suppress_parse_errors is false, this also emits SyntaxError. + * + * @param CodeBase $code_base + * @param Context $context + * @param string $file_path file path for error reporting + * @param string $file_contents file contents to pass to parser. May be overridden to ignore what is currently on disk. + * @param bool $suppress_parse_errors (If true, don't emit SyntaxError) + * @param ?Request $request - May affect the parser used for $file_path + * @param list &$errors @phan-output-reference + * @throws ParseException + * @suppress PhanThrowTypeMismatch + */ + public static function parseCodePolyfill(CodeBase $code_base, Context $context, string $file_path, string $file_contents, bool $suppress_parse_errors, ?Request $request, array &$errors = []): Node + { + // @phan-suppress-next-line PhanRedundantCondition + if (!\in_array(Config::AST_VERSION, TolerantASTConverter::SUPPORTED_AST_VERSIONS, true)) { + throw new \Error(\sprintf("Unexpected polyfill version: want %s, got %d", \implode(', ', TolerantASTConverter::SUPPORTED_AST_VERSIONS), Config::AST_VERSION)); + } + $converter = self::createConverter($file_path, $file_contents, $request); + $converter->setPHPVersionId(Config::get_closest_target_php_version_id()); + $errors = []; + error_clear_last(); + try { + $node = $converter->parseCodeAsPHPAST($file_contents, Config::AST_VERSION, $errors, self::maybeGetCache($code_base)); + } catch (\Exception $e) { + // Generic fallback. TODO: log. + throw new ParseException('Unexpected Exception of type ' . \get_class($e) . ': ' . $e->getMessage(), 0); + } + if (!$suppress_parse_errors) { + $error = error_get_last(); + if ($error) { + self::handleWarningFromPolyfill($code_base, $context, $error); + } + } + if (!$errors) { + return $node; + } + $file_position_map = new FilePositionMap($file_contents); + $emitted_warning_count = 0; + foreach ($errors as $diagnostic) { + if ($diagnostic->kind === 0) { + $start = (int)$diagnostic->start; + $diagnostic_error_message = 'Fallback parser diagnostic error: ' . $diagnostic->message; + $len = \strlen($file_contents); + $diagnostic_error_start_line = $file_position_map->getLineNumberForOffset($start); + $diagnostic_error_column = $start - (\strrpos($file_contents, "\n", $start - $len - 1) ?: 0); + + if (!$suppress_parse_errors) { + $emitted_warning_count++; + if ($emitted_warning_count <= self::MAX_POLYFILL_WARNINGS) { + Issue::maybeEmitWithParameters( + $code_base, + $context, + Issue::SyntaxError, + $diagnostic_error_start_line, + [$diagnostic_error_message], + null, + $diagnostic_error_column + ); + } + } + if (!Config::getValue('use_fallback_parser')) { + // By default, don't try to re-parse files with syntax errors. + throw new ParseException($diagnostic_error_message, $diagnostic_error_start_line); + } + + // If there's a parse error in a file that's excluded from analysis, give up on parsing it. + // Users might not see the parse error, and ignoring it (e.g. acting as though a file in vendor/ or ext/ + // that can't be parsed has class and function definitions) + // may lead to users not noticing bugs. + if (Phan::isExcludedAnalysisFile($file_path)) { + throw new ParseException($diagnostic_error_message, $diagnostic_error_start_line); + } + } + } + return $node; + } + + /** + * @param array $error + */ + private static function handleWarningFromPolyfill(CodeBase $code_base, Context $context, array $error): void + { + if (\in_array($error['type'], [\E_DEPRECATED, \E_COMPILE_WARNING], true) && + \basename($error['file']) === 'PhpTokenizer.php') { + $line = $error['line']; + if (\preg_match('/line ([0-9]+)$/D', $error['message'], $matches)) { + $line = (int)$matches[1]; + } + + + Issue::maybeEmit( + $code_base, + $context, + $error['type'] === \E_COMPILE_WARNING ? Issue::SyntaxCompileWarning : Issue::CompatibleSyntaxNotice, + $line, + $error['message'] + ); + } + } + + /** + * Remove the leading #!/path/to/interpreter/of/php from a CLI script, if any was found. + */ + public static function removeShebang(string $file_contents): string + { + if (\substr($file_contents, 0, 2) !== "#!") { + return $file_contents; + } + for ($i = 2; $i < \strlen($file_contents); $i++) { + $c = $file_contents[$i]; + if ($c === "\r") { + if (($file_contents[$i + 1] ?? '') === "\n") { + $i++; + break; + } + } elseif ($c === "\n") { + break; + } + } + if ($i >= \strlen($file_contents)) { + return ''; + } + $rest = (string)\substr($file_contents, $i + 1); + if (\strcasecmp(\substr($rest, 0, 5), "" . $rest; + } + + private static function shouldUsePolyfill(string $file_path, Request $request = null): bool + { + if (Config::getValue('use_polyfill_parser')) { + return true; + } + if ($request) { + return $request->shouldUseMappingPolyfill($file_path); + } + return false; + } + + + private static function createConverter(string $file_path, string $file_contents, Request $request = null): TolerantASTConverter + { + if ($request) { + if ($request->shouldUseMappingPolyfill($file_path)) { + // TODO: Rename to something better + $converter = new TolerantASTConverterWithNodeMapping( + $request->getTargetByteOffset($file_contents), + static function (Node $node): void { + // @phan-suppress-next-line PhanAccessMethodInternal + ConfigPluginSet::instance()->prepareNodeSelectionPluginForNode($node); + } + ); + if ($request->shouldAddPlaceholdersForPath($file_path)) { + $converter->setShouldAddPlaceholders(true); + } + return $converter; + } + return new CachingTolerantASTConverter(); + } + if (Config::getValue('__parser_keep_original_node')) { + return new TolerantASTConverterPreservingOriginal(); + } + + return new TolerantASTConverter(); + } + + /** + * Get a string representation of the AST kind value. + * @suppress PhanAccessMethodInternal + */ + public static function getKindName(int $kind): string + { + static $use_native = null; + $use_native = ($use_native ?? self::shouldUseNativeAST()); + if ($use_native) { + return \ast\get_kind_name($kind); + } + // The native function doesn't exist or is missing some constants Phan would use. + return ShimFunctions::getKindName($kind); + } + + // TODO: Refactor and make more code use this check + private static function shouldUseNativeAST(): bool + { + if (\PHP_VERSION_ID >= 80000) { + $min_version = '1.0.10'; + } elseif (\PHP_VERSION_ID >= 70400) { + $min_version = '1.0.2'; + } else { + $min_version = Config::MINIMUM_AST_EXTENSION_VERSION; + } + return \version_compare(\phpversion('ast') ?: '0.0.0', $min_version) >= 0; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/PhanAnnotationAdder.php b/bundled-libs/phan/phan/src/Phan/AST/PhanAnnotationAdder.php new file mode 100644 index 000000000..1f192bbab --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/PhanAnnotationAdder.php @@ -0,0 +1,254 @@ +children['phan_nf']) to node types that have a variable number of children or expected values for flags + * + * and returns the new Node. + * The original \ast\Node objects are not modified. + * + * This adds to $node->children - Many AST node kinds can be used in places Phan needs to know about. + * (for being potentially null/undefined) + * + * Current annotations: + * + * 1. Mark $x in isset($x['key']['nested']) as being acceptable to have null offsets. + * Same for $x in $x ?? null, empty($x['offset']), and so on. + * 2. Mark $x and $x['key'] in "$x['key'] = $y" as being acceptable to be null or undefined. + * and so on (e.g. ['key' => $x[0]] = $y) + * @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod + */ +class PhanAnnotationAdder +{ + public const PHAN_NODE_FLAGS = 'phan_nf'; + + public const FLAG_INITIALIZES = 1 << 28; + public const FLAG_IGNORE_NULLABLE = 1 << 29; + public const FLAG_IGNORE_UNDEF = 1 << 30; + + public const FLAG_IGNORE_NULLABLE_AND_UNDEF = self::FLAG_IGNORE_UNDEF | self::FLAG_IGNORE_NULLABLE; + + public function __construct() + { + } + + /** @var associative-array maps values of ast\Node->kind to closures that can be used to generate annotations (on the ast\Node instance) for that node kind */ + private static $closures_for_kind; + + /** + * Initialize the map of kinds to closures that add annotations to the corresponding node kind. + * This is called when the class is loaded. + */ + public static function init(): void + { + if (\is_array(self::$closures_for_kind)) { + return; + } + self::initInner(); + } + + public const FLAGS_NODE_TYPE_SET = [ + ast\AST_VAR => true, + ast\AST_DIM => true, + ast\AST_PROP => true, + ast\AST_STATIC_PROP => true, + ast\AST_ASSIGN => true, + ast\AST_ASSIGN_REF => true, + ]; + + /** + * @param array $children (should all be Nodes or null) + * @param int $bit_set + */ + private static function markArrayElements($children, int $bit_set): void + { + foreach ($children as $node) { + if ($node instanceof Node) { + $node->flags |= $bit_set; + } + } + } + + /** + * @param Node $node + * @param int $bit_set the bits to add to the flags + */ + private static function markNode(Node $node, int $bit_set): void + { + $kind = $node->kind; + if (\array_key_exists($kind, self::FLAGS_NODE_TYPE_SET)) { + $node->flags |= $bit_set; + } elseif ($kind === ast\AST_ARRAY) { + // flags and children are single-purpose right now + self::markArrayElements($node->children, $bit_set); + } else { + $node->children[self::PHAN_NODE_FLAGS] = $bit_set; + } + } + + private static function initInner(): void + { + /** + * @param Node $node + */ + $binary_op_handler = static function (Node $node): void { + if ($node->flags === flags\BINARY_COALESCE) { + $inner_node = $node->children['left']; + if ($inner_node instanceof Node) { + self::markNode($inner_node, self::FLAG_IGNORE_NULLABLE_AND_UNDEF); + } + } + }; + /** + * @param Node $node a node of kind ast\AST_ASSIGN_OP + */ + $assign_op_handler = static function (Node $node): void { + if ($node->flags === flags\BINARY_COALESCE) { + $inner_node = $node->children['var']; + if ($inner_node instanceof Node) { + self::markNode($inner_node, self::FLAG_IGNORE_NULLABLE_AND_UNDEF | self::FLAG_INITIALIZES); + } + } + }; + /** + * @param Node $node + * @return void + */ + $initializes_handler = static function (Node $node): void { + $inner_node = $node->children['var']; + if ($inner_node instanceof Node) { + self::markNode($inner_node, self::FLAG_IGNORE_NULLABLE_AND_UNDEF | self::FLAG_INITIALIZES); + } + }; + /** + * @param Node $node + * @return void + */ + $dim_handler = static function (Node $node): void { + if ($node->flags & self::FLAG_IGNORE_NULLABLE_AND_UNDEF) { + $inner_node = $node->children['expr']; + if ($inner_node instanceof Node) { + self::markNode($inner_node, self::FLAG_IGNORE_NULLABLE_AND_UNDEF); + } + } + }; + // also marks $expr the same way + $prop_handler = $dim_handler; + + /** + * @param Node $node + * @return void + */ + $ignore_nullable_and_undef_handler = static function (Node $node): void { + $inner_node = $node->children['var']; + if ($inner_node instanceof Node) { + self::markNode($inner_node, self::FLAG_IGNORE_NULLABLE_AND_UNDEF); + } + }; + + /** + * @param Node $node + * @return void + */ + $ignore_nullable_and_undef_expr_handler = static function (Node $node): void { + $inner_node = $node->children['expr']; + if ($inner_node instanceof Node) { + self::markNode($inner_node, self::FLAG_IGNORE_NULLABLE_AND_UNDEF); + } + }; + /** + * @param Node $node + * @return void + */ + $ast_array_elem_handler = static function (Node $node): void { + // Handle [$a1, $a2] = $array; - Don't warn about $node + $bit = $node->flags & self::FLAG_IGNORE_UNDEF; + if ($bit) { + $inner_node = $node->children['value']; + if (($inner_node instanceof Node)) { + self::markNode($inner_node, $bit); + } + } + }; + + self::$closures_for_kind = [ + ast\AST_BINARY_OP => $binary_op_handler, + ast\AST_ASSIGN_OP => $assign_op_handler, + ast\AST_DIM => $dim_handler, + ast\AST_PROP => $prop_handler, + ast\AST_EMPTY => $ignore_nullable_and_undef_expr_handler, + ast\AST_ISSET => $ignore_nullable_and_undef_handler, + ast\AST_UNSET => $ignore_nullable_and_undef_handler, + ast\AST_ASSIGN => $initializes_handler, + ast\AST_ASSIGN_REF => $initializes_handler, + // Skip over AST_ARRAY + ast\AST_ARRAY_ELEM => $ast_array_elem_handler, + ]; + } + + /** + * @param Node|array|int|string|float|bool|null $node + */ + public static function applyFull($node): void + { + if ($node instanceof Node) { + $closure = self::$closures_for_kind[$node->kind] ?? null; + if (\is_object($closure)) { + $closure($node); + } + foreach ($node->children as $inner) { + self::applyFull($inner); + } + } + } + + /** @internal */ + public const SCOPE_START_LIST = [ + ast\AST_CLASS, + ast\AST_FUNC_DECL, + ast\AST_CLOSURE, + ast\AST_ARROW_FUNC, + ]; + + /** + * @param Node|string|int|float|null $node + */ + private static function applyToScopeInner($node): void + { + if ($node instanceof Node) { + $kind = $node->kind; + if (\in_array($kind, self::SCOPE_START_LIST, true)) { + return; + } + + $closure = self::$closures_for_kind[$kind] ?? null; + if ($closure !== null) { + $closure($node); + } + foreach ($node->children as $inner) { + self::applyToScopeInner($inner); + } + } + } + + /** + * @param Node $node a node beginning a scope such as AST_FUNC, AST_STMT_LIST, AST_METHOD, etc. (Assumes these nodes don't have any annotations. + */ + public static function applyToScope(Node $node): void + { + foreach ($node->children as $inner) { + self::applyToScopeInner($inner); + } + } +} +PhanAnnotationAdder::init(); diff --git a/bundled-libs/phan/phan/src/Phan/AST/ScopeImpactCheckingVisitor.php b/bundled-libs/phan/phan/src/Phan/AST/ScopeImpactCheckingVisitor.php new file mode 100644 index 000000000..3905fc62a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/ScopeImpactCheckingVisitor.php @@ -0,0 +1,151 @@ +maybeInvoke($node->children['expr']); + } + + public function visitPrint(Node $node): void + { + $this->maybeInvoke($node->children['expr']); + } + + + public function visitVar(Node $node): void + { + if (!\is_scalar($node->children['name'])) { + throw new NodeException($node); + } + } + + /** @override */ + public function visitContinue(Node $node): void + { + throw new NodeException($node); + } + + public function visitBreak(Node $node): void + { + throw new NodeException($node); + } + + public function visitPreInc(Node $node): void + { + throw new NodeException($node); + } + + public function visitPreDec(Node $node): void + { + throw new NodeException($node); + } + + public function visitPostInc(Node $node): void + { + throw new NodeException($node); + } + + public function visitPostDec(Node $node): void + { + throw new NodeException($node); + } + + protected function checkPureIncDec(Node $node): void + { + $var = $node->children['var']; + if (!$var instanceof Node) { + throw new NodeException($node); + } + if ($var->kind !== ast\AST_VAR) { + throw new NodeException($var); + } + $this->visitVar($var); + } + + /** @override */ + public function visitGoto(Node $node): void + { + throw new NodeException($node); + } + + /** @override */ + public function visitAssignOp(Node $node): void + { + throw new NodeException($node); + } + + /** @override */ + public function visitAssign(Node $node): void + { + throw new NodeException($node); + } + + /** @override */ + public function visitReturn(Node $node): void + { + throw new NodeException($node); + } + + /** @override */ + public function visitYield(Node $node): void + { + $this->maybeInvoke($node->children['key']); + $this->maybeInvoke($node->children['value']); + } + + /** @override */ + public function visitYieldFrom(Node $node): void + { + $this->maybeInvoke($node->children['expr']); + } + + // TODO: Allow calls that accept scalars and regular data that wouldn't get modified? +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/CachingTolerantASTConverter.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/CachingTolerantASTConverter.php new file mode 100644 index 000000000..910b14125 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/CachingTolerantASTConverter.php @@ -0,0 +1,52 @@ + - an LRU cache of nodes used to generate php-ast nodes. + * The same PhpParser\Node can be used to create ast\Node instances in multiple ways (e.g. should_add_placeholders changes what gets generated) + */ + private static $php_parser_node_cache = []; + + /** + * @param Diagnostic[] &$errors @phan-output-reference (TODO: param-out) + * @override + */ + public static function phpParserParse(string $file_contents, array &$errors = []): SourceFileNode + { + $entry = self::$php_parser_node_cache[$file_contents] ?? null; + if ($entry) { + // This was recently used, move the entry to the end of the associative array. + unset(self::$php_parser_node_cache[$file_contents]); + self::$php_parser_node_cache[$file_contents] = $entry; + $errors = $entry->errors; + // \fwrite(\STDERR, "Found a cached entry for " . \md5($file_contents) . ' #errors=' . \count($errors) . "\n"); + return $entry->node; + } + $new_errors = []; + $node = parent::phpParserParse($file_contents, $new_errors); + if (\count(self::$php_parser_node_cache) >= self::MAX_CACHE_SIZE) { + // The cache is full, evict the element at the beginning of the associative array. + \reset(self::$php_parser_node_cache); + unset(self::$php_parser_node_cache[\key(self::$php_parser_node_cache)]); + } + $entry = new PhpParserNodeEntry($node, $new_errors); + self::$php_parser_node_cache[$file_contents] = $entry; + + $errors = $new_errors; + // \fwrite(\STDERR, "Creating entry for " . \md5($file_contents) . ' #errors=' . \count($errors) . "\n"); + return $node; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/InvalidNodeException.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/InvalidNodeException.php new file mode 100644 index 000000000..245c3ee17 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/InvalidNodeException.php @@ -0,0 +1,17 @@ +file_contents = $file_contents; + $this->include_offset = $include_offset; + $this->include_token_kind = $include_token_kind; + $this->indent = $indent; + } + + /** + * Should this include the byte offset in the file where the node occurred? + */ + public function setIncludeOffset(bool $include_offset): void + { + $this->include_offset = $include_offset; + } + + /** + * Should this include the token kind (default is just the text of the token) + */ + public function setIncludeTokenKind(bool $include_token_kind): void + { + $this->include_token_kind = $include_token_kind; + } + + /** + * Sets the text used for indentation (e.g. 4 spaces) + * @suppress PhanUnreferencedPublicMethod + */ + public function setIndent(string $indent): void + { + $this->indent = $indent; + } + + /** + * Converts the class name of $ast_node to a short string describing that class name. + * Removes the common `Microsoft\\PhpParser\\` prefix + */ + public static function dumpClassName(Node $ast_node): string + { + $name = get_class($ast_node); + if (\stripos($name, 'Microsoft\\PhpParser\\') === 0) { + // Remove the PhpParser namespace + $name = (string)substr($name, 20); + } + return $name; + } + + /** + * Converts the class name of $token to a short string describing that class name. + * Removes the common `Microsoft\\PhpParser\\` prefix + */ + public static function dumpTokenClassName(Token $token): string + { + $name = get_class($token); + if (\stripos($name, 'Microsoft\\PhpParser\\') === 0) { + // Remove the PhpParser namespace + $name = (string)substr($name, 20); + } + return $name; + } + + /** + * @param Node|Token|null $ast_node + * @param string $padding (to be echoed before the current node + * @throws Exception for invalid $ast_node values + */ + public function dumpTreeAsString($ast_node, string $key = '', string $padding = ''): string + { + if ($ast_node instanceof Node) { + $first_part = \sprintf( + "%s%s%s%s\n", + $padding, + $key !== '' ? $key . ': ' : '', + self::dumpClassName($ast_node), + $this->include_offset ? ' (@' . $ast_node->getStart() . ')' : '' + ); + + $result = $first_part; + foreach ($ast_node->getChildNodesAndTokens() as $name => $child) { + $result .= $this->dumpTreeAsString($child, (string) $name, $padding . $this->indent); + } + return $result; + } elseif ($ast_node instanceof Token) { + return \sprintf( + "%s%s%s: %s%s%s: %s\n", + $padding, + $key !== '' ? $key . ': ' : '', + self::dumpTokenClassName($ast_node), + $ast_node->getTokenKindNameFromValue($ast_node->kind), + $this->include_token_kind ? '(' . $ast_node->kind . ')' : '', + $this->include_offset ? ' (@' . $ast_node->start . ')' : '', + \Phan\Library\StringUtil::jsonEncode(\substr($this->file_contents, $ast_node->fullStart, $ast_node->length)) + ); + } elseif (\is_scalar($ast_node)) { + return \var_export($ast_node, true); + } elseif ($ast_node === null) { + return 'null'; + } else { + $type = is_object($ast_node) ? get_class($ast_node) : gettype($ast_node); + throw new \InvalidArgumentException("Unexpected type of \$ast_node was seen in dumper: " . $type); + } + } + + /** + * @param Node|Token $ast_node + * @param string $padding (to be echoed before the current node + * @throws Exception for invalid $ast_node values + * @suppress PhanUnreferencedPublicMethod + * @suppress PhanPluginRemoveDebugEcho + */ + public function dumpTree($ast_node, string $key = '', string $padding = ''): void + { + echo $this->dumpTreeAsString($ast_node, $key, $padding); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/NodeUtils.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/NodeUtils.php new file mode 100644 index 000000000..9c045e2fa --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/NodeUtils.php @@ -0,0 +1,63 @@ +file_contents = $file_contents; + } + + /** + * Convert a token to the string it represents, without whitespace or `$`. + * + * @phan-suppress PhanPartialTypeMismatchArgumentInternal hopefully in range + */ + public function tokenToString(Token $n): string + { + $result = \trim($n->getText($this->file_contents)); + $kind = $n->kind; + if ($kind === TokenKind::VariableName) { + return \trim($result, '$'); + } + return $result; + } + + /** + * Converts a qualified name to the string it represents, combining name parts. + */ + public function phpParserNameToString(QualifiedName $name): string + { + $name_parts = $name->nameParts; + // TODO: Handle error case (can there be missing parts?) + $result = ''; + foreach ($name_parts as $part) { + $part_as_string = $this->tokenToString($part); + if ($part_as_string !== '') { + $result .= \trim($part_as_string); + } + } + $result = \rtrim(\preg_replace('/\\\\{2,}/', '\\', $result), '\\'); + if ($result === '') { + // Would lead to "The name cannot be empty" when parsing + throw new InvalidNodeException(); + } + return $result; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ParseException.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ParseException.php new file mode 100644 index 000000000..a8664ec6c --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ParseException.php @@ -0,0 +1,35 @@ +line_number_start = $line_number_start; + } + + /** + * Returns the line of the file being parsed that caused this ParseException. + * @suppress PhanUnreferencedPublicMethod added for API completeness. + */ + public function getLineNumberStart(): int + { + return $this->line_number_start; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ParseResult.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ParseResult.php new file mode 100644 index 000000000..4e4ccab92 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ParseResult.php @@ -0,0 +1,31 @@ +node = $node; + $this->diagnostics = $diagnostics; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/PhpParserNodeEntry.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/PhpParserNodeEntry.php new file mode 100644 index 000000000..f01e148c8 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/PhpParserNodeEntry.php @@ -0,0 +1,32 @@ +node = $node; + $this->errors = $errors; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/Shim.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/Shim.php new file mode 100644 index 000000000..afd279442 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/Shim.php @@ -0,0 +1,97 @@ += 80000 ? 15 : 20); + } + if (!defined('ast\flags\TYPE_MIXED')) { + define('ast\flags\TYPE_MIXED', \PHP_MAJOR_VERSION >= 80000 ? 16 : 21); + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ShimFunctions.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ShimFunctions.php new file mode 100644 index 000000000..fc4a7c5b3 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ShimFunctions.php @@ -0,0 +1,134 @@ + 'AST_ARG_LIST', + ast\AST_LIST => 'AST_LIST', + ast\AST_ARRAY => 'AST_ARRAY', + ast\AST_ENCAPS_LIST => 'AST_ENCAPS_LIST', + ast\AST_EXPR_LIST => 'AST_EXPR_LIST', + ast\AST_STMT_LIST => 'AST_STMT_LIST', + ast\AST_IF => 'AST_IF', + ast\AST_SWITCH_LIST => 'AST_SWITCH_LIST', + ast\AST_CATCH_LIST => 'AST_CATCH_LIST', + ast\AST_PARAM_LIST => 'AST_PARAM_LIST', + ast\AST_CLOSURE_USES => 'AST_CLOSURE_USES', + ast\AST_PROP_DECL => 'AST_PROP_DECL', + ast\AST_CONST_DECL => 'AST_CONST_DECL', + ast\AST_CLASS_CONST_DECL => 'AST_CLASS_CONST_DECL', + ast\AST_NAME_LIST => 'AST_NAME_LIST', + ast\AST_TRAIT_ADAPTATIONS => 'AST_TRAIT_ADAPTATIONS', + ast\AST_USE => 'AST_USE', + ast\AST_TYPE_UNION => 'AST_TYPE_UNION', + ast\AST_ATTRIBUTE_LIST => 'AST_ATTRIBUTE_LIST', + ast\AST_MATCH_ARM_LIST => 'AST_MATCH_ARM_LIST', + ast\AST_NAME => 'AST_NAME', + ast\AST_CLOSURE_VAR => 'AST_CLOSURE_VAR', + ast\AST_NULLABLE_TYPE => 'AST_NULLABLE_TYPE', + ast\AST_FUNC_DECL => 'AST_FUNC_DECL', + ast\AST_CLOSURE => 'AST_CLOSURE', + ast\AST_METHOD => 'AST_METHOD', + ast\AST_ARROW_FUNC => 'AST_ARROW_FUNC', + ast\AST_CLASS => 'AST_CLASS', + ast\AST_MAGIC_CONST => 'AST_MAGIC_CONST', + ast\AST_TYPE => 'AST_TYPE', + ast\AST_VAR => 'AST_VAR', + ast\AST_CONST => 'AST_CONST', + ast\AST_UNPACK => 'AST_UNPACK', + ast\AST_CAST => 'AST_CAST', + ast\AST_EMPTY => 'AST_EMPTY', + ast\AST_ISSET => 'AST_ISSET', + ast\AST_SHELL_EXEC => 'AST_SHELL_EXEC', + ast\AST_CLONE => 'AST_CLONE', + ast\AST_EXIT => 'AST_EXIT', + ast\AST_PRINT => 'AST_PRINT', + ast\AST_INCLUDE_OR_EVAL => 'AST_INCLUDE_OR_EVAL', + ast\AST_UNARY_OP => 'AST_UNARY_OP', + ast\AST_PRE_INC => 'AST_PRE_INC', + ast\AST_PRE_DEC => 'AST_PRE_DEC', + ast\AST_POST_INC => 'AST_POST_INC', + ast\AST_POST_DEC => 'AST_POST_DEC', + ast\AST_YIELD_FROM => 'AST_YIELD_FROM', + ast\AST_GLOBAL => 'AST_GLOBAL', + ast\AST_UNSET => 'AST_UNSET', + ast\AST_RETURN => 'AST_RETURN', + ast\AST_LABEL => 'AST_LABEL', + ast\AST_REF => 'AST_REF', + ast\AST_HALT_COMPILER => 'AST_HALT_COMPILER', + ast\AST_ECHO => 'AST_ECHO', + ast\AST_THROW => 'AST_THROW', + ast\AST_GOTO => 'AST_GOTO', + ast\AST_BREAK => 'AST_BREAK', + ast\AST_CONTINUE => 'AST_CONTINUE', + ast\AST_CLASS_NAME => 'AST_CLASS_NAME', + ast\AST_CLASS_CONST_GROUP => 'AST_CLASS_CONST_GROUP', + ast\AST_DIM => 'AST_DIM', + ast\AST_PROP => 'AST_PROP', + ast\AST_NULLSAFE_PROP => 'AST_NULLSAFE_PROP', + ast\AST_STATIC_PROP => 'AST_STATIC_PROP', + ast\AST_CALL => 'AST_CALL', + ast\AST_CLASS_CONST => 'AST_CLASS_CONST', + ast\AST_ASSIGN => 'AST_ASSIGN', + ast\AST_ASSIGN_REF => 'AST_ASSIGN_REF', + ast\AST_ASSIGN_OP => 'AST_ASSIGN_OP', + ast\AST_BINARY_OP => 'AST_BINARY_OP', + ast\AST_ARRAY_ELEM => 'AST_ARRAY_ELEM', + ast\AST_NEW => 'AST_NEW', + ast\AST_INSTANCEOF => 'AST_INSTANCEOF', + ast\AST_YIELD => 'AST_YIELD', + ast\AST_STATIC => 'AST_STATIC', + ast\AST_WHILE => 'AST_WHILE', + ast\AST_DO_WHILE => 'AST_DO_WHILE', + ast\AST_IF_ELEM => 'AST_IF_ELEM', + ast\AST_SWITCH => 'AST_SWITCH', + ast\AST_SWITCH_CASE => 'AST_SWITCH_CASE', + ast\AST_DECLARE => 'AST_DECLARE', + ast\AST_PROP_ELEM => 'AST_PROP_ELEM', + ast\AST_PROP_GROUP => 'AST_PROP_GROUP', + ast\AST_CONST_ELEM => 'AST_CONST_ELEM', + ast\AST_USE_TRAIT => 'AST_USE_TRAIT', + ast\AST_TRAIT_PRECEDENCE => 'AST_TRAIT_PRECEDENCE', + ast\AST_METHOD_REFERENCE => 'AST_METHOD_REFERENCE', + ast\AST_NAMESPACE => 'AST_NAMESPACE', + ast\AST_USE_ELEM => 'AST_USE_ELEM', + ast\AST_TRAIT_ALIAS => 'AST_TRAIT_ALIAS', + ast\AST_GROUP_USE => 'AST_GROUP_USE', + ast\AST_ATTRIBUTE => 'AST_ATTRIBUTE', + ast\AST_MATCH => 'AST_MATCH', + ast\AST_MATCH_ARM => 'AST_MATCH_ARM', + ast\AST_NAMED_ARG => 'AST_NAMED_ARG', + ast\AST_METHOD_CALL => 'AST_METHOD_CALL', + ast\AST_NULLSAFE_METHOD_CALL => 'AST_NULLSAFE_METHOD_CALL', + ast\AST_STATIC_CALL => 'AST_STATIC_CALL', + ast\AST_CONDITIONAL => 'AST_CONDITIONAL', + ast\AST_TRY => 'AST_TRY', + ast\AST_CATCH => 'AST_CATCH', + ast\AST_FOR => 'AST_FOR', + ast\AST_FOREACH => 'AST_FOREACH', + ]; + + /** + * Get a string representation of the AST kind value. + * @see Parser:: + */ + public static function getKindName(int $kind): string + { + $name = self::KIND_LOOKUP[$kind] ?? null; + if (!$name) { + throw new \LogicException("Unknown kind $kind"); + } + return $name; + } +} +Shim::load(); diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/StringUtil.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/StringUtil.php new file mode 100644 index 000000000..91615609e --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/StringUtil.php @@ -0,0 +1,201 @@ + '\\', + '$' => '$', + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'f' => "\f", + 'v' => "\v", + 'e' => "\x1B", + ]; + + /** + * @internal + * + * Parses a string token. + * + * @param string $str String token content + * + * @return string The parsed string + */ + public static function parse(string $str): string + { + $c = $str[0]; + if ($c === '<') { + return self::parseHeredoc($str); + } + $binary_length = 0; + if ('b' === $c || 'B' === $c) { + $binary_length = 1; + } + + if ('\'' === $str[$binary_length]) { + return str_replace( + ['\\\\', '\\\''], + ['\\', '\''], + // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal + substr($str, $binary_length + 1, -1) + ); + } else { + return self::parseEscapeSequences( + substr($str, $binary_length + 1, -1), + '"' + ); + } + } + + /** + * Converts a fragment of raw (possibly indented) + * heredoc to the string that the PHP interpreter would treat it as. + */ + public static function parseHeredoc(string $str): string + { + // TODO: handle dos newlines + // TODO: Parse escape sequences + $first_line_index = (int)strpos($str, "\n"); + $last_line_index = (int)strrpos($str, "\n"); + // $last_line = substr($str, $last_line_index + 1); + $spaces = strspn($str, " \t", $last_line_index + 1); + + // On Windows, the "\r" must also be removed from the last line of the heredoc + $inner = (string)substr($str, $first_line_index + 1, $last_line_index - ($first_line_index + 1) - ($str[$last_line_index - 1] === "\r" ? 1 : 0)); + + if ($spaces > 0) { + $inner = preg_replace("/^" . substr($str, $last_line_index + 1, $spaces) . "/m", '', $inner); + } + if (strpos(substr($str, 0, $first_line_index), "'") === false) { + // If the start of the here/nowdoc doesn't contain a "'", it's heredoc. + // The contents have to be unescaped. + return self::parseEscapeSequences($inner, null); + } + return $inner; + } + + /** + * Parses escape sequences in strings (all string types apart from single quoted). + * + * @param string|false $str String without quotes + * @param null|string $quote Quote type + * + * @return string String with escape sequences parsed + * @throws InvalidNodeException for invalid code points + */ + public static function parseEscapeSequences($str, ?string $quote): string + { + if (!is_string($str)) { + // Invalid AST input; give up + return ''; + } + if (null !== $quote) { + $str = str_replace('\\' . $quote, $quote, $str); + } + + return \preg_replace_callback( + '~\\\\([\\\\$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~', + /** + * @param list $matches + */ + static function (array $matches): string { + $str = $matches[1]; + + if (isset(self::REPLACEMENTS[$str])) { + return self::REPLACEMENTS[$str]; + } elseif ('x' === $str[0] || 'X' === $str[0]) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal, PhanPossiblyFalseTypeArgumentInternal + return chr(hexdec(substr($str, 1))); + } elseif ('u' === $str[0]) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + return self::codePointToUtf8(hexdec($matches[2])); + } else { + // @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal + return chr(octdec($str)); + } + }, + $str + ); + } + + /** + * Converts a Unicode code point to its UTF-8 encoded representation. + * + * @param int $num Code point + * + * @return string UTF-8 representation of code point + * + * @throws InvalidNodeException for invalid code points + */ + private static function codePointToUtf8(int $num): string + { + if ($num <= 0x7F) { + return chr($num); + } + if ($num <= 0x7FF) { + return chr(($num >> 6) + 0xC0) . chr(($num & 0x3F) + 0x80); + } + if ($num <= 0xFFFF) { + return chr(($num >> 12) + 0xE0) . chr((($num >> 6) & 0x3F) + 0x80) . chr(($num & 0x3F) + 0x80); + } + if ($num <= 0x1FFFFF) { + return chr(($num >> 18) + 0xF0) . chr((($num >> 12) & 0x3F) + 0x80) + . chr((($num >> 6) & 0x3F) + 0x80) . chr(($num & 0x3F) + 0x80); + } + throw new InvalidNodeException('Invalid UTF-8 codepoint escape sequence: Codepoint too large'); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverter.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverter.php new file mode 100644 index 000000000..4347b2167 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverter.php @@ -0,0 +1,3284 @@ + true, + TokenKind::OpenBraceToken => true, + TokenKind::DollarOpenBraceToken => true, + TokenKind::CloseBraceToken => true, + ]; + + // If this environment variable is set, this will throw. + // (For debugging, may be removed in the future) + public const ENV_AST_THROW_INVALID = 'AST_THROW_INVALID'; + + public const INCOMPLETE_CLASS_CONST = '__INCOMPLETE_CLASS_CONST__'; + public const INCOMPLETE_PROPERTY = '__INCOMPLETE_PROPERTY__'; + public const INCOMPLETE_VARIABLE = '__INCOMPLETE_VARIABLE__'; + + /** + * @var int - A version in SUPPORTED_AST_VERSIONS + */ + protected static $php_version_id_parsing = PHP_VERSION_ID; + + /** + * @var int - Internal counter for declarations, to generate __declId in `ast\Node`s for declarations. + */ + protected static $decl_id = 0; + + /** @var bool should placeholder nodes be added as child nodes instead of refusing to generate a Node for an invalid statement? */ + protected static $should_add_placeholders = false; + + /** @var string the contents of the file currently being parsed */ + protected static $file_contents = ''; + + /** @var FilePositionMap maps byte offsets of the currently parsed file to line numbers */ + protected static $file_position_map; + + /** @var bool Sets equivalent static option in self::_start_parsing() */ + protected $instance_should_add_placeholders = false; + + /** + * @var int can be used to tweak behavior for compatibility. + * Set to a newer version to support comments on class constants, etc. + */ + protected $instance_php_version_id_parsing = PHP_VERSION_ID; + + // No-op. + public function __construct() + { + } + + /** + * Controls whether this should add placeholders for nodes that couldn't be parsed + * (enabled for code completion) + */ + public function setShouldAddPlaceholders(bool $value): void + { + $this->instance_should_add_placeholders = $value; + } + + /** + * Records the PHP major+minor version id (70100, 70200, etc.) + * that this polyfill should emulate the behavior of php-ast for. + */ + public function setPHPVersionId(int $value): void + { + $this->instance_php_version_id_parsing = $value; + } + + /** + * Generates an ast\Node with this converter's current settings. (caching if $cache is non-null) + * + * @param Diagnostic[] &$errors @phan-output-reference + * @param ?Cache $cache + * @throws InvalidArgumentException if the requested AST version is invalid. + */ + public function parseCodeAsPHPAST(string $file_contents, int $version, array &$errors = [], Cache $cache = null): \ast\Node + { + if (!\in_array($version, self::SUPPORTED_AST_VERSIONS, true)) { + throw new \InvalidArgumentException(sprintf("Unexpected version: want %s, got %d", \implode(', ', self::SUPPORTED_AST_VERSIONS), $version)); + } + $errors = []; + $cache_key = null; + if ($cache) { + $cache_key = $this->generateCacheKey($file_contents, $version); + $result = \Phan\Library\StringUtil::isNonZeroLengthString($cache_key) ? $cache->getIfExists($cache_key) : null; + if ($result) { + $errors = $result->diagnostics; + return $result->node; + } + } + $result = $this->parseCodeAsPHPASTUncached($file_contents, $version, $errors); + if ($cache && \Phan\Library\StringUtil::isNonZeroLengthString($cache_key)) { + $cache->save($cache_key, new ParseResult($result, $errors)); + } + return $result; + } + + /** + * Generates an ast\Node with this converter's current settings. + * + * @param Diagnostic[] &$errors @phan-output-reference + * @throws InvalidArgumentException if the requested AST version is invalid. + */ + public function parseCodeAsPHPASTUncached(string $file_contents, int $version, array &$errors = []): \ast\Node + { + $parser_node = static::phpParserParse($file_contents, $errors); + try { + return $this->phpParserToPhpast($parser_node, $version, $file_contents); + } finally { + // Remove object reference cycles manually to free memory - automatic cyclic garbage collection is disabled for performance in older php 7 versions. + self::unlinkDescendantNodes($parser_node); + } + } + + /** + * Unlink the nodes manually to free memory (or to exclude them from var_export()) + * + * Automatic cyclic garbage collection is disabled for performance in older php 7 versions. + */ + public static function unlinkDescendantNodes(SourceFileNode $root): void + { + // Avoid creating cyclic data structures. + // Node->getRoot() requires a valid parent node path to a SourceFileNode because it needs getDocCommentText() to work. + $placeholder_root = new SourceFileNode(); + $placeholder_root->fileContents = $root->fileContents; + + foreach ($root->getDescendantNodes() as $descendant) { + $descendant->parent = $placeholder_root; + } + $root->parent = null; + } + + /** + * @param Diagnostic[] &$errors @phan-output-reference (TODO: param-out) + */ + public static function phpParserParse(string $file_contents, array &$errors = []): PhpParser\Node\SourceFileNode + { + $parser = new Parser(); // TODO: In php 7.3, we might need to provide a version, due to small changes in lexing? + $result = $parser->parseSourceFile($file_contents); + $errors = DiagnosticsProvider::getDiagnostics($result); + return $result; + } + + /** + * Visible for testing + * + * @param PhpParser\Node $parser_node + * @param int $ast_version + * @param string $file_contents + * @throws InvalidArgumentException if the provided AST version isn't valid + */ + public function phpParserToPhpast(PhpParser\Node $parser_node, int $ast_version, string $file_contents): \ast\Node + { + if (!\in_array($ast_version, self::SUPPORTED_AST_VERSIONS, true)) { + throw new \InvalidArgumentException(sprintf("Unexpected version: want %s, got %d", implode(', ', self::SUPPORTED_AST_VERSIONS), $ast_version)); + } + $this->startParsing($file_contents); + $stmts = static::phpParserNodeToAstNode($parser_node); + // return static::normalizeNamespaces($stmts); + return $stmts; + } + + protected function startParsing(string $file_contents): void + { + self::$decl_id = 0; + self::$should_add_placeholders = $this->instance_should_add_placeholders; + self::$php_version_id_parsing = $this->instance_php_version_id_parsing; + self::$file_position_map = new FilePositionMap($file_contents); + // $file_contents required for looking up line numbers. + // TODO: Other data structures? + self::$file_contents = $file_contents; + } + + /** + * @param null|bool|int|string|PhpParser\Node|Token|(PhpParser\Node|Token)[] $n + * @throws Exception if node is invalid + */ + protected static function debugDumpNodeOrToken($n): string + { + if (\is_scalar($n)) { + return var_export($n, true); + } + if (!\is_array($n)) { + $n = [$n]; + } + $result = []; + foreach ($n as $e) { + $dumper = new NodeDumper(self::$file_contents); + $dumper->setIncludeTokenKind(true); + $result[] = $dumper->dumpTreeAsString($e); + } + return implode("\n", $result); + } + + /** + * @param Token|PhpParser\Node[]|PhpParser\Node\StatementNode $parser_nodes + * This is represented as a single node for `if` with a colon (macro style) + * @param ?int $lineno + * @param bool $return_null_on_empty (return null if non-array (E.g. semicolon is seen)) + * @return ?ast\Node + * Throws RuntimeException|Exception if the statement list is invalid + * @suppress PhanThrowTypeAbsentForCall|PhanThrowTypeMismatchForCall + */ + private static function phpParserStmtlistToAstNode($parser_nodes, ?int $lineno, bool $return_null_on_empty = false): ?\ast\Node + { + if ($parser_nodes instanceof PhpParser\Node\Statement\CompoundStatementNode) { + $parser_nodes = $parser_nodes->statements; + } elseif ($parser_nodes instanceof PhpParser\Node\StatementNode) { + if ($parser_nodes instanceof PhpParser\Node\Statement\EmptyStatement) { + $parser_nodes = []; + } else { + $parser_nodes = [$parser_nodes]; + } + } elseif ($parser_nodes instanceof Token) { + if ($parser_nodes->kind === TokenKind::SemicolonToken) { + if ($return_null_on_empty) { + return null; + } + return new ast\Node( + ast\AST_STMT_LIST, + 0, + [], + $lineno ?? 0 + ); + } + } + + if (!\is_array($parser_nodes)) { + throw new RuntimeException("Unexpected type for statements: " . static::debugDumpNodeOrToken($parser_nodes)); + } + $children = []; + foreach ($parser_nodes as $parser_node) { + try { + $child_node = static::phpParserNodeToAstNode($parser_node); + } catch (InvalidNodeException $_) { + continue; + } + if (\is_array($child_node)) { + // EchoExpression returns multiple children. + foreach ($child_node as $child_node_part) { + $children[] = $child_node_part; + } + } elseif (!\is_null($child_node)) { + $children[] = $child_node; + } + } + if (!\is_int($lineno)) { + foreach ($parser_nodes as $parser_node) { + $child_node_line = static::getEndLine($parser_node); + if ($child_node_line > 0) { + $lineno = $child_node_line; + break; + } + } + } + return new ast\Node(ast\AST_STMT_LIST, 0, $children, $lineno ?? 0); + } + + private static function phpParserExprListToExprList(PhpParser\Node\DelimitedList\ExpressionList $expressions_list, int $lineno): ast\Node + { + $children = []; + $expressions_children = $expressions_list->children; + foreach ($expressions_children as $expr) { + if ($expr instanceof Token && $expr->kind === TokenKind::CommaToken) { + continue; + } + $child_node = static::phpParserNodeToAstNode($expr); + if (\is_array($child_node)) { + // EchoExpression returns multiple children in php-ast + foreach ($child_node as $child_node_part) { + $children[] = $child_node_part; + } + } elseif (!\is_null($child_node)) { + $children[] = $child_node; + } + } + foreach ($expressions_children as $parser_node) { + $child_node_line = static::getEndLine($parser_node); + if ($child_node_line > 0) { + $lineno = $child_node_line; + break; + } + } + return new ast\Node( + ast\AST_EXPR_LIST, + 0, + $children, + $lineno + ); + } + + /** + * @param PhpParser\Node|Token $n - The node from PHP-Parser + * @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent. + * This does not convert names to ast\AST_CONST. + * @throws InvalidArgumentException if Phan doesn't know what $n is + */ + protected static function phpParserNonValueNodeToAstNode($n) + { + static $callback_map; + static $fallback_closure; + if (\is_null($callback_map)) { + $callback_map = static::initHandleMap(); + /** + * @param PhpParser\Node|Token $n + * @return ast\Node - Not a real node, but a node indicating the TODO + * @throws InvalidArgumentException for invalid node classes + * @throws Error if the environment variable AST_THROW_INVALID is set (for debugging) + */ + $fallback_closure = static function ($n, int $unused_start_line): \ast\Node { + if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) { + // @phan-suppress-next-line PhanThrowTypeMismatchForCall debugDumpNodeOrToken can throw + throw new \InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . static::debugDumpNodeOrToken($n)); + } + return static::astStub($n); + }; + } + $callback = $callback_map[\get_class($n)] ?? $fallback_closure; + // @phan-suppress-next-line PhanThrowTypeMismatch + return $callback($n, self::getStartLine($n)); + } + + /** + * @param PhpParser\Node|Token $n - The node from PHP-Parser + * @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent. + * Generates a valid placeholder for invalid nodes if $should_add_placeholders is true. + * @throws InvalidNodeException when self::$should_add_placeholders is false, like many of these methods. + */ + protected static function phpParserNodeToAstNodeOrPlaceholderExpr($n) + { + if (!self::$should_add_placeholders) { + return static::phpParserNodeToAstNode($n); + } + try { + return static::phpParserNodeToAstNode($n); + } catch (InvalidNodeException $_) { + return static::newPlaceholderExpression($n); + } + } + + /** + * @param PhpParser\Node|Token $n - The node from PHP-Parser + * @return ast\Node|ast\Node[]|string|int|float|null - whatever ast\parse_code would return as the equivalent. + */ + protected static function phpParserNodeToAstNode($n) + { + static $callback_map; + static $fallback_closure; + if (\is_null($callback_map)) { + $callback_map = static::initHandleMap(); + /** + * @param PhpParser\Node|Token $n + * @return ast\Node - Not a real node, but a node indicating the TODO + * @throws InvalidArgumentException|Exception for invalid node classes + * @throws Error if the environment variable AST_THROW_INVALID is set to debug. + */ + $fallback_closure = static function ($n, int $unused_start_line): \ast\Node { + if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) { + throw new \InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . static::debugDumpNodeOrToken($n)); + } + + return static::astStub($n); + }; + } + $callback = $callback_map[\get_class($n)] ?? $fallback_closure; + // @phan-suppress-next-line PhanThrowTypeAbsent + $result = $callback($n, self::$file_position_map->getStartLine($n)); + if (($result instanceof ast\Node) && $result->kind === ast\AST_NAME) { + return new ast\Node(ast\AST_CONST, 0, ['name' => $result], $result->lineno); + } + return $result; + } + + /** + * @param PhpParser\Node|Token $n + * @throws InvalidNodeException if this was called on an unexpected type + */ + final protected static function getStartLine($n): int + { + if (\is_object($n)) { + return self::$file_position_map->getStartLine($n); + } + throw new InvalidNodeException(); + } + + /** + * @param ?PhpParser\Node|?Token $n + * @throws InvalidNodeException if this was called on an unexpected type + */ + final protected static function getEndLine($n): int + { + if (!\is_object($n)) { + if (\is_null($n)) { + return 0; + } + throw new InvalidNodeException(); + } + return self::$file_position_map->getEndLine($n); + } + + /** + * This returns an array of values mapping class names to the closures which converts them to a scalar or ast\Node + * + * Why not a switch? Switches are slow until php 7.2, and there are dozens of class names to handle. + * + * - In php <= 7.1, the interpreter would loop through all possible cases, and compare against the value one by one. + * - There are a lot of local variables to look at. + * + * @return array + */ + protected static function initHandleMap(): array + { + $closures = [ + /** @return ?ast\Node */ + 'Microsoft\PhpParser\Node\SourceFileNode' => static function (PhpParser\Node\SourceFileNode $n, int $start_line): ?\ast\Node { + return static::phpParserStmtlistToAstNode($n->statementList, $start_line, false); + }, + /** @return mixed */ + 'Microsoft\PhpParser\Node\Expression\ArgumentExpression' => static function (PhpParser\Node\Expression\ArgumentExpression $n, int $start_line) { + $result = static::phpParserNodeToAstNode($n->expression); + if ($n->dotDotDotToken !== null) { + return new ast\Node(ast\AST_UNPACK, 0, ['expr' => $result], $start_line); + } + return $result; + }, + /** + * @return ast\Node|string|int|float + * @throws InvalidNodeException + */ + 'Microsoft\PhpParser\Node\Expression\SubscriptExpression' => static function (PhpParser\Node\Expression\SubscriptExpression $n, int $start_line) { + $expr = static::phpParserNodeToAstNode($n->postfixExpression); + try { + return new ast\Node( + ast\AST_DIM, + ($n->openBracketOrBrace->kind ?? null) === TokenKind::OpenBraceToken ? ast\flags\DIM_ALTERNATIVE_SYNTAX : 0, + [ + 'expr' => $expr, + 'dim' => $n->accessExpression !== null ? static::phpParserNodeToAstNode($n->accessExpression) : null, + ], + $start_line + ); + } catch (InvalidNodeException $_) { + return $expr; + } + }, + /** @return ?(ast\Node|float|int|string) */ + 'Microsoft\PhpParser\Node\Expression\AssignmentExpression' => static function (PhpParser\Node\Expression\AssignmentExpression $n, int $start_line) { + try { + $var_node = static::phpParserNodeToAstNode($n->leftOperand); + } catch (InvalidNodeException $_) { + if (self::$should_add_placeholders) { + $var_node = new ast\Node(ast\AST_VAR, 0, ['name' => self::INCOMPLETE_VARIABLE], $start_line); + } else { + // convert `;= $b;` to `;$b;` + return static::phpParserNodeToAstNode($n->rightOperand); + } + } + $expr_node = static::phpParserNodeToAstNodeOrPlaceholderExpr($n->rightOperand); + // FIXME switch on $n->kind + return static::astNodeAssign( + $var_node, + $expr_node, + $start_line, + $n->byRef !== null + ); + }, + /** + * @return ast\Node|string|float|int (can return a non-Node if the left or right-hand side could not be parsed + */ + 'Microsoft\PhpParser\Node\Expression\BinaryExpression' => static function (PhpParser\Node\Expression\BinaryExpression $n, int $start_line) { + static $lookup = [ + TokenKind::AmpersandAmpersandToken => flags\BINARY_BOOL_AND, + TokenKind::AmpersandToken => flags\BINARY_BITWISE_AND, + TokenKind::AndKeyword => flags\BINARY_BOOL_AND, + TokenKind::AsteriskAsteriskToken => flags\BINARY_POW, + TokenKind::AsteriskToken => flags\BINARY_MUL, + TokenKind::BarBarToken => flags\BINARY_BOOL_OR, + TokenKind::BarToken => flags\BINARY_BITWISE_OR, + TokenKind::CaretToken => flags\BINARY_BITWISE_XOR, + TokenKind::DotToken => flags\BINARY_CONCAT, + TokenKind::EqualsEqualsEqualsToken => flags\BINARY_IS_IDENTICAL, + TokenKind::EqualsEqualsToken => flags\BINARY_IS_EQUAL, + TokenKind::ExclamationEqualsEqualsToken => flags\BINARY_IS_NOT_IDENTICAL, + TokenKind::ExclamationEqualsToken => flags\BINARY_IS_NOT_EQUAL, + TokenKind::GreaterThanEqualsToken => flags\BINARY_IS_GREATER_OR_EQUAL, + TokenKind::GreaterThanGreaterThanToken => flags\BINARY_SHIFT_RIGHT, + TokenKind::GreaterThanToken => flags\BINARY_IS_GREATER, + TokenKind::LessThanEqualsGreaterThanToken => flags\BINARY_SPACESHIP, + TokenKind::LessThanEqualsToken => flags\BINARY_IS_SMALLER_OR_EQUAL, + TokenKind::LessThanLessThanToken => flags\BINARY_SHIFT_LEFT, + TokenKind::LessThanToken => flags\BINARY_IS_SMALLER, + TokenKind::MinusToken => flags\BINARY_SUB, + TokenKind::OrKeyword => flags\BINARY_BOOL_OR, + TokenKind::PercentToken => flags\BINARY_MOD, + TokenKind::PlusToken => flags\BINARY_ADD, + TokenKind::QuestionQuestionToken => flags\BINARY_COALESCE, + TokenKind::SlashToken => flags\BINARY_DIV, + TokenKind::XorKeyword => flags\BINARY_BOOL_XOR, + ]; + static $assign_lookup = [ + TokenKind::AmpersandEqualsToken => flags\BINARY_BITWISE_AND, + TokenKind::AsteriskAsteriskEqualsToken => flags\BINARY_POW, + TokenKind::AsteriskEqualsToken => flags\BINARY_MUL, + TokenKind::BarEqualsToken => flags\BINARY_BITWISE_OR, + TokenKind::CaretEqualsToken => flags\BINARY_BITWISE_XOR, + TokenKind::DotEqualsToken => flags\BINARY_CONCAT, + TokenKind::MinusEqualsToken => flags\BINARY_SUB, + TokenKind::PercentEqualsToken => flags\BINARY_MOD, + TokenKind::PlusEqualsToken => flags\BINARY_ADD, + TokenKind::SlashEqualsToken => flags\BINARY_DIV, + TokenKind::GreaterThanGreaterThanEqualsToken => flags\BINARY_SHIFT_RIGHT, + TokenKind::LessThanLessThanEqualsToken => flags\BINARY_SHIFT_LEFT, + TokenKind::QuestionQuestionEqualsToken => flags\BINARY_COALESCE, + ]; + $kind = $n->operator->kind; + if ($kind === TokenKind::InstanceOfKeyword) { + return new ast\Node(ast\AST_INSTANCEOF, 0, [ + 'expr' => static::phpParserNodeToAstNode($n->leftOperand), + 'class' => static::phpParserNonValueNodeToAstNode($n->rightOperand), + ], $start_line); + } + $ast_kind = $lookup[$kind] ?? null; + if ($ast_kind === null) { + $ast_kind = $assign_lookup[$kind] ?? null; + if ($ast_kind === null) { + throw new AssertionError("missing $kind (" . Token::getTokenKindNameFromValue($kind) . ")"); + } + return static::astNodeAssignop($ast_kind, $n, $start_line); + } + return static::astNodeBinaryop($ast_kind, $n, $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\UnaryOpExpression' => static function (PhpParser\Node\Expression\UnaryOpExpression $n, int $start_line): ast\Node { + static $lookup = [ + TokenKind::TildeToken => flags\UNARY_BITWISE_NOT, + TokenKind::MinusToken => flags\UNARY_MINUS, + TokenKind::PlusToken => flags\UNARY_PLUS, + TokenKind::ExclamationToken => flags\UNARY_BOOL_NOT, + ]; + $kind = $n->operator->kind; + $ast_kind = $lookup[$kind] ?? null; + if ($ast_kind === null) { + throw new AssertionError("missing $kind(" . Token::getTokenKindNameFromValue($kind) . ")"); + } + return new ast\Node( + ast\AST_UNARY_OP, + $ast_kind, + ['expr' => static::phpParserNodeToAstNode($n->operand)], + $start_line + ); + }, + 'Microsoft\PhpParser\Node\Expression\CastExpression' => static function (PhpParser\Node\Expression\CastExpression $n, int $start_line): ast\Node { + static $lookup = [ + // From Parser->parseCastExpression() + TokenKind::ArrayCastToken => flags\TYPE_ARRAY, + TokenKind::BoolCastToken => flags\TYPE_BOOL, + TokenKind::DoubleCastToken => flags\TYPE_DOUBLE, + TokenKind::IntCastToken => flags\TYPE_LONG, + TokenKind::ObjectCastToken => flags\TYPE_OBJECT, + TokenKind::StringCastToken => flags\TYPE_STRING, + TokenKind::UnsetCastToken => flags\TYPE_NULL, + + // From Parser->parseCastExpressionGranular() + // This is a syntax error, but try to match what the intent was + TokenKind::ArrayKeyword => flags\TYPE_ARRAY, + TokenKind::BinaryReservedWord => flags\TYPE_STRING, + TokenKind::BoolReservedWord => flags\TYPE_BOOL, + TokenKind::BooleanReservedWord => flags\TYPE_BOOL, + TokenKind::DoubleReservedWord => flags\TYPE_DOUBLE, + TokenKind::IntReservedWord => flags\TYPE_LONG, + TokenKind::IntegerReservedWord => flags\TYPE_LONG, + TokenKind::FloatReservedWord => flags\TYPE_DOUBLE, + TokenKind::ObjectReservedWord => flags\TYPE_OBJECT, + TokenKind::RealReservedWord => flags\TYPE_DOUBLE, + TokenKind::StringReservedWord => flags\TYPE_STRING, + TokenKind::UnsetKeyword => flags\TYPE_NULL, + TokenKind::StaticKeyword => flags\TYPE_STATIC, + ]; + $kind = $n->castType->kind; + $ast_kind = $lookup[$kind] ?? null; + if ($ast_kind === null) { + throw new AssertionError("missing $kind"); + } + return new ast\Node( + ast\AST_CAST, + $ast_kind, + ['expr' => static::phpParserNodeToAstNode($n->operand)], + static::getEndLine($n) ?: $start_line + ); + }, + 'Microsoft\PhpParser\Node\Expression\AnonymousFunctionCreationExpression' => static function ( + PhpParser\Node\Expression\AnonymousFunctionCreationExpression $n, + int $start_line + ): ast\Node { + $ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line); + if (($ast_return_type->children['name'] ?? null) === '') { + $ast_return_type = null; + } + if ($n->questionToken !== null && $ast_return_type !== null) { + $ast_return_type = new ast\Node(ast\AST_NULLABLE_TYPE, 0, ['type' => $ast_return_type], $start_line); + } + $use_variable_name_list = $n->anonymousFunctionUseClause->useVariableNameList ?? null; + if (!$use_variable_name_list instanceof PhpParser\Node\DelimitedList\UseVariableNameList) { + $use_variable_name_list = null; + } + return static::astDeclClosure( + $n->byRefToken !== null, + $n->staticModifier !== null, + static::phpParserParamsToAstParams($n->parameters, $start_line), + static::phpParserClosureUsesToAstClosureUses($use_variable_name_list, $start_line), + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable, PhanPossiblyUndeclaredProperty return_null_on_empty is false. + static::phpParserStmtlistToAstNode($n->compoundStatementOrSemicolon->statements, self::getStartLine($n->compoundStatementOrSemicolon), false), + $ast_return_type, + $start_line, + static::getEndLine($n), + static::resolveDocCommentForClosure($n) + ); + }, + 'Microsoft\PhpParser\Node\Expression\ArrowFunctionCreationExpression' => static function ( + PhpParser\Node\Expression\ArrowFunctionCreationExpression $n, + int $start_line + ): ast\Node { + $ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line); + if (($ast_return_type->children['name'] ?? null) === '') { + $ast_return_type = null; + } + if ($n->questionToken !== null && $ast_return_type !== null) { + $ast_return_type = new ast\Node(ast\AST_NULLABLE_TYPE, 0, ['type' => $ast_return_type], $start_line); + } + $return_line = self::getStartLine($n->resultExpression); + return static::newASTDecl( + ast\AST_ARROW_FUNC, + ($n->byRefToken !== null ? flags\FUNC_RETURNS_REF : 0) | ($n->staticModifier !== null ? flags\MODIFIER_STATIC : null), + [ + 'params' => static::phpParserParamsToAstParams($n->parameters, $start_line), + 'stmts' => new ast\Node( + ast\AST_RETURN, + 0, + ['expr' => static::phpParserNodeToAstNode($n->resultExpression)], + $return_line + ), + 'returnType' => $ast_return_type, + ], + $start_line, + static::resolveDocCommentForClosure($n), + '{closure}', + static::getEndLine($n), + self::nextDeclId() + ); + }, + /** + * @throws InvalidNodeException if the resulting AST would not be analyzable by Phan + */ + 'Microsoft\PhpParser\Node\Expression\ScopedPropertyAccessExpression' => static function (PhpParser\Node\Expression\ScopedPropertyAccessExpression $n, int $start_line): ?\ast\Node { + $member_name = $n->memberName; + if ($member_name instanceof PhpParser\Node\Expression\Variable) { + try { + $prop_node = static::phpParserNodeToAstNode($member_name->name); + } catch (InvalidNodeException $e) { + if (self::$should_add_placeholders) { + $prop_node = ''; + } else { + throw $e; + } + } + return new ast\Node( + ast\AST_STATIC_PROP, + 0, + [ + 'class' => static::phpParserNonValueNodeToAstNode($n->scopeResolutionQualifier), + 'prop' => $prop_node, + ], + $start_line + ); + } else { + if ($member_name instanceof Token) { + if (\get_class($member_name) !== Token::class) { + if (self::$should_add_placeholders) { + $member_name = self::INCOMPLETE_CLASS_CONST; + } else { + throw new InvalidNodeException(); + } + } else { + $member_name = static::tokenToString($member_name); + } + } else { + // E.g. Node\Expression\BracedExpression + throw new InvalidNodeException(); + } + return static::phpParserClassConstFetchToAstClassConstFetch($n->scopeResolutionQualifier, $member_name, $start_line); + } + }, + 'Microsoft\PhpParser\Node\Expression\CloneExpression' => static function (PhpParser\Node\Expression\CloneExpression $n, int $start_line): ast\Node { + return new ast\Node(ast\AST_CLONE, 0, ['expr' => static::phpParserNodeToAstNode($n->expression)], $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\ErrorControlExpression' => static function (PhpParser\Node\Expression\ErrorControlExpression $n, int $start_line): ast\Node { + return new ast\Node( + ast\AST_UNARY_OP, + flags\UNARY_SILENCE, + ['expr' => static::phpParserNodeToAstNode($n->operand)], + $start_line + ); + }, + 'Microsoft\PhpParser\Node\Expression\EmptyIntrinsicExpression' => static function (PhpParser\Node\Expression\EmptyIntrinsicExpression $n, int $start_line): ast\Node { + return new ast\Node(ast\AST_EMPTY, 0, ['expr' => static::phpParserNodeToAstNode($n->expression)], $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\EvalIntrinsicExpression' => static function (PhpParser\Node\Expression\EvalIntrinsicExpression $n, int $start_line): ast\Node { + return new ast\Node( + ast\AST_INCLUDE_OR_EVAL, + flags\EXEC_EVAL, + ['expr' => static::phpParserNodeToAstNode($n->expression)], + $start_line + ); + }, + /** @return string|ast\Node */ + 'Microsoft\PhpParser\Token' => static function (PhpParser\Token $token, int $start_line) { + $kind = $token->kind; + $str = static::tokenToString($token); + if ($kind === TokenKind::StaticKeyword) { + return new ast\Node(ast\AST_NAME, flags\NAME_NOT_FQ, ['name' => $str], $start_line); + } + return $str; + }, + /** + * @throws InvalidNodeException + */ + 'Microsoft\PhpParser\MissingToken' => static function (PhpParser\MissingToken $unused_node, int $_): void { + throw new InvalidNodeException(); + }, + /** + * @throws InvalidNodeException + */ + 'Microsoft\PhpParser\SkippedToken' => static function (PhpParser\SkippedToken $unused_node, int $_): void { + throw new InvalidNodeException(); + }, + 'Microsoft\PhpParser\Node\Expression\ExitIntrinsicExpression' => static function (PhpParser\Node\Expression\ExitIntrinsicExpression $n, int $start_line): ast\Node { + $expression = $n->expression; + $expr_node = $expression !== null ? static::phpParserNodeToAstNode($expression) : null; + return new ast\Node(ast\AST_EXIT, 0, ['expr' => $expr_node], $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\CallExpression' => static function (PhpParser\Node\Expression\CallExpression $n, int $start_line): ast\Node { + $callable_expression = $n->callableExpression; + $arg_list = static::phpParserArgListToAstArgList($n->argumentExpressionList, $start_line); + if ($callable_expression instanceof PhpParser\Node\Expression\MemberAccessExpression) { // $a->f() + return static::astNodeMethodCall( + $callable_expression->arrowToken->kind === TokenKind::QuestionArrowToken ? ast\AST_NULLSAFE_METHOD_CALL : ast\AST_METHOD_CALL, + static::phpParserNonValueNodeToAstNode($callable_expression->dereferencableExpression), + static::phpParserNodeToAstNode($callable_expression->memberName), + $arg_list, + $start_line + ); + } elseif ($callable_expression instanceof PhpParser\Node\Expression\ScopedPropertyAccessExpression) { // a::f() + return static::astNodeStaticCall( + static::phpParserNonValueNodeToAstNode($callable_expression->scopeResolutionQualifier), + static::phpParserNodeToAstNode($callable_expression->memberName), + $arg_list, + $start_line + ); + } else { // f() + return static::astNodeCall( + static::phpParserNonValueNodeToAstNode($callable_expression), + $arg_list, + $start_line + ); + } + }, + 'Microsoft\PhpParser\Node\Expression\ScriptInclusionExpression' => static function (PhpParser\Node\Expression\ScriptInclusionExpression $n, int $start_line): ast\Node { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall should not happen + $flags = static::phpParserIncludeTokenToAstIncludeFlags($n->requireOrIncludeKeyword); + return new ast\Node( + ast\AST_INCLUDE_OR_EVAL, + $flags, + ['expr' => static::phpParserNodeToAstNode($n->expression)], + $start_line + ); + }, + /** + * @return ?ast\Node + */ + 'Microsoft\PhpParser\Node\Expression\IssetIntrinsicExpression' => static function (PhpParser\Node\Expression\IssetIntrinsicExpression $n, int $start_line): ?\ast\Node { + $ast_issets = []; + foreach ($n->expressions->children ?? [] as $var) { + if ($var instanceof Token) { + if ($var->kind === TokenKind::CommaToken) { + continue; + } elseif ($var->length === 0) { + continue; + } + } + $ast_issets[] = new ast\Node(ast\AST_ISSET, 0, [ + 'var' => static::phpParserNodeToAstNode($var), + ], $start_line); + } + $e = $ast_issets[0] ?? null; + for ($i = 1; $i < \count($ast_issets); $i++) { + $right = $ast_issets[$i]; + $e = new ast\Node( + ast\AST_BINARY_OP, + flags\BINARY_BOOL_AND, + [ + 'left' => $e, + 'right' => $right, + ], + // $e should always be set + $e->lineno ?? 0 + ); + } + return $e; + }, + 'Microsoft\PhpParser\Node\Expression\ArrayCreationExpression' => static function (PhpParser\Node\Expression\ArrayCreationExpression $n, int $start_line): ast\Node { + return static::phpParserArrayToAstArray($n, $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\ListIntrinsicExpression' => static function (PhpParser\Node\Expression\ListIntrinsicExpression $n, int $start_line): ast\Node { + return static::phpParserListToAstList($n, $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\ObjectCreationExpression' => static function (PhpParser\Node\Expression\ObjectCreationExpression $n, int $start_line): ast\Node { + $end_line = static::getEndLine($n); + $class_type_designator = $n->classTypeDesignator; + if ($class_type_designator instanceof Token && $class_type_designator->kind === TokenKind::ClassKeyword) { + // Node of type AST_CLASS + $base_class = $n->classBaseClause->baseClass ?? null; + $class_node = static::astStmtClass( + flags\CLASS_ANONYMOUS, + null, + $base_class !== null ? static::phpParserNonValueNodeToAstNode($base_class) : null, + $n->classInterfaceClause, + static::phpParserStmtlistToAstNode($n->classMembers->classMemberDeclarations ?? [], $start_line, false), + $start_line, + $end_line, + $n->getDocCommentText() + ); + } else { + $class_node = static::phpParserNonValueNodeToAstNode($class_type_designator); + } + return new ast\Node(ast\AST_NEW, 0, [ + 'class' => $class_node, + 'args' => static::phpParserArgListToAstArgList($n->argumentExpressionList, $start_line), + ], $start_line); + }, + /** @return mixed */ + 'Microsoft\PhpParser\Node\Expression\ParenthesizedExpression' => static function (PhpParser\Node\Expression\ParenthesizedExpression $n, int $_) { + return static::phpParserNodeToAstNode($n->expression); + }, + 'Microsoft\PhpParser\Node\Expression\PrefixUpdateExpression' => static function (PhpParser\Node\Expression\PrefixUpdateExpression $n, int $start_line): ast\Node { + $type = $n->incrementOrDecrementOperator->kind === TokenKind::PlusPlusToken ? ast\AST_PRE_INC : ast\AST_PRE_DEC; + + return new ast\Node($type, 0, ['var' => static::phpParserNodeToAstNode($n->operand)], $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\PostfixUpdateExpression' => static function (PhpParser\Node\Expression\PostfixUpdateExpression $n, int $start_line): ast\Node { + $type = $n->incrementOrDecrementOperator->kind === TokenKind::PlusPlusToken ? ast\AST_POST_INC : ast\AST_POST_DEC; + + return new ast\Node($type, 0, ['var' => static::phpParserNodeToAstNode($n->operand)], $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\PrintIntrinsicExpression' => static function (PhpParser\Node\Expression\PrintIntrinsicExpression $n, int $start_line): ast\Node { + return new ast\Node( + ast\AST_PRINT, + 0, + ['expr' => static::phpParserNodeToAstNode($n->expression)], + $start_line + ); + }, + /** @return ?ast\Node */ + 'Microsoft\PhpParser\Node\Expression\MemberAccessExpression' => static function (PhpParser\Node\Expression\MemberAccessExpression $n, int $start_line): ?\ast\Node { + return static::phpParserMemberAccessExpressionToAstProp($n, $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\TernaryExpression' => static function (TernaryExpression $n, int $start_line): ast\Node { + $n = self::normalizeTernaryExpression($n); + $is_parenthesized = $n->parent instanceof PhpParser\Node\Expression\ParenthesizedExpression; + $result = new ast\Node( + ast\AST_CONDITIONAL, + $is_parenthesized ? ast\flags\PARENTHESIZED_CONDITIONAL : 0, + [ + 'cond' => static::phpParserNodeToAstNode($n->condition), + 'true' => $n->ifExpression !== null ? static::phpParserNodeToAstNode($n->ifExpression) : null, + 'false' => static::phpParserNodeToAstNode($n->elseExpression), + ], + $start_line + ); + if (PHP_VERSION_ID < 70400 && !$is_parenthesized) { + // This is a way to indicate that this AST is definitely unparenthesized in cases where the native parser would not provide this information. + // @phan-suppress-next-line PhanUndeclaredProperty + $result->is_not_parenthesized = true; + } + return $result; + }, + /** + * @throws InvalidNodeException if the variable would be unanalyzable + * TODO: Consider ${''} as a placeholder instead? + */ + 'Microsoft\PhpParser\Node\Expression\Variable' => static function (PhpParser\Node\Expression\Variable $n, int $start_line): \ast\Node { + $name_node = $n->name; + // Note: there are 2 different ways to handle an Error. 1. Add a placeholder. 2. remove all of the statements in that tree. + if ($name_node instanceof PhpParser\Node) { + $name_node = static::phpParserNodeToAstNode($name_node); + } elseif ($name_node instanceof Token) { + if ($name_node instanceof PhpParser\MissingToken) { + if (self::$should_add_placeholders) { + $name_node = '__INCOMPLETE_VARIABLE__'; + } else { + throw new InvalidNodeException(); + } + } else { + if ($name_node->kind === TokenKind::VariableName) { + $name_node = static::variableTokenToString($name_node); + } else { + $name_node = static::tokenToString($name_node); + } + } + } + return new ast\Node(ast\AST_VAR, 0, ['name' => $name_node], $start_line); + }, + /** + * @return ast\Node|int|float|string + */ + 'Microsoft\PhpParser\Node\Expression\BracedExpression' => static function (PhpParser\Node\Expression\BracedExpression $n, int $_) { + return static::phpParserNodeToAstNode($n->expression); + }, + 'Microsoft\PhpParser\Node\Expression\YieldExpression' => static function (PhpParser\Node\Expression\YieldExpression $n, int $start_line): ast\Node { + $kind = $n->yieldOrYieldFromKeyword->kind === TokenKind::YieldFromKeyword ? ast\AST_YIELD_FROM : ast\AST_YIELD; + + $array_element = $n->arrayElement; + $element_value = $array_element->elementValue ?? null; + // Workaround for <= 0.0.5 + // TODO: Remove workaround? + $ast_expr = ($element_value !== null && !($element_value instanceof MissingToken)) ? static::phpParserNodeToAstNode($array_element->elementValue) : null; + if ($kind === \ast\AST_YIELD) { + $element_key = $array_element->elementKey ?? null; + $children = [ + 'value' => $ast_expr, + 'key' => $element_key !== null ? static::phpParserNodeToAstNode($element_key) : null, + ]; + } else { + $children = [ + 'expr' => $ast_expr, + ]; + } + return new ast\Node( + $kind, + 0, + $children, + $start_line + ); + }, + 'Microsoft\PhpParser\Node\ReservedWord' => static function (PhpParser\Node\ReservedWord $n, int $start_line): ast\Node { + return new ast\Node( + ast\AST_NAME, + flags\NAME_NOT_FQ, + ['name' => static::tokenToString($n->children)], + $start_line + ); + }, + 'Microsoft\PhpParser\Node\QualifiedName' => static function (PhpParser\Node\QualifiedName $n, int $start_line): ast\Node { + $name_parts = $n->nameParts; + if (\count($name_parts) === 1) { + $part = $name_parts[0]; + '@phan-var Token $part'; + $imploded_parts = static::tokenToString($part); + if ($part->kind === TokenKind::Name) { + if (\preg_match('@^__(LINE|FILE|DIR|FUNCTION|CLASS|TRAIT|METHOD|NAMESPACE)__$@iD', $imploded_parts) > 0) { + return new ast\Node( + ast\AST_MAGIC_CONST, + self::MAGIC_CONST_LOOKUP[\strtoupper($imploded_parts)], + [], + self::getStartLine($part) + ); + } + } + } else { + $imploded_parts = static::phpParserNameToString($n); + } + if ($n->globalSpecifier !== null) { + $ast_kind = flags\NAME_FQ; + } elseif (($n->relativeSpecifier->namespaceKeyword ?? null) !== null) { + $ast_kind = flags\NAME_RELATIVE; + } else { + $ast_kind = flags\NAME_NOT_FQ; + } + return new ast\Node(ast\AST_NAME, $ast_kind, ['name' => $imploded_parts], $start_line); + }, + 'Microsoft\PhpParser\Node\Parameter' => static function (PhpParser\Node\Parameter $n, int $start_line): ast\Node { + $type_line = static::getEndLine($n->typeDeclaration) ?: $start_line; + $default = $n->default; + $default_node = $default !== null ? static::phpParserNodeToAstNode($default) : null; + return static::astNodeParam( + $n->questionToken !== null, + $n->byRefToken !== null, + $n->dotDotDotToken !== null, + static::phpParserUnionTypeToAstNode($n->typeDeclaration, $n->otherTypeDeclarations, $type_line), + static::variableTokenToString($n->variableName), + $default_node, + $start_line + ); + }, + /** @return int|float */ + 'Microsoft\PhpParser\Node\NumericLiteral' => static function (PhpParser\Node\NumericLiteral $n, int $_) { + // Support php 7.4 numeric literal separators. Ignore `_`. + $n = $n->children; + $text = \str_replace('_', '', static::tokenToString($n)); + if (($n->kind ?? null) === TokenKind::IntegerLiteralToken) { + $as_int = \filter_var($text, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_OCTAL | FILTER_FLAG_ALLOW_HEX); + if ($as_int !== false) { + return $as_int; + } + if (\preg_match('/^0[0-7]+$/D', $text)) { + // this is octal - FILTER_VALIDATE_FLOAT would treat it like decimal + return \intval($text, 8); + } + } + if ($text[0] === '0' && !\preg_match('/[.eE]/', $text)) { + $c = $text[1]; + if ($c === 'b' || $c === 'B') { + return \bindec($text); + } + if ($c === 'x' || $c === 'X') { + return \hexdec($text); + } + return \octdec(substr($text, 0, \strcspn($text, '89'))); + } + return (float)$text; + }, + /** + * @return ast\Node|string + * @throws Exception if the tokens of the string literal are invalid, etc. + */ + 'Microsoft\PhpParser\Node\StringLiteral' => static function (PhpParser\Node\StringLiteral $n, int $start_line) { + $children = $n->children; + if ($children instanceof Token) { + $inner_node = static::parseQuotedString($n); + } elseif (\count($children) === 0) { + $inner_node = ''; + } elseif (\count($children) === 1 && $children[0] instanceof Token) { + $inner_node = static::parseQuotedString($n); + } else { + $inner_node = self::parseMultiPartString($n, $children); + } + if ($n->startQuote !== null && $n->startQuote->kind === TokenKind::BacktickToken) { + return new ast\Node(ast\AST_SHELL_EXEC, 0, ['expr' => $inner_node], isset($children[0]) ? self::getStartLine($children[0]) : $start_line); + // TODO: verify match + } + return $inner_node; + }, + /** @return list - Can return a node or a scalar, depending on the settings */ + 'Microsoft\PhpParser\Node\Statement\CompoundStatementNode' => static function (PhpParser\Node\Statement\CompoundStatementNode $n, int $_) { + $children = []; + foreach ($n->statements as $parser_node) { + $child_node = static::phpParserNodeToAstNode($parser_node); + if (\is_array($child_node)) { + // EchoExpression returns multiple children. + foreach ($child_node as $child_node_part) { + $children[] = $child_node_part; + } + } elseif (!\is_null($child_node)) { + $children[] = $child_node; + } + } + return $children; + }, + /** + * @return int|string|ast\Node|null + * null if incomplete + * int|string for no-op scalar statements like `;2;` + */ + 'Microsoft\PhpParser\Node\Statement\ExpressionStatement' => static function (PhpParser\Node\Statement\ExpressionStatement $n, int $_) { + $expression = $n->expression; + // tolerant-php-parser uses parseExpression(..., $force=true), which can return an array. + // It is the only thing that uses $force=true at the time of writing. + if (!\is_object($expression)) { + return null; + } + return static::phpParserNodeToAstNode($n->expression); + }, + 'Microsoft\PhpParser\Node\Statement\BreakOrContinueStatement' => static function (PhpParser\Node\Statement\BreakOrContinueStatement $n, int $start_line): ast\Node { + $kind = $n->breakOrContinueKeyword->kind === TokenKind::ContinueKeyword ? ast\AST_CONTINUE : ast\AST_BREAK; + $breakout_level = $n->breakoutLevel; + if ($breakout_level !== null) { + $breakout_level = static::phpParserNodeToAstNode($breakout_level); + if (!\is_int($breakout_level)) { + $breakout_level = null; + } + } + return new ast\Node($kind, 0, ['depth' => $breakout_level], $start_line); + }, + 'Microsoft\PhpParser\Node\CatchClause' => static function (PhpParser\Node\CatchClause $n, int $start_line): ast\Node { + $qualified_name = $n->qualifiedName; + $catch_inner_list = []; + // Handle `catch()` syntax error + if ($qualified_name instanceof PhpParser\Node\QualifiedName) { + $catch_inner_list[] = static::phpParserNonValueNodeToAstNode($qualified_name); + } + foreach ($n->otherQualifiedNameList as $other_qualified_name) { + if ($other_qualified_name instanceof PhpParser\Node\QualifiedName) { + $catch_inner_list[] = static::phpParserNonValueNodeToAstNode($other_qualified_name); + } + } + $catch_list_node = new ast\Node(ast\AST_NAME_LIST, 0, $catch_inner_list, $catch_inner_list[0]->lineno ?? $start_line); + $variableName = $n->variableName; + return static::astStmtCatch( + $catch_list_node, + $variableName !== null ? static::variableTokenToString($variableName) : null, + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false. + static::phpParserStmtlistToAstNode($n->compoundStatement, $start_line, false), + $start_line + ); + }, + 'Microsoft\PhpParser\Node\Statement\InterfaceDeclaration' => static function (PhpParser\Node\Statement\InterfaceDeclaration $n, int $start_line): ast\Node { + $end_line = static::getEndLine($n) ?: $start_line; + return static::astStmtClass( + flags\CLASS_INTERFACE, + static::tokenToString($n->name), + static::interfaceBaseClauseToNode($n->interfaceBaseClause), + null, + static::phpParserStmtlistToAstNode($n->interfaceMembers->interfaceMemberDeclarations ?? [], $start_line, false), + $start_line, + $end_line, + $n->getDocCommentText() + ); + }, + 'Microsoft\PhpParser\Node\Statement\ClassDeclaration' => static function (PhpParser\Node\Statement\ClassDeclaration $n, int $start_line): ast\Node { + $end_line = static::getEndLine($n); + $base_class = $n->classBaseClause->baseClass ?? null; + return static::astStmtClass( + static::phpParserClassModifierToAstClassFlags($n->abstractOrFinalModifier), + static::tokenToString($n->name), + $base_class !== null ? static::phpParserNonValueNodeToAstNode($base_class) : null, + $n->classInterfaceClause, + static::phpParserStmtlistToAstNode($n->classMembers->classMemberDeclarations ?? [], self::getStartLine($n->classMembers), false), + $start_line, + $end_line, + $n->getDocCommentText() + ); + }, + 'Microsoft\PhpParser\Node\Statement\TraitDeclaration' => static function (PhpParser\Node\Statement\TraitDeclaration $n, int $start_line): ast\Node { + $end_line = static::getEndLine($n) ?: $start_line; + return static::astStmtClass( + flags\CLASS_TRAIT, + static::tokenToString($n->name), + null, + null, + static::phpParserStmtlistToAstNode($n->traitMembers->traitMemberDeclarations ?? [], self::getStartLine($n->traitMembers), false), + $start_line, + $end_line, + $n->getDocCommentText() + ); + }, + 'Microsoft\PhpParser\Node\ClassConstDeclaration' => static function (PhpParser\Node\ClassConstDeclaration $n, int $start_line): ast\Node { + return static::phpParserClassConstToAstNode($n, $start_line); + }, + /** @return null - A stub that will be removed by the caller. */ + 'Microsoft\PhpParser\Node\MissingMemberDeclaration' => static function (PhpParser\Node\MissingMemberDeclaration $unused_n, int $unused_start_line) { + // This node type is generated for something that isn't a function/constant/property. e.g. "public example();" + return null; + }, + /** @return null - A stub that will be removed by the caller. */ + 'Microsoft\PhpParser\Node\MissingDeclaration' => static function (PhpParser\Node\MissingDeclaration $unused_n, int $unused_start_line) { + // This node type is generated for something that starts with an attribute but isn't a declaration. + return null; + }, + /** + * @throws InvalidNodeException + */ + 'Microsoft\PhpParser\Node\MethodDeclaration' => static function (PhpParser\Node\MethodDeclaration $n, int $start_line): ast\Node { + $statements = $n->compoundStatementOrSemicolon; + $ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line); + if (($ast_return_type->children['name'] ?? null) === '') { + $ast_return_type = null; + } + $original_method_name = $n->name; + if (!($original_method_name instanceof Token)) { + throw new InvalidNodeException(); + } + if ($original_method_name->kind === TokenKind::Name) { + $method_name = static::tokenToString($original_method_name); + } else { + $method_name = 'placeholder_' . $original_method_name->fullStart; + } + + if ($n->questionToken !== null && $ast_return_type !== null) { + $ast_return_type = new ast\Node(ast\AST_NULLABLE_TYPE, 0, ['type' => $ast_return_type], $start_line); + } + return static::newAstDecl( + ast\AST_METHOD, + static::phpParserVisibilityToAstVisibility($n->modifiers) | ($n->byRefToken !== null ? flags\FUNC_RETURNS_REF : 0), + [ + 'params' => static::phpParserParamsToAstParams($n->parameters, $start_line), + 'stmts' => static::phpParserStmtlistToAstNode($statements, self::getStartLine($statements), true), + 'returnType' => $ast_return_type, + ], + $start_line, + $n->getDocCommentText(), + $method_name, + static::getEndLine($n), + self::nextDeclId() + ); + }, + 'Microsoft\PhpParser\Node\Statement\ConstDeclaration' => static function (PhpParser\Node\Statement\ConstDeclaration $n, int $start_line): ast\Node { + return static::phpParserConstToAstNode($n, $start_line); + }, + 'Microsoft\PhpParser\Node\Statement\DeclareStatement' => static function (PhpParser\Node\Statement\DeclareStatement $n, int $start_line): ast\Node { + $doc_comment = $n->getDocCommentText(); + $directive = $n->declareDirective; + if (!($directive instanceof PhpParser\Node\DeclareDirective)) { + throw new AssertionError("Unexpected type for directive"); + } + return static::astStmtDeclare( + static::phpParserDeclareListToAstDeclares($directive, $start_line, $doc_comment), + $n->statements !== null ? static::phpParserStmtlistToAstNode($n->statements, $start_line, true) : null, + $start_line + ); + }, + 'Microsoft\PhpParser\Node\Statement\DoStatement' => static function (PhpParser\Node\Statement\DoStatement $n, int $start_line): ast\Node { + return new ast\Node( + ast\AST_DO_WHILE, + 0, + [ + 'stmts' => static::phpParserStmtlistToAstNode($n->statement, $start_line, false), + 'cond' => static::phpParserNodeToAstNode($n->expression), + ], + $start_line + ); + }, + /** + * @return ast\Node|ast\Node[] + */ + 'Microsoft\PhpParser\Node\Expression\EchoExpression' => static function (PhpParser\Node\Expression\EchoExpression $n, int $start_line) { + $ast_echos = []; + foreach ($n->expressions->children ?? [] as $expr) { + if ($expr instanceof Token && $expr->kind === TokenKind::CommaToken) { + continue; + } + $ast_echos[] = new ast\Node( + ast\AST_ECHO, + 0, + ['expr' => static::phpParserNodeToAstNode($expr)], + $start_line + ); + } + return \count($ast_echos) === 1 ? $ast_echos[0] : $ast_echos; + }, + /** + * @return ?ast\Node + */ + 'Microsoft\PhpParser\Node\ForeachKey' => static function (PhpParser\Node\ForeachKey $n, int $_): ?\ast\Node { + $result = static::phpParserNodeToAstNode($n->expression); + if (!$result instanceof ast\Node) { + return null; + } + return $result; + }, + 'Microsoft\PhpParser\Node\Statement\ForeachStatement' => static function (PhpParser\Node\Statement\ForeachStatement $n, int $start_line): ast\Node { + $foreach_value = $n->foreachValue; + $value = static::phpParserNodeToAstNode($foreach_value->expression); + if ($foreach_value->ampersand) { + $value = new ast\Node( + ast\AST_REF, + 0, + ['var' => $value], + $value->lineno ?? $start_line + ); + } + $foreach_key = $n->foreachKey; + return new ast\Node( + ast\AST_FOREACH, + 0, + [ + 'expr' => static::phpParserNodeToAstNode($n->forEachCollectionName), + 'value' => $value, + 'key' => $foreach_key !== null ? static::phpParserNodeToAstNode($foreach_key) : null, + 'stmts' => static::phpParserStmtlistToAstNode($n->statements, $start_line, true), + ], + $start_line + ); + //return static::phpParserStmtlistToAstNode($n->statements, $start_line); + }, + 'Microsoft\PhpParser\Node\FinallyClause' => static function (PhpParser\Node\FinallyClause $n, int $start_line): ast\Node { + // @phan-suppress-next-line PhanTypeMismatchReturnNullable return_null_on_empty is false. + return static::phpParserStmtlistToAstNode($n->compoundStatement, $start_line, false); + }, + /** + * @throws InvalidNodeException + */ + 'Microsoft\PhpParser\Node\Statement\FunctionDeclaration' => static function (PhpParser\Node\Statement\FunctionDeclaration $n, int $start_line): ast\Node { + $end_line = static::getEndLine($n) ?: $start_line; + $ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line); + if (($ast_return_type->children['name'] ?? null) === '') { + $ast_return_type = null; + } + if ($n->questionToken !== null && $ast_return_type !== null) { + $ast_return_type = new ast\Node(ast\AST_NULLABLE_TYPE, 0, ['type' => $ast_return_type], $start_line); + } + $name = $n->name; + if (!($name instanceof Token)) { + throw new InvalidNodeException(); + } + + return static::astDeclFunction( + $n->byRefToken !== null, + static::tokenToString($name), + static::phpParserParamsToAstParams($n->parameters, $start_line), + $ast_return_type, + static::phpParserStmtlistToAstNode($n->compoundStatementOrSemicolon, self::getStartLine($n->compoundStatementOrSemicolon), false), + $start_line, + $end_line, + $n->getDocCommentText() + ); + }, + /** @return ast\Node|ast\Node[] */ + 'Microsoft\PhpParser\Node\Statement\GlobalDeclaration' => static function (PhpParser\Node\Statement\GlobalDeclaration $n, int $start_line) { + $global_nodes = []; + foreach ($n->variableNameList->children ?? [] as $var) { + if ($var instanceof Token && $var->kind === TokenKind::CommaToken) { + continue; + } + $global_nodes[] = new ast\Node(ast\AST_GLOBAL, 0, ['var' => static::phpParserNodeToAstNode($var)], static::getEndLine($var) ?: $start_line); + } + return \count($global_nodes) === 1 ? $global_nodes[0] : $global_nodes; + }, + 'Microsoft\PhpParser\Node\Statement\IfStatementNode' => static function (PhpParser\Node\Statement\IfStatementNode $n, int $start_line): ast\Node { + return static::phpParserIfStmtToAstIfStmt($n, $start_line); + }, + /** @return ast\Node|ast\Node[] */ + 'Microsoft\PhpParser\Node\Statement\InlineHtml' => static function (PhpParser\Node\Statement\InlineHtml $n, int $start_line) { + $text = $n->text; + if ($text === null) { + return []; // For the beginning/end of files + } + return new ast\Node( + ast\AST_ECHO, + 0, + ['expr' => static::tokenToRawString($n->text)], + $start_line + ); + }, + /** @suppress PhanTypeMismatchArgument TODO: Make ForStatement have more accurate docs? */ + 'Microsoft\PhpParser\Node\Statement\ForStatement' => static function (PhpParser\Node\Statement\ForStatement $n, int $start_line): ast\Node { + return new ast\Node( + ast\AST_FOR, + 0, + [ + 'init' => $n->forInitializer !== null ? static::phpParserExprListToExprList($n->forInitializer, $start_line) : null, + 'cond' => $n->forControl !== null ? static::phpParserExprListToExprList($n->forControl, $start_line) : null, + 'loop' => $n->forEndOfLoop !== null ? static::phpParserExprListToExprList($n->forEndOfLoop, $start_line) : null, + 'stmts' => static::phpParserStmtlistToAstNode($n->statements, $start_line, true), + ], + $start_line + ); + }, + /** @return ast\Node[] */ + 'Microsoft\PhpParser\Node\Statement\NamespaceUseDeclaration' => static function (PhpParser\Node\Statement\NamespaceUseDeclaration $n, int $start_line): array { + $use_clauses = $n->useClauses; + $results = []; + $parser_use_kind = $n->functionOrConst->kind ?? null; + foreach ($use_clauses->children ?? [] as $use_clause) { + if (!($use_clause instanceof PhpParser\Node\NamespaceUseClause)) { + continue; + } + $results[] = static::astStmtUseOrGroupUseFromUseClause($use_clause, $parser_use_kind, $start_line); + } + return $results; + }, + 'Microsoft\PhpParser\Node\Statement\NamespaceDefinition' => static function (PhpParser\Node\Statement\NamespaceDefinition $n, int $start_line): ast\Node { + $stmt = $n->compoundStatementOrSemicolon; + $name_node = $n->name; + if ($stmt instanceof PhpParser\Node) { + $stmts_start_line = self::getStartLine($stmt); + $ast_stmt = static::phpParserStmtlistToAstNode($n->compoundStatementOrSemicolon, $stmts_start_line, true); + $start_line = $name_node !== null ? self::getStartLine($name_node) : $stmts_start_line; // imitate php-ast + } else { + $ast_stmt = null; + } + return new ast\Node( + ast\AST_NAMESPACE, + 0, + [ + 'name' => $name_node !== null ? static::phpParserNameToString($name_node) : null, + 'stmts' => $ast_stmt, + ], + $start_line + ); + }, + /** @return array{} */ + 'Microsoft\PhpParser\Node\Statement\EmptyStatement' => static function (PhpParser\Node\Statement\EmptyStatement $unused_node, int $unused_start_line): array { + // `;;` + return []; + }, + 'Microsoft\PhpParser\Node\PropertyDeclaration' => static function (PhpParser\Node\PropertyDeclaration $n, int $start_line): ast\Node { + return static::phpParserPropertyToAstNode($n, $start_line); + }, + 'Microsoft\PhpParser\Node\Statement\ReturnStatement' => static function (PhpParser\Node\Statement\ReturnStatement $n, int $start_line): ast\Node { + $e = $n->expression; + $expr_node = $e !== null ? static::phpParserNodeToAstNode($e) : null; + return new ast\Node(ast\AST_RETURN, 0, ['expr' => $expr_node], $start_line); + }, + /** @return ast\Node|ast\Node[] */ + 'Microsoft\PhpParser\Node\Statement\FunctionStaticDeclaration' => static function (PhpParser\Node\Statement\FunctionStaticDeclaration $n, int $start_line) { + $static_nodes = []; + foreach ($n->staticVariableNameList->children ?? [] as $var) { + if ($var instanceof Token) { + continue; + } + if (!($var instanceof PhpParser\Node\StaticVariableDeclaration)) { + // FIXME error tolerance + throw new AssertionError("Expected StaticVariableDeclaration"); + } + + $assignment = $var->assignment; + $static_nodes[] = new ast\Node(ast\AST_STATIC, 0, [ + 'var' => new ast\Node(ast\AST_VAR, 0, ['name' => static::phpParserNodeToAstNode($var->variableName)], static::getEndLine($var) ?: $start_line), + 'default' => $assignment !== null ? static::phpParserNodeToAstNode($assignment) : null, + ], static::getEndLine($var) ?: $start_line); + } + return \count($static_nodes) === 1 ? $static_nodes[0] : $static_nodes; + }, + 'Microsoft\PhpParser\Node\Statement\SwitchStatementNode' => static function (PhpParser\Node\Statement\SwitchStatementNode $n, int $_): ast\Node { + return static::phpParserSwitchListToAstSwitch($n); + }, + 'Microsoft\PhpParser\Node\Statement\ThrowStatement' => static function (PhpParser\Node\Statement\ThrowStatement $n, int $start_line): ast\Node { + return static::phpParserThrowToASTThrow($n, $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\ThrowExpression' => static function (PhpParser\Node\Expression\ThrowExpression $n, int $start_line): ast\Node { + return static::phpParserThrowToASTThrow($n, $start_line); + }, + 'Microsoft\PhpParser\Node\Expression\MatchExpression' => static function (PhpParser\Node\Expression\MatchExpression $n, int $start_line): ast\Node { + return self::phpParserMatchToAstMatch($n, $start_line); + }, + + 'Microsoft\PhpParser\Node\TraitUseClause' => static function (PhpParser\Node\TraitUseClause $n, int $start_line): ast\Node { + $clauses_list_node = $n->traitSelectAndAliasClauses; + if ($clauses_list_node instanceof PhpParser\Node\DelimitedList\TraitSelectOrAliasClauseList) { + $adaptations_inner = []; + foreach ($clauses_list_node->children as $select_or_alias_clause) { + if ($select_or_alias_clause instanceof Token) { + continue; + } + if (!($select_or_alias_clause instanceof PhpParser\Node\TraitSelectOrAliasClause)) { + throw new AssertionError("Expected TraitSelectOrAliasClause"); + } + $result = static::phpParserNodeToAstNode($select_or_alias_clause); + if ($result instanceof ast\Node) { + $adaptations_inner[] = $result; + } + } + $adaptations = new ast\Node(ast\AST_TRAIT_ADAPTATIONS, 0, $adaptations_inner, $adaptations_inner[0]->lineno ?? $start_line); + } else { + $adaptations = null; + } + return new ast\Node( + ast\AST_USE_TRAIT, + 0, + [ + 'traits' => static::phpParserNameListToAstNameList($n->traitNameList->children ?? [], $start_line), + 'adaptations' => $adaptations, + ], + $start_line + ); + }, + + /** + * @return ?ast\Node + */ + 'Microsoft\PhpParser\Node\TraitSelectOrAliasClause' => static function (PhpParser\Node\TraitSelectOrAliasClause $n, int $start_line): ?\ast\Node { + // FIXME targetName phpdoc is wrong. + $name = $n->name; + if ($n->asOrInsteadOfKeyword->kind === TokenKind::InsteadOfKeyword) { + if (!$name instanceof ScopedPropertyAccessExpression) { + return null; + } + $member_name_list = $name->memberName; + if ($member_name_list === null) { + return null; + } + + $target_name_list = array_merge([$n->targetName], $n->remainingTargetNames ?? []); + if (\is_object($member_name_list)) { + $member_name_list = [$member_name_list]; + } + // Trait::y insteadof OtherTrait + $trait_node = static::phpParserNonValueNodeToAstNode($name->scopeResolutionQualifier); + $method_node = static::phpParserNameListToAstNameList($member_name_list, $start_line); + $target_node = static::phpParserNameListToAstNameList($target_name_list, $start_line); + $outer_method_node = new ast\Node(ast\AST_METHOD_REFERENCE, 0, [ + 'class' => $trait_node, + 'method' => $method_node->children[0] + ], $start_line); + + if (\count($member_name_list) !== 1) { + throw new AssertionError("Expected insteadof member_name_list length to be 1"); + } + $children = [ + 'method' => $outer_method_node, + 'insteadof' => $target_node, + ]; + return new ast\Node(ast\AST_TRAIT_PRECEDENCE, 0, $children, $start_line); + } else { + if ($name instanceof PhpParser\Node\Expression\ScopedPropertyAccessExpression) { + $class_node = static::phpParserNonValueNodeToAstNode($name->scopeResolutionQualifier); + $method_node = static::phpParserNodeToAstNode($name->memberName); + } else { + $class_node = null; + $method_node = static::phpParserNameToString($name); + } + $flags = static::phpParserVisibilityToAstVisibility($n->modifiers, false); + $target_name = $n->targetName; + $target_name = $target_name instanceof PhpParser\Node\QualifiedName ? static::phpParserNameToString($target_name) : null; + $children = [ + 'method' => new ast\Node(ast\AST_METHOD_REFERENCE, 0, [ + 'class' => $class_node, + 'method' => $method_node, + ], $start_line), + 'alias' => $target_name, + ]; + + return new ast\Node(ast\AST_TRAIT_ALIAS, $flags, $children, $start_line); + } + }, + 'Microsoft\PhpParser\Node\Statement\TryStatement' => static function (PhpParser\Node\Statement\TryStatement $n, int $start_line): ast\Node { + $finally_clause = $n->finallyClause; + return static::astNodeTry( + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false. + static::phpParserStmtlistToAstNode($n->compoundStatement, $start_line, false), // $n->try + static::phpParserCatchlistToAstCatchlist($n->catchClauses ?? [], $start_line), + $finally_clause !== null ? static::phpParserStmtlistToAstNode($finally_clause->compoundStatement, self::getStartLine($finally_clause), false) : null, + $start_line + ); + }, + /** @return ast\Node|ast\Node[] */ + 'Microsoft\PhpParser\Node\Expression\UnsetIntrinsicExpression' => static function (PhpParser\Node\Expression\UnsetIntrinsicExpression $n, int $start_line) { + $stmts = []; + foreach ($n->expressions->children ?? [] as $var) { + if ($var instanceof Token) { + // Skip over ',' and invalid tokens + continue; + } + $stmts[] = new ast\Node(ast\AST_UNSET, 0, ['var' => static::phpParserNodeToAstNode($var)], static::getEndLine($var) ?: $start_line); + } + return \count($stmts) === 1 ? $stmts[0] : $stmts; + }, + 'Microsoft\PhpParser\Node\Statement\WhileStatement' => static function (PhpParser\Node\Statement\WhileStatement $n, int $start_line): ast\Node { + return static::astNodeWhile( + static::phpParserNodeToAstNode($n->expression), + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false. + static::phpParserStmtlistToAstNode($n->statements, $start_line, false), + $start_line + ); + }, + 'Microsoft\PhpParser\Node\Statement\GotoStatement' => static function (PhpParser\Node\Statement\GotoStatement $n, int $start_line): ast\Node { + return new ast\Node(ast\AST_GOTO, 0, ['label' => static::tokenToString($n->name)], $start_line); + }, + /** @return ast\Node[]|ast\Node */ + 'Microsoft\PhpParser\Node\Statement\NamedLabelStatement' => static function (PhpParser\Node\Statement\NamedLabelStatement $n, int $start_line) { + $label = new ast\Node(ast\AST_LABEL, 0, ['name' => static::tokenToString($n->name)], $start_line); + $raw_statement = $n->statement; + if (!$raw_statement) { + // Hopefully, newer versions of tolerant-php-parser will treat named labels as a standlone statement + return $label; + } + $statement = static::phpParserNodeToAstNode($raw_statement); + if (is_array($statement)) { + // E.g. there are multiple labels in a row. + \array_unshift($statement, $label); + return $statement; + } + return [$label, $statement]; + }, + ]; + + foreach ($closures as $key => $_) { + if (!(\class_exists($key))) { + throw new AssertionError("Class $key should exist"); + } + } + return $closures; + } + + /** + * Overridden in TolerantASTConverterWithNodeMapping + * + * @param PhpParser\Node\NamespaceUseClause $use_clause + * @param ?int $parser_use_kind + * @param int $start_line + * @throws InvalidNodeException + */ + protected static function astStmtUseOrGroupUseFromUseClause(PhpParser\Node\NamespaceUseClause $use_clause, ?int $parser_use_kind, int $start_line): ast\Node + { + $namespace_name_node = $use_clause->namespaceName; + if ($namespace_name_node instanceof PhpParser\Node\QualifiedName) { + $namespace_name = \rtrim(static::phpParserNameToString($namespace_name_node), '\\'); + } else { + throw new InvalidNodeException(); + } + if ($use_clause->groupClauses !== null) { + return static::astStmtGroupUse( + $parser_use_kind, // E.g. kind is FunctionKeyword or ConstKeyword or null + $namespace_name, + static::phpParserNamespaceUseListToAstUseList($use_clause->groupClauses->children ?? []), + $start_line + ); + } else { + $alias_token = $use_clause->namespaceAliasingClause->name ?? null; + $alias = $alias_token !== null ? static::tokenToString($alias_token) : null; + return static::astStmtUse($parser_use_kind, $namespace_name, $alias, $start_line); + } + } + + private static function astNodeTry( + \ast\Node $try_node, + ?\ast\Node $catches_node, + ?\ast\Node $finally_node, + int $start_line + ): ast\Node { + // Return fields of $node->children in the same order as php-ast + $children = [ + 'try' => $try_node, + ]; + if ($catches_node !== null) { + $children['catches'] = $catches_node; + } + $children['finally'] = $finally_node; + return new ast\Node(ast\AST_TRY, 0, $children, $start_line); + } + + private static function astStmtCatch(ast\Node $types, ?string $var, \ast\Node $stmts, int $lineno): ast\Node + { + return new ast\Node( + ast\AST_CATCH, + 0, + [ + 'class' => $types, + // php 8.0 allows catch statements without variables + 'var' => is_string($var) ? new ast\Node(ast\AST_VAR, 0, ['name' => $var], $lineno) : null, + 'stmts' => $stmts, + ], + $lineno + ); + } + + /** + * @param PhpParser\Node\CatchClause[] $catches + */ + private static function phpParserCatchlistToAstCatchlist(array $catches, int $lineno): ast\Node + { + $children = []; + foreach ($catches as $parser_catch) { + $children[] = static::phpParserNonValueNodeToAstNode($parser_catch); + } + return new ast\Node(ast\AST_CATCH_LIST, 0, $children, $children[0]->lineno ?? $lineno); + } + + /** + * @param list $types + */ + private static function phpParserNameListToAstNameList(array $types, int $line): ast\Node + { + $ast_types = []; + foreach ($types as $type) { + if ($type instanceof Token && $type->kind === TokenKind::CommaToken) { + continue; + } + $ast_types[] = static::phpParserNonValueNodeToAstNode($type); + } + return new ast\Node(ast\AST_NAME_LIST, 0, $ast_types, $line); + } + + /** + * @param ast\Node|string|int|float $cond + */ + private static function astNodeWhile($cond, ast\Node $stmts, int $start_line): ast\Node + { + return new ast\Node( + ast\AST_WHILE, + 0, + [ + 'cond' => $cond, + 'stmts' => $stmts, + ], + $start_line + ); + } + + /** + * @param ast\Node|string|int|float $var + * @param ast\Node|string|int|float $expr + */ + private static function astNodeAssign($var, $expr, int $line, bool $ref): ast\Node + { + return new ast\Node( + $ref ? ast\AST_ASSIGN_REF : ast\AST_ASSIGN, + 0, + [ + 'var' => $var, + 'expr' => $expr, + ], + $line + ); + } + + /** + * @throws Error if the kind could not be found + */ + private static function phpParserIncludeTokenToAstIncludeFlags(Token $type): int + { + switch ($type->kind) { + case TokenKind::IncludeKeyword: + return flags\EXEC_INCLUDE; + case TokenKind::IncludeOnceKeyword: + return flags\EXEC_INCLUDE_ONCE; + case TokenKind::RequireKeyword: + return flags\EXEC_REQUIRE; + case TokenKind::RequireOnceKeyword: + return flags\EXEC_REQUIRE_ONCE; + default: + throw new \Error("Unrecognized PhpParser include/require type"); + } + } + + /** + * @param PhpParser\Node\QualifiedName|Token|null $type + */ + protected static function phpParserUnionTypeToAstNode($type, ?PhpParser\Node\DelimitedList\QualifiedNameList $other_types, int $line): ?\ast\Node + { + $types = []; + if (!\is_null($type) && !($type instanceof Token && $type->kind === TokenKind::BarToken)) { + $result = static::phpParserTypeToAstNode($type, $line); + if ($result) { + $types[] = $result; + } + } + if ($other_types instanceof PhpParser\Node\DelimitedList\QualifiedNameList) { + foreach ($other_types->children as $child) { + if ($child instanceof Token && $child->kind === TokenKind::BarToken) { + continue; + } + $result = static::phpParserTypeToAstNode($child, static::getEndLine($child) ?: $line); + if ($result) { + $types[] = $result; + } + } + } + $n = \count($types); + if ($n === 0) { + return null; + } elseif ($n === 1) { + return $types[0]; + } + return new ast\Node(ast\AST_TYPE_UNION, 0, $types, $types[0]->lineno); + } + + /** + * @param PhpParser\Node\QualifiedName|Token|null $type + */ + protected static function phpParserTypeToAstNode($type, int $line): ?ast\Node + { + if (\is_null($type)) { + return null; + } + $original_type = $type; + if ($type instanceof PhpParser\Node\QualifiedName) { + $type = static::phpParserNameToString($type); + } elseif ($type instanceof Token) { + $type = static::tokenToString($type); + } + if (\is_string($type)) { + switch (\strtolower($type)) { + case 'null': + $flags = flags\TYPE_NULL; + break; + case 'bool': + $flags = flags\TYPE_BOOL; + break; + case 'int': + $flags = flags\TYPE_LONG; + break; + case 'float': + $flags = flags\TYPE_DOUBLE; + break; + case 'string': + $flags = flags\TYPE_STRING; + break; + case 'array': + $flags = flags\TYPE_ARRAY; + break; + case 'object': + $flags = flags\TYPE_OBJECT; + break; + case 'callable': + $flags = flags\TYPE_CALLABLE; + break; + case 'void': + $flags = flags\TYPE_VOID; + break; + case 'iterable': + $flags = flags\TYPE_ITERABLE; + break; + case 'false': + $flags = flags\TYPE_FALSE; + break; + case 'static': + $flags = flags\TYPE_STATIC; + break; + default: + // TODO: Refactor this into a function accepting a QualifiedName + if ($original_type instanceof PhpParser\Node\QualifiedName) { + if ($original_type->globalSpecifier !== null) { + $ast_kind = flags\NAME_FQ; + } elseif (($original_type->relativeSpecifier->namespaceKeyword ?? null) !== null) { + $ast_kind = flags\NAME_RELATIVE; + } else { + $ast_kind = flags\NAME_NOT_FQ; + } + } else { + $ast_kind = flags\NAME_NOT_FQ; + } + return new ast\Node( + ast\AST_NAME, + $ast_kind, + ['name' => $type], + $line + ); + } + return new ast\Node(ast\AST_TYPE, $flags, [], $line); + } + return static::phpParserNodeToAstNode($type); + } + + /** + * @param bool $by_ref + * @param ?ast\Node $type + * @param string $name + * @param ?ast\Node|?int|?string|?float $default + */ + private static function astNodeParam(bool $is_nullable, bool $by_ref, bool $variadic, ?\ast\Node $type, string $name, $default, int $line): ast\Node + { + if ($is_nullable) { + $type = new ast\Node( + ast\AST_NULLABLE_TYPE, + 0, + ['type' => $type], + $line + ); + } + return new ast\Node( + ast\AST_PARAM, + ($by_ref ? flags\PARAM_REF : 0) | ($variadic ? flags\PARAM_VARIADIC : 0), + [ + 'type' => $type, + 'name' => $name, + 'default' => $default, + ], + $line + ); + } + + private static function phpParserParamsToAstParams(?\Microsoft\PhpParser\node\delimitedlist\parameterdeclarationlist $parser_params, int $line): ast\Node + { + $new_params = []; + foreach ($parser_params->children ?? [] as $parser_node) { + if ($parser_node instanceof Token) { + continue; + } + $new_params[] = static::phpParserNodeToAstNode($parser_node); + } + $result = new ast\Node( + ast\AST_PARAM_LIST, + 0, + $new_params, + $new_params[0]->lineno ?? $line + ); + if (($parser_node->kind ?? null) === TokenKind::CommaToken) { + // @phan-suppress-next-line PhanUndeclaredProperty + $result->polyfill_has_trailing_comma = true; + } + return $result; + } + + /** + * @param PhpParser\Node|PhpParser\Token $parser_node + * @suppress UnusedSuppression, TypeMismatchProperty + */ + protected static function astStub($parser_node): ast\Node + { + // Debugging code. + if (\getenv(self::ENV_AST_THROW_INVALID)) { + // @phan-suppress-next-line PhanThrowTypeAbsent only throws for debugging + throw new \Error("TODO:" . get_class($parser_node)); + } + + $node = new ast\Node(); + $node->kind = "TODO:" . get_class($parser_node); + $node->flags = 0; + $node->lineno = self::getStartLine($parser_node); + $node->children = []; + return $node; + } + + private static function phpParserClosureUsesToAstClosureUses( + ?\Microsoft\PhpParser\Node\DelimitedList\UseVariableNameList $uses, + int $line + ): ?\ast\Node { + $children = $uses->children ?? []; + if (count($children) === 0) { + return null; + } + $ast_uses = []; + foreach ($children as $use) { + if ($use instanceof Token) { + continue; + } + if (!($use instanceof PhpParser\Node\UseVariableName)) { + throw new AssertionError("Expected UseVariableName"); + } + $ast_uses[] = new ast\Node(ast\AST_CLOSURE_VAR, $use->byRef ? ast\flags\CLOSURE_USE_REF : 0, ['name' => static::tokenToString($use->variableName)], self::getStartLine($use)); + } + $result = new ast\Node(ast\AST_CLOSURE_USES, 0, $ast_uses, $ast_uses[0]->lineno ?? $line); + if (($use->kind ?? null) === TokenKind::CommaToken) { + // @phan-suppress-next-line PhanUndeclaredProperty + $result->polyfill_has_trailing_comma = true; + } + return $result; + } + + private static function resolveDocCommentForClosure(PhpParser\Node\Expression $node): ?string + { + $doc_comment = $node->getDocCommentText(); + if (\Phan\Library\StringUtil::isNonZeroLengthString($doc_comment)) { + return $doc_comment; + } + for ($prev_node = $node; $node = $node->parent; $prev_node = $node) { + if ($node instanceof PhpParser\Node\Expression\AssignmentExpression || + $node instanceof PhpParser\Node\Expression\ParenthesizedExpression || + $node instanceof PhpParser\Node\ArrayElement || + $node instanceof PhpParser\Node\Statement\ReturnStatement) { + $doc_comment = $node->getDocCommentText(); + if (\Phan\Library\StringUtil::isNonZeroLengthString($doc_comment)) { + return $doc_comment; + } + continue; + } + if ($node instanceof PhpParser\Node\Expression\ArgumentExpression) { + // Skip ArgumentExpression and the PhpParser\Node\DelimitedList\ArgumentExpressionList + // to get to the CallExpression + // @phan-suppress-next-line PhanPossiblyUndeclaredProperty + $node = $node->parent->parent; + // fall through + } + if ($node instanceof PhpParser\Node\Expression\MemberAccessExpression) { + // E.g. ((Closure)->bindTo()) + if ($prev_node !== $node->dereferencableExpression) { + return null; + } + $doc_comment = $node->getDocCommentText(); + if (is_string($doc_comment)) { + return $doc_comment; + } + continue; + } + if ($node instanceof PhpParser\Node\Expression\CallExpression) { + if ($prev_node === $node->callableExpression) { + $doc_comment = $node->getDocCommentText(); + if (is_string($doc_comment)) { + return $doc_comment; + } + continue; + } + if ($node->callableExpression instanceof PhpParser\Node\Expression\AnonymousFunctionCreationExpression) { + return null; + } + $found = false; + foreach ($node->argumentExpressionList->children ?? [] as $argument_expression) { + if (!($argument_expression instanceof PhpParser\Node\Expression\ArgumentExpression)) { + continue; + } + $expression = $argument_expression->expression; + if ($expression === $prev_node) { + $found = true; + $doc_comment = $node->getDocCommentText(); + if (is_string($doc_comment)) { + return $doc_comment; + } + break; + } + if (!($expression instanceof PhpParser\Node)) { + continue; + } + if ($expression instanceof PhpParser\Node\ConstElement || $expression instanceof PhpParser\Node\NumericLiteral || $expression instanceof PhpParser\Node\StringLiteral) { + continue; + } + return null; + } + + if ($found) { + continue; + } + } + break; + } + return null; + } + + private static function astDeclClosure( + bool $by_ref, + bool $static, + ast\Node $params, + ?\ast\Node $uses, + ast\Node $stmts, + ?\ast\Node $return_type, + int $start_line, + int $end_line, + ?string $doc_comment + ): ast\Node { + return static::newAstDecl( + ast\AST_CLOSURE, + ($by_ref ? flags\FUNC_RETURNS_REF : 0) | ($static ? flags\MODIFIER_STATIC : 0), + [ + 'params' => $params, + 'uses' => $uses, + 'stmts' => $stmts, + 'returnType' => $return_type, + ], + $start_line, + $doc_comment, + '{closure}', + $end_line, + self::nextDeclId() + ); + } + + /** + * @param ?ast\Node $return_type + * @param ?ast\Node $stmts (TODO: create empty statement list instead of null) + * @param ?string $doc_comment + */ + private static function astDeclFunction( + bool $by_ref, + string $name, + ast\Node $params, + ?\ast\Node $return_type, + ?\ast\Node $stmts, + int $line, + int $end_line, + ?string $doc_comment + ): ast\Node { + return static::newAstDecl( + ast\AST_FUNC_DECL, + $by_ref ? flags\FUNC_RETURNS_REF : 0, + [ + 'params' => $params, + 'stmts' => $stmts, + 'returnType' => $return_type, + ], + $line, + $doc_comment, + $name, + $end_line, + self::nextDeclId() + ); + } + + /** + * @param ?Token $flags + * @throws InvalidArgumentException if the class flags were unexpected + */ + private static function phpParserClassModifierToAstClassFlags(?Token $flags): int + { + if ($flags === null) { + return 0; + } + switch ($flags->kind) { + case TokenKind::AbstractKeyword: + return flags\CLASS_ABSTRACT; + case TokenKind::FinalKeyword: + return flags\CLASS_FINAL; + default: + throw new InvalidArgumentException("Unexpected kind '" . Token::getTokenKindNameFromValue($flags->kind) . "'"); + } + } + + private static function interfaceBaseClauseToNode(?\Microsoft\PhpParser\Node\InterfaceBaseClause $node): ?\ast\Node + { + if (!$node instanceof PhpParser\Node\InterfaceBaseClause) { + // TODO: real placeholder? + return null; + } + + $interface_extends_name_list = []; + foreach ($node->interfaceNameList->children ?? [] as $implement) { + if ($implement instanceof Token && $implement->kind === TokenKind::CommaToken) { + continue; + } + $interface_name_node = static::phpParserNonValueNodeToAstNode($implement); + if (!$interface_name_node instanceof ast\Node) { + throw new AssertionError("Expected valid node for interfaces inherited by class"); + } + $interface_extends_name_list[] = $interface_name_node; + } + if (\count($interface_extends_name_list) === 0) { + return null; + } + return new ast\Node(ast\AST_NAME_LIST, 0, $interface_extends_name_list, $interface_extends_name_list[0]->lineno); + } + + private static function astStmtClass( + int $flags, + ?string $name, + ?\ast\Node $extends, + ?\Microsoft\PhpParser\node\classinterfaceclause $implements, + ?\ast\Node $stmts, + int $line, + int $end_line, + ?string $doc_comment + ): ast\Node { + + // NOTE: `null` would be an anonymous class. + // the empty string is a missing string we pretend is an anonymous class + // so that Phan won't throw an UnanalyzableException during the analysis phase + if ($name === null || $name === '') { + $flags |= flags\CLASS_ANONYMOUS; + } + + if (($flags & flags\CLASS_INTERFACE) > 0) { + $children = [ + 'extends' => null, + 'implements' => $extends, + 'stmts' => $stmts, + ]; + } else { + if ($implements !== null) { + $ast_implements_inner = []; + foreach ($implements->interfaceNameList->children ?? [] as $implement) { + // TODO: simplify? + if ($implement instanceof Token && $implement->kind === TokenKind::CommaToken) { + continue; + } + $implement_node = static::phpParserNonValueNodeToAstNode($implement); + if (!$implement_node instanceof ast\Node) { + continue; + } + $ast_implements_inner[] = $implement_node; + } + if (\count($ast_implements_inner) > 0) { + $ast_implements = new ast\Node(ast\AST_NAME_LIST, 0, $ast_implements_inner, $ast_implements_inner[0]->lineno); + } else { + $ast_implements = null; + } + } else { + $ast_implements = null; + } + $children = [ + 'extends' => $extends, + 'implements' => $ast_implements, + 'stmts' => $stmts, + ]; + } + + return static::newAstDecl( + ast\AST_CLASS, + $flags, + $children, + $line, + $doc_comment, + $name, + $end_line, + self::nextDeclId() + ); + } + + private static function phpParserArgListToAstArgList(?\Microsoft\PhpParser\node\delimitedlist\argumentexpressionlist $args, int $line): ast\Node + { + $ast_args = []; + foreach ($args->children ?? [] as $arg) { + if ($arg instanceof Token && $arg->kind === TokenKind::CommaToken) { + continue; + } + $ast_args[] = static::phpParserNodeToAstNode($arg); + } + $result = new ast\Node(ast\AST_ARG_LIST, 0, $ast_args, $args ? self::getStartLine($args) : $line); + if (($arg->kind ?? null) === TokenKind::CommaToken) { + // NOTE: This is deliberately using a dynamic property instead of a flag because other applications may use flags + // @phan-suppress-next-line PhanUndeclaredProperty + $result->polyfill_has_trailing_comma = true; + } + return $result; + } + + /** + * @param PhpParser\Node\Expression\ThrowExpression|PhpParser\Node\Statement\ThrowStatement $n + */ + private static function phpParserThrowToASTThrow(object $n, int $start_line): ast\Node + { + $expression = $n->expression; + if (!$expression) { + throw new InvalidNodeException(); + } + return new ast\Node( + ast\AST_THROW, + 0, + ['expr' => static::phpParserNodeToAstNode($expression)], + $start_line + ); + } + + protected static function phpParserMatchToAstMatch(PhpParser\Node\Expression\MatchExpression $n, int $start_line): ast\Node + { + $expression = $n->expression; + if (!$expression) { + throw new InvalidNodeException(); + } + return new ast\Node( + ast\AST_MATCH, + 0, + [ + 'cond' => static::phpParserNodeToAstNode($expression), + 'stmts' => static::phpParserMatchArmListToAstMatchArmList($n->arms, $start_line), + ], + $start_line + ); + } + + protected static function phpParserMatchArmListToAstMatchArmList(?\Microsoft\PhpParser\Node\DelimitedList\MatchExpressionArmList $arms, int $start_line): ast\Node + { + $ast_arms = []; + foreach ($arms->children ?? [] as $arm) { + if (!$arm instanceof PhpParser\Node\MatchArm) { + continue; + } + // @phan-suppress-next-line PhanTypeMismatchArgument + try { + $ast_arms[] = static::phpParserMatchArmToAstMatchArm($arm); + } catch (InvalidNodeException $_) { + continue; + } + } + return new ast\Node(ast\AST_MATCH_ARM_LIST, 0, $ast_arms, $arms ? self::getStartLine($arms) : $start_line); + } + + private static function phpParserMatchConditionListToAstNode(?PhpParser\Node\DelimitedList\MatchArmConditionList $condition_list): ?ast\Node + { + if (!$condition_list) { + throw new InvalidNodeException(); + } + $conditions = []; + foreach ($condition_list->children ?? [] as $phpparser_condition) { + if ($phpparser_condition instanceof Token) { + switch ($phpparser_condition->kind) { + case TokenKind::DefaultKeyword: + return null; + case TokenKind::CommaToken: + continue 2; + } + } + $conditions[] = static::phpParserNodeToAstNode($phpparser_condition); + } + if (!$conditions) { + throw new InvalidNodeException(); + } + return new ast\Node(ast\AST_EXPR_LIST, 0, $conditions, self::getStartLine($condition_list)); + } + + private static function phpParserMatchArmToAstMatchArm(PhpParser\Node\MatchArm $arm): ast\Node + { + return new ast\Node( + ast\AST_MATCH_ARM, + 0, + [ + 'cond' => static::phpParserMatchConditionListToAstNode($arm->conditionList), + 'expr' => static::phpParserNodeToAstNode($arm->body), + ], + self::getStartLine($arm) + ); + } + + /** + * @param ?int $kind + * @throws InvalidArgumentException if the token kind was somehow invalid + */ + private static function phpParserNamespaceUseKindToASTUseFlags(?int $kind): int + { + switch ($kind ?? 0) { + case TokenKind::FunctionKeyword: + return flags\USE_FUNCTION; + case TokenKind::ConstKeyword: + return flags\USE_CONST; + case 0: + return flags\USE_NORMAL; + default: + throw new \InvalidArgumentException("Unexpected kind '" . Token::getTokenKindNameFromValue($kind ?? 0) . "'"); + } + } + + /** + * @param Token[]|PhpParser\Node\NamespaceUseGroupClause[]|PhpParser\Node[] $uses + * @return ast\Node[] + */ + private static function phpParserNamespaceUseListToAstUseList(array $uses): array + { + $ast_uses = []; + foreach ($uses as $use_clause) { + if (!($use_clause instanceof PhpParser\Node\NamespaceUseGroupClause)) { + continue; + } + $raw_namespace_name = $use_clause->namespaceName; + if (!$raw_namespace_name instanceof PhpParser\Node\QualifiedName) { + // Invalid AST, ignore. We should have already warned about the syntax + continue; + } + // ast doesn't fill in an alias if it's identical to the real name, + // but phpParser does? + $namespace_name = \rtrim(static::phpParserNameToString($raw_namespace_name), '\\'); + $alias_token = $use_clause->namespaceAliasingClause->name ?? null; + $alias = $alias_token !== null ? static::tokenToString($alias_token) : null; + + $ast_uses[] = new ast\Node( + ast\AST_USE_ELEM, + static::phpParserNamespaceUseKindToASTUseFlags($use_clause->functionOrConst->kind ?? 0), + [ + 'name' => $namespace_name, + 'alias' => $alias !== $namespace_name ? $alias : null, + ], + self::getStartLine($use_clause) + ); + } + return $ast_uses; + } + + private static function astStmtUse(?int $type, string $name, ?string $alias, int $line): ast\Node + { + $use_inner = new ast\Node(ast\AST_USE_ELEM, 0, ['name' => $name, 'alias' => $alias], $line); + return new ast\Node( + ast\AST_USE, + static::phpParserNamespaceUseKindToASTUseFlags($type), + [$use_inner], + $line + ); + } + + /** + * @param ?int $type + * @param ?string $prefix + * @param list $uses + * @suppress PhanPossiblyUndeclaredProperty $use should always be a node + */ + private static function astStmtGroupUse(?int $type, ?string $prefix, array $uses, int $line): ast\Node + { + $flags = static::phpParserNamespaceUseKindToASTUseFlags($type); + $uses = new ast\Node(ast\AST_USE, 0, $uses, $line); + if ($flags === flags\USE_NORMAL) { + foreach ($uses->children as $use) { + if ($use->flags !== 0) { + $flags = 0; + break; + } + } + } else { + foreach ($uses->children as $use) { + if ($use->flags === flags\USE_NORMAL) { + $use->flags = 0; + } + } + } + + return new ast\Node( + ast\AST_GROUP_USE, + $flags, + [ + 'prefix' => $prefix, + 'uses' => $uses, + ], + $line + ); + } + + /** + * @param ast\Node|string|int|float|null $cond (null for else statements) + * @param ast\Node $stmts + * @param int $line + */ + private static function astIfElem($cond, \ast\Node $stmts, int $line): ast\Node + { + return new ast\Node(ast\AST_IF_ELEM, 0, ['cond' => $cond, 'stmts' => $stmts], $line); + } + + private static function phpParserSwitchListToAstSwitch(PhpParser\Node\Statement\SwitchStatementNode $node): ast\Node + { + $stmts = []; + $node_line = static::getStartLine($node); + foreach ($node->caseStatements as $case) { + if (!($case instanceof PhpParser\Node\CaseStatementNode)) { + continue; + } + $case_line = static::getStartLine($case); + $stmts[] = new ast\Node( + ast\AST_SWITCH_CASE, + 0, + [ + 'cond' => $case->expression !== null ? static::phpParserNodeToAstNode($case->expression) : null, + 'stmts' => static::phpParserStmtlistToAstNode($case->statementList, $case_line, false), + ], + $case_line + ); + } + return new ast\Node(ast\AST_SWITCH, 0, [ + 'cond' => static::phpParserNodeToAstNode($node->expression), + 'stmts' => new ast\Node(ast\AST_SWITCH_LIST, 0, $stmts, $stmts[0]->lineno ?? $node_line), + ], $node_line); + } + + /** + * @param PhpParser\Node[]|PhpParser\Node|Token $stmts + */ + private static function getStartLineOfStatementOrStatements($stmts): int + { + if (is_array($stmts)) { + return isset($stmts[0]) ? self::getStartLine($stmts[0]) : 0; + } + return self::getStartLine($stmts); + } + + private static function phpParserIfStmtToAstIfStmt(PhpParser\Node\Statement\IfStatementNode $node, int $start_line): ast\Node + { + $if_elem = static::astIfElem( + static::phpParserNodeToAstNode($node->expression), + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false. + static::phpParserStmtlistToAstNode( + $node->statements, + self::getStartLineOfStatementOrStatements($node->statements) ?: $start_line, + false + ), + $start_line + ); + $if_elems = [$if_elem]; + foreach ($node->elseIfClauses as $else_if) { + $if_elem_line = self::getStartLine($else_if); + $if_elem = static::astIfElem( + static::phpParserNodeToAstNode($else_if->expression), + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false. + static::phpParserStmtlistToAstNode( + $else_if->statements, + self::getStartLineOfStatementOrStatements($else_if->statements) + ), + $if_elem_line + ); + $if_elems[] = $if_elem; + } + $parser_else_node = $node->elseClause; + if ($parser_else_node) { + $parser_else_line = self::getStartLineOfStatementOrStatements($parser_else_node->statements); + $if_elems[] = static::astIfElem( + null, + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false. + static::phpParserStmtlistToAstNode($parser_else_node->statements, $parser_else_line, false), + $parser_else_line + ); + } + return new ast\Node(ast\AST_IF, 0, $if_elems, $start_line); + } + + /** + * @return ast\Node|string|int|float + */ + private static function astNodeBinaryop(int $flags, PhpParser\Node\Expression\BinaryExpression $n, int $start_line) + { + try { + $left_node = static::phpParserNodeToAstNode($n->leftOperand); + } catch (InvalidNodeException $_) { + if (self::$should_add_placeholders) { + $left_node = static::newPlaceholderExpression($n->leftOperand); + } else { + // convert `;$b ^;` to `;$b;` + return static::phpParserNodeToAstNode($n->rightOperand); + } + } + try { + $right_node = static::phpParserNodeToAstNode($n->rightOperand); + } catch (InvalidNodeException $_) { + if (self::$should_add_placeholders) { + $right_node = static::newPlaceholderExpression($n->rightOperand); + } else { + // convert `;^ $b;` to `;$b;` + return $left_node; + } + } + + return new ast\Node( + ast\AST_BINARY_OP, + $flags, + [ + 'left' => $left_node, + 'right' => $right_node, + ], + $start_line + ); + } + + /** + * Binary assignment operation such as `+=` + * + * @return ast\Node|string|int|float + * (Can return non-Node for an invalid AST if the right-hand is a scalar) + */ + private static function astNodeAssignop(int $flags, PhpParser\Node\Expression\BinaryExpression $n, int $start_line) + { + try { + $var_node = static::phpParserNodeToAstNode($n->leftOperand); + } catch (InvalidNodeException $_) { + if (self::$should_add_placeholders) { + $var_node = new ast\Node(ast\AST_VAR, 0, ['name' => '__INCOMPLETE_VARIABLE__'], $start_line); + } else { + // convert `;= $b;` to `;$b;` + return static::phpParserNodeToAstNode($n->rightOperand); + } + } + $expr_node = static::phpParserNodeToAstNode($n->rightOperand); + return new ast\Node( + ast\AST_ASSIGN_OP, + $flags, + [ + 'var' => $var_node, + 'expr' => $expr_node, + ], + $start_line + ); + } + + /** + * @param PhpParser\Node\Expression\AssignmentExpression|PhpParser\Node\Expression\Variable $n + * @param ?string $doc_comment + * @throws InvalidNodeException if the type can't be converted to a valid AST + * @throws InvalidArgumentException if the passed in class is completely unexpected + */ + private static function phpParserPropelemToAstPropelem($n, ?string $doc_comment): ast\Node + { + if ($n instanceof PhpParser\Node\Expression\AssignmentExpression) { + $name_node = $n->leftOperand; + if (!($name_node instanceof PhpParser\Node\Expression\Variable)) { + throw new InvalidNodeException(); + } + $children = [ + 'name' => static::phpParserNodeToAstNode($name_node->name), + 'default' => $n->rightOperand ? static::phpParserNodeToAstNode($n->rightOperand) : null, + ]; + } elseif ($n instanceof PhpParser\Node\Expression\Variable) { + $name = $n->name; + if (!($name instanceof Token) || !$name->length) { + throw new InvalidNodeException(); + } + $children = [ + 'name' => static::tokenToString($name), + 'default' => null, + ]; + } else { + // @phan-suppress-next-line PhanThrowTypeMismatchForCall debugDumpNodeOrToken can throw + throw new \InvalidArgumentException("Unexpected class for property element: Expected Variable or AssignmentExpression, got: " . static::debugDumpNodeOrToken($n)); + } + + $start_line = self::getStartLine($n); + + $children['docComment'] = static::extractPhpdocComment($n) ?? $doc_comment; + return new ast\Node(ast\AST_PROP_ELEM, 0, $children, $start_line); + } + + private static function phpParserConstelemToAstConstelem(PhpParser\Node\ConstElement $n, ?string $doc_comment): ast\Node + { + $start_line = self::getStartLine($n); + $children = [ + 'name' => static::variableTokenToString($n->name), + 'value' => static::phpParserNodeToAstNode($n->assignment), + ]; + + $children['docComment'] = static::extractPhpdocComment($n) ?? $doc_comment; + return new ast\Node(ast\AST_CONST_ELEM, 0, $children, $start_line); + } + + /** + * @param Token[] $visibility + * @throws RuntimeException if a visibility token was unexpected + */ + private static function phpParserVisibilityToAstVisibility(array $visibility, bool $automatically_add_public = true): int + { + $ast_visibility = 0; + foreach ($visibility as $token) { + switch ($token->kind) { + case TokenKind::VarKeyword: + $ast_visibility |= flags\MODIFIER_PUBLIC; + break; + case TokenKind::PublicKeyword: + $ast_visibility |= flags\MODIFIER_PUBLIC; + break; + case TokenKind::ProtectedKeyword: + $ast_visibility |= flags\MODIFIER_PROTECTED; + break; + case TokenKind::PrivateKeyword: + $ast_visibility |= flags\MODIFIER_PRIVATE; + break; + case TokenKind::StaticKeyword: + $ast_visibility |= flags\MODIFIER_STATIC; + break; + case TokenKind::AbstractKeyword: + $ast_visibility |= flags\MODIFIER_ABSTRACT; + break; + case TokenKind::FinalKeyword: + $ast_visibility |= flags\MODIFIER_FINAL; + break; + default: + throw new \RuntimeException("Unexpected visibility modifier '" . Token::getTokenKindNameFromValue($token->kind) . "'"); + } + } + if ($automatically_add_public && !($ast_visibility & (flags\MODIFIER_PUBLIC | flags\MODIFIER_PROTECTED | flags\MODIFIER_PRIVATE))) { + $ast_visibility |= flags\MODIFIER_PUBLIC; + } + return $ast_visibility; + } + + private static function phpParserPropertyToAstNode(PhpParser\Node\PropertyDeclaration $n, int $start_line): ast\Node + { + $prop_elems = []; + $doc_comment = $n->getDocCommentText(); + + foreach ($n->propertyElements->children ?? [] as $i => $prop) { + if ($prop instanceof Token) { + continue; + } + // @phan-suppress-next-line PhanTypeMismatchArgument casting to a more specific node + $prop_elems[] = static::phpParserPropelemToAstPropelem($prop, $i === 0 ? $doc_comment : null); + } + $flags = static::phpParserVisibilityToAstVisibility($n->modifiers, false); + + $line = $prop_elems[0]->lineno ?? (self::getStartLine($n) ?: $start_line); + $prop_decl = new ast\Node(ast\AST_PROP_DECL, 0, $prop_elems, $line); + $type_line = static::getEndLine($n->typeDeclaration) ?: $start_line; + return new ast\Node(ast\AST_PROP_GROUP, $flags, [ + 'type' => static::phpParserUnionTypeToAstNode($n->typeDeclaration, $n->otherTypeDeclarations, $type_line), + 'props' => $prop_decl, + ], $line); + } + + private static function phpParserClassConstToAstNode(PhpParser\Node\ClassConstDeclaration $n, int $start_line): ast\Node + { + $const_elems = []; + $doc_comment = $n->getDocCommentText(); + foreach ($n->constElements->children ?? [] as $i => $const_elem) { + if ($const_elem instanceof Token) { + continue; + } + // @phan-suppress-next-line PhanTypeMismatchArgument casting to a more specific node + $const_elems[] = static::phpParserConstelemToAstConstelem($const_elem, $i === 0 ? $doc_comment : null); + } + $flags = static::phpParserVisibilityToAstVisibility($n->modifiers); + + return new ast\Node(ast\AST_CLASS_CONST_DECL, $flags, $const_elems, $const_elems[0]->lineno ?? $start_line); + } + + /** + * @throws InvalidNodeException + */ + private static function phpParserConstToAstNode(PhpParser\Node\Statement\ConstDeclaration $n, int $start_line): ast\Node + { + $const_elems = []; + $doc_comment = $n->getDocCommentText(); + foreach ($n->constElements->children ?? [] as $i => $prop) { + if ($prop instanceof Token) { + continue; + } + if (!($prop instanceof PhpParser\Node\ConstElement)) { + throw new InvalidNodeException(); + } + $const_elems[] = static::phpParserConstelemToAstConstelem($prop, $i === 0 ? $doc_comment : null); + } + + return new ast\Node(ast\AST_CONST_DECL, 0, $const_elems, $const_elems[0]->lineno ?? $start_line); + } + + private static function phpParserDeclareListToAstDeclares(PhpParser\Node\DeclareDirective $declare, int $start_line, ?string $first_doc_comment): ast\Node + { + $ast_declare_elements = []; + if ($declare->name->length > 0 && $declare->literal->length > 0) { + // Skip SkippedToken or MissingToken + $children = [ + 'name' => static::tokenToString($declare->name), + 'value' => static::tokenToScalar($declare->literal), + ]; + $doc_comment = static::extractPhpdocComment($declare) ?? $first_doc_comment; + // $first_doc_comment = null; + $children['docComment'] = $doc_comment; + $node = new ast\Node(ast\AST_CONST_ELEM, 0, $children, self::getStartLine($declare)); + $ast_declare_elements[] = $node; + } + return new ast\Node(ast\AST_CONST_DECL, 0, $ast_declare_elements, $start_line); + } + + private static function astStmtDeclare(ast\Node $declares, ?\ast\Node $stmts, int $start_line): ast\Node + { + $children = [ + 'declares' => $declares, + 'stmts' => $stmts, + ]; + return new ast\Node(ast\AST_DECLARE, 0, $children, $start_line); + } + + /** + * @param string|ast\Node $expr + * @param ast\Node $args + */ + private static function astNodeCall($expr, \ast\Node $args, int $start_line): ast\Node + { + if (\is_string($expr)) { + if (substr($expr, 0, 1) === '\\') { + $expr = substr($expr, 1); + } + $expr = new ast\Node(ast\AST_NAME, flags\NAME_FQ, ['name' => $expr], $start_line); + } + return new ast\Node(ast\AST_CALL, 0, ['expr' => $expr, 'args' => $args], $start_line); + } + + /** + * @param ast\Node|string $expr (can parse non-nodes, but they'd cause runtime errors) + * @param ast\Node|string $method + */ + private static function astNodeMethodCall(int $kind, $expr, $method, ast\Node $args, int $start_line): ast\Node + { + return new ast\Node($kind, 0, ['expr' => $expr, 'method' => $method, 'args' => $args], $start_line); + } + + /** + * @param ast\Node|string $class + * @param ast\Node|string $method + */ + private static function astNodeStaticCall($class, $method, ast\Node $args, int $start_line): ast\Node + { + // TODO: is this applicable? + if (\is_string($class)) { + if (substr($class, 0, 1) === '\\') { + $class = substr($class, 1); + } + $class = new ast\Node(ast\AST_NAME, flags\NAME_FQ, ['name' => $class], $start_line); + } + return new ast\Node(ast\AST_STATIC_CALL, 0, ['class' => $class, 'method' => $method, 'args' => $args], $start_line); + } + + /** + * TODO: Get rid of this function? + * @param string|PhpParser\Node|null|array $comments + * @return ?string the doc comment, or null + */ + private static function extractPhpdocComment($comments): ?string + { + if (\is_string($comments)) { + return $comments; + } + if ($comments instanceof PhpParser\Node) { + // TODO: Extract only the substring with doc comment text? + return $comments->getDocCommentText() ?: null; + } + return null; + // TODO: Could extract comments from elsewhere + /* + if ($comments === null) { + return null; + } + if (!(\is_array($comments))) { + throw new AssertionError("Expected an array of comments"); + } + if (\count($comments) === 0) { + return null; + } + */ + } + + private static function phpParserListToAstList(PhpParser\Node\Expression\ListIntrinsicExpression $n, int $start_line): ast\Node + { + $ast_items = []; + $prev_was_element = false; + foreach ($n->listElements->children ?? [] as $item) { + if ($item instanceof Token) { + if (!$prev_was_element) { + $ast_items[] = null; + continue; + } + $prev_was_element = false; + continue; + } else { + $prev_was_element = true; + } + if (!($item instanceof PhpParser\Node\ArrayElement)) { + throw new AssertionError("Expected ArrayElement"); + } + $element_key = $item->elementKey; + $ast_items[] = new ast\Node(ast\AST_ARRAY_ELEM, 0, [ + 'value' => static::phpParserNodeToAstNode($item->elementValue), + 'key' => $element_key !== null ? static::phpParserNodeToAstNode($element_key) : null, + ], self::getStartLine($item)); + } + if (self::$php_version_id_parsing < 70100 && \count($ast_items) === 0) { + $ast_items[] = null; + } + return new ast\Node(ast\AST_ARRAY, flags\ARRAY_SYNTAX_LIST, $ast_items, $start_line); + } + + private static function phpParserArrayToAstArray(PhpParser\Node\Expression\ArrayCreationExpression $n, int $start_line): ast\Node + { + $ast_items = []; + $prev_was_element = false; + foreach ($n->arrayElements->children ?? [] as $item) { + if ($item instanceof Token) { + if (!$prev_was_element) { + $ast_items[] = null; + continue; + } + $prev_was_element = false; + continue; + } else { + $prev_was_element = true; + } + if (!($item instanceof PhpParser\Node\ArrayElement)) { + throw new AssertionError("Expected ArrayElement"); + } + if ($item->dotDotDot) { + $ast_items[] = new ast\Node(ast\AST_UNPACK, 0, [ + 'expr' => static::phpParserNodeToAstNode($item->elementValue), + ], self::getStartLine($item)); + continue; + } + $flags = $item->byRef ? flags\ARRAY_ELEM_REF : 0; + $element_key = $item->elementKey; + $ast_items[] = new ast\Node(ast\AST_ARRAY_ELEM, $flags, [ + 'value' => static::phpParserNodeToAstNode($item->elementValue), + 'key' => $element_key !== null ? static::phpParserNodeToAstNode($element_key) : null, + ], self::getStartLine($item)); + } + if (self::$php_version_id_parsing < 70100) { + $flags = 0; + } else { + $kind = $n->openParenOrBracket->kind; + if ($kind === TokenKind::OpenBracketToken) { + $flags = flags\ARRAY_SYNTAX_SHORT; + } else { + $flags = flags\ARRAY_SYNTAX_LONG; + } + } + // Workaround for ast line choice + return new ast\Node(ast\AST_ARRAY, $flags, $ast_items, $ast_items[0]->lineno ?? $start_line); + } + + /** + * @throws InvalidNodeException if the member name could not be converted + * + * (and various other exceptions) + */ + private static function phpParserMemberAccessExpressionToAstProp(PhpParser\Node\Expression\MemberAccessExpression $n, int $start_line): \ast\Node + { + // TODO: Check for incomplete tokens? + $member_name = $n->memberName; + try { + $name = static::phpParserNodeToAstNode($member_name); // complex expression + } catch (InvalidNodeException $e) { + if (self::$should_add_placeholders) { + $name = self::INCOMPLETE_PROPERTY; + } else { + throw $e; + } + } + return new ast\Node( + $n->arrowToken->kind === TokenKind::QuestionArrowToken ? ast\AST_NULLSAFE_PROP : ast\AST_PROP, + 0, + [ + 'expr' => static::phpParserNodeToAstNode($n->dereferencableExpression), + 'prop' => $name, // ast\Node|string + ], + $start_line + ); + } + + /** + * @return int|string|float|bool|null + */ + private static function tokenToScalar(Token $n) + { + $str = static::tokenToString($n); + $int = \filter_var($str, FILTER_VALIDATE_INT); + if ($int !== false) { + return $int; + } + $float = \filter_var($str, FILTER_VALIDATE_FLOAT); + if ($float !== false) { + return $float; + } + + return StringUtil::parse($str); + } + + /** + * @throws Exception if node is invalid + */ + private static function parseQuotedString(PhpParser\Node\StringLiteral $n): string + { + $start = $n->getStart(); + $text = (string)substr(self::$file_contents, $start, $n->getEndPosition() - $start); + return StringUtil::parse($text); + } + + /** + * @suppress PhanPartialTypeMismatchArgumentInternal hopefully in range + */ + private static function variableTokenToString(Token $n): string + { + return \ltrim(\trim($n->getText(self::$file_contents)), '$'); + } + + /** + * @suppress PhanPartialTypeMismatchReturn this is in bounds and $file_contents is a string + */ + private static function tokenToRawString(Token $n): string + { + return $n->getText(self::$file_contents); + } + + /** @internal */ + private const MAGIC_CONST_LOOKUP = [ + '__LINE__' => flags\MAGIC_LINE, + '__FILE__' => flags\MAGIC_FILE, + '__DIR__' => flags\MAGIC_DIR, + '__NAMESPACE__' => flags\MAGIC_NAMESPACE, + '__FUNCTION__' => flags\MAGIC_FUNCTION, + '__METHOD__' => flags\MAGIC_METHOD, + '__CLASS__' => flags\MAGIC_CLASS, + '__TRAIT__' => flags\MAGIC_TRAIT, + ]; + + // FIXME don't use in places expecting non-strings. + /** + * @phan-suppress PhanPartialTypeMismatchArgumentInternal hopefully in range + */ + private static function tokenToString(Token $n): string + { + $result = \trim($n->getText(self::$file_contents)); + $kind = $n->kind; + if ($kind === TokenKind::VariableName) { + return \trim($result, '$'); + } + return $result; + } + + /** + * @param PhpParser\Node\Expression|PhpParser\Node\QualifiedName|Token $scope_resolution_qualifier + */ + private static function phpParserClassConstFetchToAstClassConstFetch($scope_resolution_qualifier, string $name, int $start_line): ast\Node + { + if (\strcasecmp($name, 'class') === 0) { + $class_node = static::phpParserNonValueNodeToAstNode($scope_resolution_qualifier); + if (!$class_node instanceof ast\Node) { + // e.g. (0)::class + $class_node = new ast\Node(ast\AST_NAME, ast\flags\NAME_FQ, ['name' => $class_node], $start_line); + } + return new ast\Node(ast\AST_CLASS_NAME, 0, [ + 'class' => $class_node, + ], $start_line); + } + return new ast\Node(ast\AST_CLASS_CONST, 0, [ + 'class' => static::phpParserNonValueNodeToAstNode($scope_resolution_qualifier), + 'const' => $name, + ], $start_line); + } + + /** + * @throws InvalidNodeException if the qualified type name could not be converted to a valid php-ast type name + */ + private static function phpParserNameToString(PhpParser\Node\QualifiedName $name): string + { + $name_parts = $name->nameParts; + // TODO: Handle error case (can there be missing parts?) + $result = ''; + foreach ($name_parts as $part) { + $part_as_string = static::tokenToString($part); + if ($part_as_string !== '') { + $result .= \trim($part_as_string); + } + } + $result = \rtrim(\preg_replace('/\\\\{2,}/', '\\', $result), '\\'); + if ($result === '') { + // Would lead to "The name cannot be empty" when parsing + throw new InvalidNodeException(); + } + return $result; + } + + /** + * @param array $children + */ + private static function newAstDecl(int $kind, int $flags, array $children, int $lineno, string $doc_comment = null, string $name = null, int $end_lineno = 0, int $decl_id = -1): ast\Node + { + $decl_children = []; + $decl_children['name'] = $name; + $decl_children['docComment'] = $doc_comment; + $decl_children += $children; + if ($decl_id >= 0) { + $decl_children['__declId'] = $decl_id; + } + $node = new ast\Node($kind, $flags, $decl_children, $lineno); + $node->endLineno = $end_lineno; + return $node; + } + + private static function nextDeclId(): int + { + return self::$decl_id++; + } + + /** @param PhpParser\Node|Token $n the node or token to convert to a placeholder */ + private static function newPlaceholderExpression($n): ast\Node + { + $start_line = self::getStartLine($n); + $name_node = new ast\Node(ast\AST_NAME, flags\NAME_FQ, ['name' => '__INCOMPLETE_EXPR__'], $start_line); + return new ast\Node(ast\AST_CONST, 0, ['name' => $name_node], $start_line); + } + + /** + * @param PhpParser\Node[]|PhpParser\Token[] $children $children + */ + private static function parseMultiPartString(PhpParser\Node\StringLiteral $n, array $children): ast\Node + { + if ($n->startQuote->length >= 3) { + return self::parseMultiPartHeredoc($n, $children); + } + return self::parseMultiPartRegularString($n, $children); + } + + /** + * @param PhpParser\Node[]|PhpParser\Token[] $children $children + */ + private static function parseMultiPartRegularString(PhpParser\Node\StringLiteral $n, array $children): ast\Node + { + $inner_node_parts = []; + $start_quote_text = static::tokenToString($n->startQuote); + $end_quote_text = $n->endQuote->getText(self::$file_contents); + + foreach ($children as $part) { + if ($part instanceof PhpParser\Node) { + $inner_node_parts[] = static::phpParserNodeToAstNode($part); + } else { + $kind = $part->kind; + if (\array_key_exists($kind, self::_IGNORED_STRING_TOKEN_KIND_SET)) { + continue; + } + // ($part->kind === TokenKind::EncapsedAndWhitespace) + $raw_string = static::tokenToRawString($part); + if (\strlen($start_quote_text) > 1) { + // I guess it depends on what's before it. + // TODO: Use a correct heuristic instead + $raw_string = "\n$raw_string\n"; + } + + // Pass in '"\\n"' and get "\n" (somewhat inefficient) + $represented_string = StringUtil::parse($start_quote_text . $raw_string . $end_quote_text); + $inner_node_parts[] = $represented_string; + } + } + return new ast\Node(ast\AST_ENCAPS_LIST, 0, $inner_node_parts, self::getStartLine($children[0])); + } + + /** + * @param PhpParser\Node[]|PhpParser\Token[] $children $children + */ + private static function parseMultiPartHeredoc(PhpParser\Node\StringLiteral $n, array $children): ast\Node + { + $inner_node_parts = []; + $end_of_start_quote = self::$file_contents[$n->startQuote->start + $n->startQuote->length - 1]; + $end_quote_text = $n->endQuote->getText(self::$file_contents); + + $spaces = \strspn($end_quote_text, " \t"); + $raw_spaces = substr($end_quote_text, 0, $spaces); + + foreach ($children as $i => $part) { + if ($part instanceof PhpParser\Node) { + $inner_node_parts[] = static::phpParserNodeToAstNode($part); + continue; + } + $kind = $part->kind; + if (\array_key_exists($kind, self::_IGNORED_STRING_TOKEN_KIND_SET)) { + continue; + } + // ($part->kind === TokenKind::EncapsedAndWhitespace) + $raw_string = static::tokenToRawString($part); + if ($i > 0) { + $raw_string = $raw_spaces . $raw_string; + } + + $represented_string = $spaces > 0 ? \preg_replace("/^" . $raw_spaces . "/m", '', $raw_string) : $raw_string; + if ($end_of_start_quote !== "'") { + $represented_string = StringUtil::parseEscapeSequences($represented_string, null); + } + $inner_node_parts[] = $represented_string; + } + $i = \count($inner_node_parts) - 1; + $s = $inner_node_parts[$i]; + if (\is_string($s)) { + $s = substr($s, 0, -1); + // On Windows, the "\r" must also be removed from the last line of the heredoc + if (substr($s, -1) === "\r") { + $s = substr($s, 0, -1); + } + $inner_node_parts[$i] = $s; + } + + return new ast\Node(ast\AST_ENCAPS_LIST, 0, $inner_node_parts, self::getStartLine($children[0])); + } + + /** + * Gets a string based on environment details that could affect parsing + */ + private static function getEnvironmentDetails(): string + { + static $details = null; + if ($details === null) { + $details = \sha1(var_export([ + \PHP_VERSION, + \PHP_BINARY, + self::getDevelopmentBuildDate(), + \phpversion('ast'), + \ini_get('short_open_tag'), + \sha1((string)\file_get_contents(__DIR__ . '/ast_shim.php')), + class_exists(CLI::class) ? CLI::getDevelopmentVersionId() : 'unknown' + ], true)); + } + return $details; + } + + /** + * For development PHP versions such as 8.0.0-dev, use the build date as part of the cache key to invalidate cached ASTs when this gets rebuilt. + * @suppress PhanImpossibleTypeComparison, PhanRedundantCondition, PhanImpossibleCondition, PhanSuspiciousValueComparison Phan evaluates the strpos to a constant, so this is either impossible or redundant + */ + private static function getDevelopmentBuildDate(): ?string + { + if (\strpos(\PHP_VERSION, '-dev') === false) { + return null; + } + \ob_start(); + \phpinfo(\INFO_GENERAL); + $contents = (string)\ob_get_clean(); + \preg_match('/^Build Date.*=>\s*(.+)$/m', $contents, $matches); + return $matches[1] ?? 'unknown'; + } + + /** + * @return ?string - null if this should not be cached + */ + public function generateCacheKey(string $file_contents, int $version): ?string + { + $details = var_export([ + \sha1($file_contents), + $version, + self::getEnvironmentDetails(), + $this->instance_should_add_placeholders, + ], true); + return \sha1($details); + } + + private static function normalizeTernaryExpression(TernaryExpression $n): TernaryExpression + { + $else = $n->elseExpression; + if (!($else instanceof TernaryExpression)) { + return $n; + } + // The else expression is an unparenthesized ternary expression. Rearrange the parts. + // (Convert a ? b : (c ? d : e) to (a ? b : c) ? d : e) + $inner_left = clone($n); + // @phan-suppress-next-line PhanPartialTypeMismatchProperty pretty much all expressions can be tokens, type is incorrect + $inner_left->elseExpression = $else->condition; + $outer = clone($else); + $outer->condition = $inner_left; + return $outer; + } +} +class_exists(TolerantASTConverterWithNodeMapping::class); diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterPreservingOriginal.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterPreservingOriginal.php new file mode 100644 index 000000000..64f0a27df --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterPreservingOriginal.php @@ -0,0 +1,91 @@ +tolerant_ast_node = $n; + } + return $ast_node; + } + + /** + * @param PhpParser\Node|Token $n - The node from PHP-Parser + * @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent. + * @override + */ + protected static function phpParserNodeToAstNode($n) + { + $ast_node = parent::phpParserNodeToAstNode($n); + if ($ast_node instanceof ast\Node) { + $ast_node->tolerant_ast_node = $n; + } + return $ast_node; + } + + /** + * @param PhpParser\Node|Token $n - The node from PHP-Parser + * @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent. + * @override + */ + protected static function phpParserNonValueNodeToAstNode($n) + { + $ast_node = parent::phpParserNonValueNodeToAstNode($n); + if ($ast_node instanceof ast\Node) { + $ast_node->tolerant_ast_node = $n; + } + return $ast_node; + } + + /** + * @override + */ + protected static function astStmtUseOrGroupUseFromUseClause( + PhpParser\Node\NamespaceUseClause $use_clause, + ?int $parser_use_kind, + int $start_line + ): ast\Node { + // fwrite(STDERR, "Calling astStmtUseOrGroupUseFromUseClause for " . json_encode($use_clause) . "\n"); + $ast_node = parent::astStmtUseOrGroupUseFromUseClause($use_clause, $parser_use_kind, $start_line); + $ast_node->tolerant_ast_node = $use_clause; + return $ast_node; + } + + /** + * @param PhpParser\Node\QualifiedName|Token|null $type + * @override + */ + protected static function phpParserTypeToAstNode($type, int $line): ?\ast\Node + { + $ast_node = parent::phpParserTypeToAstNode($type, $line); + if ($ast_node instanceof ast\Node) { + $ast_node->tolerant_ast_node = $type; + } + return $ast_node; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterWithNodeMapping.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterWithNodeMapping.php new file mode 100644 index 000000000..d144289a1 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/TolerantASTConverterWithNodeMapping.php @@ -0,0 +1,446 @@ +instance_desired_byte_offset = $desired_byte_offset; + $this->instance_handle_selected_node = $handle_selected_node; + } + + /** + * @param Diagnostic[] &$errors @phan-output-reference + * @unused-param $cache + * @throws InvalidArgumentException for invalid $version + * @throws Throwable (after logging) if anything is thrown by the parser + */ + public function parseCodeAsPHPAST(string $file_contents, int $version, array &$errors = [], Cache $cache = null): \ast\Node + { + // Force the byte offset to be within the + $byte_offset = \max(0, \min(\strlen($file_contents), $this->instance_desired_byte_offset)); + self::$desired_byte_offset = $byte_offset; + self::$handle_selected_node = $this->instance_handle_selected_node; + + if (!\in_array($version, self::SUPPORTED_AST_VERSIONS, true)) { + throw new InvalidArgumentException(\sprintf("Unexpected version: want %s, got %d", \implode(', ', self::SUPPORTED_AST_VERSIONS), $version)); + } + + // Aside: this can be implemented as a stub. + try { + $parser_node = static::phpParserParse($file_contents, $errors); + self::findNodeAtOffset($parser_node, $byte_offset); + // phpcs:ignore Generic.Files.LineLength.MaxExceeded + // fwrite(STDERR, "Seeking node: " . json_encode(self::$closest_node_or_token, JSON_PRETTY_PRINT) . "nearby: " . json_encode(self::$closest_node_or_token_symbol, JSON_PRETTY_PRINT) . "\n"); + return $this->phpParserToPhpast($parser_node, $version, $file_contents); + } catch (Throwable $e) { + // fprintf(STDERR, "saw exception: %s\n", $e->getMessage()); + throw $e; + } finally { + self::$closest_node_or_token = null; + self::$closest_node_or_token_symbol = null; + } + } + + /** + * @unused-param $file_contents + * @unused-param $version + * @return ?string - null if this should not be cached + */ + public function generateCacheKey(string $file_contents, int $version): ?string + { + return null; + } + + /** + * Records the closest node or token to the given offset. + * Heuristics are used to ensure that this can map to an ast\Node. + * TODO: Finish implementing + */ + private static function findNodeAtOffset(PhpParser\Node $parser_node, int $offset): void + { + self::$closest_node_or_token = null; + self::$closest_node_or_token_symbol = null; + // fprintf(STDERR, "Seeking offset %d\n", $offset); + self::findNodeAtOffsetRecursive($parser_node, $offset); + } + + /** + * We use a blacklist because there are more many more tokens we want to use the parent for. + * For example, when navigating to class names in comments, the comment can be prior to pretty much any token (e.g. AmpersandToken, PublicKeyword, etc.) + */ + private const KINDS_TO_NOT_RETURN_PARENT = [ + TokenKind::QualifiedName => true, + ]; + + /** + * @param PhpParser\Node $parser_node + * @return bool|PhpParser\Node|PhpParser\Token (Returns $parser_node if that node was what the cursor is pointing directly to) + */ + private static function findNodeAtOffsetRecursive(\Microsoft\PhpParser\Node $parser_node, int $offset) + { + foreach ($parser_node->getChildNodesAndTokens() as $key => $node_or_token) { + if ($node_or_token instanceof Token) { + // fprintf( + // STDERR, + // "Scanning over Token %s (fullStart=%d) %d-%d for offset=%d\n", + // Token::getTokenKindNameFromValue($node_or_token->kind), + // $node_or_token->fullStart, + // $node_or_token->start, + // $node_or_token->getEndPosition(), + // $offset + // ); + if ($node_or_token->getEndPosition() > $offset) { + if ($node_or_token->start > $offset) { + if ($node_or_token->fullStart <= $offset) { + // The cursor falls within the leading comments (doc comment or otherwise) + // of this token. + self::$closest_node_or_token_symbol = $node_or_token; + } elseif (self::$closest_node_or_token_symbol === null) { + // The cursor is hovering over whitespace. + // Give up. + return true; + } + } + if (!\in_array($node_or_token->kind, self::KINDS_TO_NOT_RETURN_PARENT, true)) { + // We want the parent of a Name, e.g. a class + self::$closest_node_or_token = $parser_node; + // fwrite(STDERR, "Found node: " . json_encode($parser_node) . "\n"); + return $parser_node; + } + // fwrite(STDERR, "Found token (parent " . get_class($parser_node) . "): " . json_encode($node_or_token)); + self::$closest_node_or_token = $node_or_token; + // TODO: Handle other cases + return $node_or_token; + } + } + if ($node_or_token instanceof PhpParser\Node) { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall shouldn't happen for generated ASTs + $end_position = $node_or_token->getEndPosition(); + // fprintf(STDERR, "Scanning over Node %s %d-%d\n", get_class($node_or_token), $node_or_token->getStart(), $end_position); + if ($end_position < $offset) { + // End this early if this token ends before the cursor even starts + continue; + } + // Either the node, or true if a the node was found as a descendant, or false. + $state = self::findNodeAtOffsetRecursive($node_or_token, $offset); + if (\is_object($state)) { + // fwrite(STDERR, "Found parent node for $key: " . get_class($parser_node) . "\n"); + // fwrite(STDERR, "Found parent node for $key: " . json_encode($parser_node) . "\n"); + // $state is either a Node or a Token + if (!is_string($key)) { + throw new AssertionError("Expected key to be a string"); + } + return self::adjustClosestNodeOrToken($parser_node, $key); + } elseif ($state) { + return true; + } + } + } + return false; + } + + /** + * This optionally adjusts the closest_node_or_token to a more useful value. + * (so that functionality such as "go to definition" for classes, properties, etc. will work as expected) + * + * @param PhpParser\Node $node the parent node of the old value of + * @param string $key + * @return PhpParser\Node|true + */ + private static function adjustClosestNodeOrToken(PhpParser\Node $node, string $key) + { + switch ($key) { + case 'memberName': + case 'callableExpression': + case 'namespaceName': + case 'namespaceAliasingClause': + // fwrite(STDERR, "Adjusted node: " . json_encode($node) . "\n"); + self::$closest_node_or_token = $node; + return $node; + } + return true; + } + + /** + * @param PhpParser\Node|Token $n - The node from PHP-Parser + * @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent. + * @throws InvalidNodeException when self::$should_add_placeholders is false, like many of these methods. + * @override + */ + protected static function phpParserNodeToAstNodeOrPlaceholderExpr($n) + { + // fprintf(STDERR, "Comparing %s to %s\n", get_class($n), get_class(self::$closest_node_or_token)); + $ast_node = parent::phpParserNodeToAstNodeOrPlaceholderExpr($n); + if ($n === self::$closest_node_or_token) { + self::markNodeAsSelected($n, $ast_node); + } + return $ast_node; + } + + /** + * This marks the tolerant-php-parser Node as being selected, + * and adds any information that will be useful to code handling the corresponding + * + * @param PhpParser\Node|Token $n @phan-unused-param the tolerant-php-parser node that generated the $ast_node + * @param mixed $ast_node the node that was selected because it was under the cursor + */ + private static function markNodeAsSelected($n, $ast_node): void + { + // fwrite(STDERR, "Marking corresponding node as flagged: " . json_encode($n) . "\n" . \Phan\Debug::nodeToString($ast_node) . "\n"); + // fflush(STDERR); + if ($ast_node instanceof ast\Node) { + if (self::$closest_node_or_token_symbol !== null) { + // fwrite(STDERR, "Marking corresponding node as flagged: " . json_encode($n) . "\n" . json_encode($ast_node) . "\n"); + // fflush(STDERR); + + // TODO: This won't work if the comment is at the end of the file. Add a dummy statement or something to associate it with. + // + // TODO: Extract the longest class name or method name from the doc comment + $fragment = self::extractFragmentFromCommentLike(); + if ($fragment === null) { + // We're inside of a string or doc comment but failed to extract a class name + return; + } + // fwrite(STDERR, "Marking selectedFragment = $fragment\n"); + $ast_node->isSelectedApproximate = self::$closest_node_or_token_symbol; + $ast_node->selectedFragment = $fragment; + } + // fwrite(STDERR, "Marking node with kind " . ast\get_kind_name($ast_node->kind) . " as selected\n"); + $ast_node->isSelected = true; + $closure = self::$handle_selected_node; + if ($closure) { + $closure($ast_node); + } + } + } + + private const VALID_FRAGMENT_CHARACTER_REGEX = '/[\\\\a-z0-9_\x7f-\xff]/i'; + + /** + * @return ?string A fragment that is a potentially valid class or function identifier (e.g. 'MyNs\MyClass', '\MyClass') + * for the comment or string under the cursor + * + * TODO: Support method identifiers? + * TODO: Support variables? + * TODO: Implement support for going to function definitions if no class could be found + */ + private static function extractFragmentFromCommentLike(): ?string + { + $offset = self::$desired_byte_offset; + $contents = self::$file_contents; + + // fwrite(STDERR, __METHOD__ . " looking for $offset\n"); + if (!preg_match(self::VALID_FRAGMENT_CHARACTER_REGEX, $contents[$offset] ?? '')) { + // fwrite(STDERR, "Giving up, invalid character at $offset\n"); + // Give up if the character under the cursor is an invalid character for a token + return null; + } + // Iterate backwards to find the start of this class identifier + while ($offset > 0 && preg_match(self::VALID_FRAGMENT_CHARACTER_REGEX, $contents[$offset - 1])) { + $offset--; + } + // fwrite(STDERR, "Moved back to $offset, searching at " . json_encode(substr($contents, $offset, 20)) . "\n"); + + if (preg_match('/\\\\?[a-z_\x7f-\xff][a-z0-9_\x7f-\xff]*(\\\\[a-z_\x7f-\xff][a-z0-9_\x7f-\xff]*)*/i', $contents, $matches, 0, $offset) > 0) { + // fwrite(STDERR, "Returning $matches[0]\n"); + return $matches[0]; + } + return null; + } + + /** + * @param PhpParser\Node|Token $n - The node from PHP-Parser + * @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent. + * @override + */ + protected static function phpParserNodeToAstNode($n) + { + static $callback_map; + static $fallback_closure; + if (\is_null($callback_map)) { + // XXX: If initHandleMap is called on TolerantASTConverter in the parent implementation before TolerantASTConverterWithNodeMapping, + // then static:: in the callbacks would point to TolerantASTConverter, not this subclass. + // + // This is worked around by copying and pasting the parent implementation + $callback_map = static::initHandleMap(); + /** + * @param PhpParser\Node|Token $n + * @throws InvalidArgumentException for invalid token classes + * @suppress PhanThrowTypeMismatchForCall can throw if debugDumpNodeOrToken fails + */ + $fallback_closure = static function ($n, int $unused_start_line): ast\Node { + if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) { + throw new InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . static::debugDumpNodeOrToken($n)); + } + + return static::astStub($n); + }; + } + $callback = $callback_map[\get_class($n)] ?? $fallback_closure; + $result = $callback($n, self::$file_position_map->getStartLine($n)); + if (($result instanceof ast\Node) && $result->kind === ast\AST_NAME) { + $result = new ast\Node(ast\AST_CONST, 0, ['name' => $result], $result->lineno); + } + if ($n === self::$closest_node_or_token) { + self::markNodeAsSelected($n, $result); + } + return $result; + } + + /** + * @param PhpParser\Node|Token $n - The node from PHP-Parser + * @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent. + * @override + */ + protected static function phpParserNonValueNodeToAstNode($n) + { + // fprintf(STDERR, "Comparing %s to %s\n", get_class($n), get_class(self::$closest_node_or_token)); + static $callback_map; + static $fallback_closure; + if (\is_null($callback_map)) { + // XXX: If initHandleMap is called on TolerantASTConverter in the parent implementation before TolerantASTConverterWithNodeMapping, + // then static:: in the callbacks would point to TolerantASTConverter, not this subclass. + // + // This is worked around by copying and pasting the parent implementation + $callback_map = static::initHandleMap(); + /** + * @param PhpParser\Node|Token $n + * @throws InvalidArgumentException for invalid token classes + */ + $fallback_closure = static function ($n, int $unused_start_line): ast\Node { + if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) { + // @phan-suppress-next-line PhanThrowTypeMismatchForCall debugDumpNodeOrToken can throw + throw new InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . static::debugDumpNodeOrToken($n)); + } + return static::astStub($n); + }; + } + $callback = $callback_map[\get_class($n)] ?? $fallback_closure; + $ast_node = $callback($n, self::getStartLine($n)); + if ($n === self::$closest_node_or_token) { + self::markNodeAsSelected($n, $ast_node); + } + return $ast_node; + } + + /** + * @override + */ + protected static function astStmtUseOrGroupUseFromUseClause( + PhpParser\Node\NamespaceUseClause $use_clause, + ?int $parser_use_kind, + int $start_line + ): ast\Node { + // fwrite(STDERR, "Calling astStmtUseOrGroupUseFromUseClause for " . json_encode($use_clause) . "\n"); + $ast_node = parent::astStmtUseOrGroupUseFromUseClause($use_clause, $parser_use_kind, $start_line); + if ($use_clause === self::$closest_node_or_token) { + // NOTE: This selects AST_USE instead of AST_USE_ELEM so that we have + // full information on whether it is a function, constant, or class/namespace + // fwrite(STDERR, "Marking corresponding node as flagged: " . json_encode($use_clause) . "\n" . json_encode($ast_node) . "\n"); + self::markNodeAsSelected($use_clause, $ast_node); + } + return $ast_node; + } + + /** + * @param PhpParser\Node\QualifiedName|Token|null $type + * @override + */ + protected static function phpParserTypeToAstNode($type, int $line): ?\ast\Node + { + $ast_node = parent::phpParserTypeToAstNode($type, $line); + if ($type === self::$closest_node_or_token && $type !== null) { + self::markNodeAsSelected($type, $ast_node); + } + return $ast_node; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ast_shim.php b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ast_shim.php new file mode 100644 index 000000000..9888f63f8 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/TolerantASTConverter/ast_shim.php @@ -0,0 +1,307 @@ + + * @author Nikita Popov + * + * With modifications to be a functional replacement for the data + * structures and global constants of ext-ast. (for class ast\Node) + * + * This supports AST version 70 + * + * However, this file does not define any global functions such as + * ast\parse_code() and ast\parse_file(). (to avoid confusion) + * + * + * @phan-file-suppress PhanUnreferencedConstant, UnusedPluginFileSuppression - Plugins may reference some of these constants + * @phan-file-suppress PhanPluginUnknownArrayPropertyType, PhanPluginUnknownArrayMethodParamType this is a stub + * + * @author Tyson Andre + */ + +// AST KIND CONSTANTS +namespace ast; + +const AST_ARG_LIST = 128; +const AST_LIST = 255; +const AST_ARRAY = 129; +const AST_ENCAPS_LIST = 130; +const AST_EXPR_LIST = 131; +const AST_STMT_LIST = 132; +const AST_IF = 133; +const AST_SWITCH_LIST = 134; +const AST_CATCH_LIST = 135; +const AST_PARAM_LIST = 136; +const AST_CLOSURE_USES = 137; +const AST_PROP_DECL = 138; +const AST_CONST_DECL = 139; +const AST_CLASS_CONST_DECL = 140; +const AST_NAME_LIST = 141; +const AST_TRAIT_ADAPTATIONS = 142; +const AST_USE = 143; +const AST_TYPE_UNION = 144; +const AST_ATTRIBUTE_LIST = 145; +const AST_ATTRIBUTE_GROUP = 146; +const AST_MATCH_ARM_LIST = 147; +const AST_NAME = 2048; +const AST_CLOSURE_VAR = 2049; +const AST_NULLABLE_TYPE = 2050; +const AST_FUNC_DECL = 67; +const AST_CLOSURE = 68; +const AST_METHOD = 69; +const AST_ARROW_FUNC = 71; +const AST_CLASS = 70; +const AST_MAGIC_CONST = 0; +const AST_TYPE = 1; +const AST_VAR = 256; +const AST_CONST = 257; +const AST_UNPACK = 258; +const AST_CAST = 261; +const AST_EMPTY = 262; +const AST_ISSET = 263; +const AST_SHELL_EXEC = 265; +const AST_CLONE = 266; +const AST_EXIT = 267; +const AST_PRINT = 268; +const AST_INCLUDE_OR_EVAL = 269; +const AST_UNARY_OP = 270; +const AST_PRE_INC = 271; +const AST_PRE_DEC = 272; +const AST_POST_INC = 273; +const AST_POST_DEC = 274; +const AST_YIELD_FROM = 275; +const AST_GLOBAL = 277; +const AST_UNSET = 278; +const AST_RETURN = 279; +const AST_LABEL = 280; +const AST_REF = 281; +const AST_HALT_COMPILER = 282; +const AST_ECHO = 283; +const AST_THROW = 284; +const AST_GOTO = 285; +const AST_BREAK = 286; +const AST_CONTINUE = 287; +const AST_CLASS_NAME = 276; +const AST_CLASS_CONST_GROUP = 546; +const AST_DIM = 512; +const AST_PROP = 513; +const AST_NULLSAFE_PROP = 514; +const AST_STATIC_PROP = 515; +const AST_CALL = 516; +const AST_CLASS_CONST = 517; +const AST_ASSIGN = 518; +const AST_ASSIGN_REF = 519; +const AST_ASSIGN_OP = 520; +const AST_BINARY_OP = 521; +const AST_ARRAY_ELEM = 526; +const AST_NEW = 527; +const AST_INSTANCEOF = 528; +const AST_YIELD = 529; +const AST_STATIC = 532; +const AST_WHILE = 533; +const AST_DO_WHILE = 534; +const AST_IF_ELEM = 535; +const AST_SWITCH = 536; +const AST_SWITCH_CASE = 537; +const AST_DECLARE = 538; +const AST_PROP_ELEM = 775; +const AST_PROP_GROUP = 774; +const AST_CONST_ELEM = 776; +const AST_USE_TRAIT = 539; +const AST_TRAIT_PRECEDENCE = 540; +const AST_METHOD_REFERENCE = 541; +const AST_NAMESPACE = 542; +const AST_USE_ELEM = 543; +const AST_TRAIT_ALIAS = 544; +const AST_GROUP_USE = 545; +const AST_ATTRIBUTE = 547; +const AST_MATCH = 548; +const AST_MATCH_ARM = 549; +const AST_NAMED_ARG = 550; +const AST_METHOD_CALL = 768; +const AST_NULLSAFE_METHOD_CALL = 769; +const AST_STATIC_CALL = 770; +const AST_CONDITIONAL = 771; +const AST_TRY = 772; +const AST_CATCH = 773; +const AST_FOR = 1024; +const AST_FOREACH = 1025; +const AST_PARAM = 1280; +// END AST KIND CONSTANTS + +// AST FLAG CONSTANTS +namespace ast\flags; + +const NAME_FQ = 0; +const NAME_NOT_FQ = 1; +const NAME_RELATIVE = 2; +const MODIFIER_PUBLIC = 1; +const MODIFIER_PROTECTED = 2; +const MODIFIER_PRIVATE = 4; +const MODIFIER_STATIC = 16; +const MODIFIER_ABSTRACT = 64; +const MODIFIER_FINAL = 32; +const PARAM_MODIFIER_PUBLIC = 1; +const PARAM_MODIFIER_PROTECTED = 2; +const PARAM_MODIFIER_PRIVATE = 4; +const RETURNS_REF = 4096; +const FUNC_RETURNS_REF = 4096; +const FUNC_GENERATOR = 16777216; +const ARRAY_ELEM_REF = 1; +const CLOSURE_USE_REF = 1; +const CLASS_ABSTRACT = 64; +const CLASS_FINAL = 32; +const CLASS_TRAIT = 2; +const CLASS_INTERFACE = 1; +const CLASS_ANONYMOUS = 4; +const PARAM_REF = 8; +const PARAM_VARIADIC = 16; +const TYPE_NULL = 1; +const TYPE_FALSE = 2; +const TYPE_BOOL = 17; +const TYPE_LONG = 4; +const TYPE_DOUBLE = 5; +const TYPE_STRING = 6; +const TYPE_ARRAY = 7; +const TYPE_OBJECT = 8; +const TYPE_CALLABLE = 12; +const TYPE_VOID = 14; +const TYPE_ITERABLE = 13; +const TYPE_STATIC = 15; +const TYPE_MIXED = 16; +const UNARY_BOOL_NOT = 14; +const UNARY_BITWISE_NOT = 13; +const UNARY_SILENCE = 260; +const UNARY_PLUS = 261; +const UNARY_MINUS = 262; +const BINARY_BOOL_AND = 259; +const BINARY_BOOL_OR = 258; +const BINARY_BOOL_XOR = 15; +const BINARY_BITWISE_OR = 9; +const BINARY_BITWISE_AND = 10; +const BINARY_BITWISE_XOR = 11; +const BINARY_CONCAT = 8; +const BINARY_ADD = 1; +const BINARY_SUB = 2; +const BINARY_MUL = 3; +const BINARY_DIV = 4; +const BINARY_MOD = 5; +const BINARY_POW = 12; +const BINARY_SHIFT_LEFT = 6; +const BINARY_SHIFT_RIGHT = 7; +const BINARY_IS_IDENTICAL = 16; +const BINARY_IS_NOT_IDENTICAL = 17; +const BINARY_IS_EQUAL = 18; +const BINARY_IS_NOT_EQUAL = 19; +const BINARY_IS_SMALLER = 20; +const BINARY_IS_SMALLER_OR_EQUAL = 21; +const BINARY_IS_GREATER = 256; +const BINARY_IS_GREATER_OR_EQUAL = 257; +const BINARY_SPACESHIP = 170; +const BINARY_COALESCE = 260; +const EXEC_EVAL = 1; +const EXEC_INCLUDE = 2; +const EXEC_INCLUDE_ONCE = 4; +const EXEC_REQUIRE = 8; +const EXEC_REQUIRE_ONCE = 16; +const USE_NORMAL = 1; +const USE_FUNCTION = 2; +const USE_CONST = 4; +const MAGIC_LINE = 375; +const MAGIC_FILE = 376; +const MAGIC_DIR = 377; +const MAGIC_NAMESPACE = 382; +const MAGIC_FUNCTION = 381; +const MAGIC_METHOD = 380; +const MAGIC_CLASS = 378; +const MAGIC_TRAIT = 379; +const ARRAY_SYNTAX_LIST = 1; +const ARRAY_SYNTAX_LONG = 2; +const ARRAY_SYNTAX_SHORT = 3; +const DIM_ALTERNATIVE_SYNTAX = 2; +const PARENTHESIZED_CONDITIONAL = 1; +// END AST FLAG CONSTANTS + +namespace ast; + +// The parse_file(), parse_code(), get_kind_name(), and kind_uses_flags() are deliberately omitted from this stub. +// Use Phan\Debug and Phan\AST\Parser instead. + +if (!\class_exists('\ast\Node')) { + /** + * This class describes a single node in a PHP AST. + * @suppress PhanRedefineClassInternal + */ + class Node + { + /** @var int AST Node Kind. Values are one of ast\AST_* constants. */ + public $kind; + + /** + * @var int AST Flags. + * Certain node kinds have flags that can be set. + * These will be a bitfield of ast\flags\* constants. + */ + public $flags; + + /** @var int Line the node starts in */ + public $lineno; + + /** @var array Child nodes (may be empty) */ + public $children; + /** + * A constructor which validates data types but not the values themselves. + * For backwards compatibility reasons, all values are optional and properties default to null + * @suppress PhanPossiblyNullTypeMismatchProperty + */ + public function __construct(int $kind = null, int $flags = null, array $children = null, int $lineno = null) + { + $this->kind = $kind; + $this->flags = $flags; + $this->children = $children; + $this->lineno = $lineno; + } + } +} + +if (!\class_exists('ast\Metadata')) { + /** + * Metadata entry for a single AST kind, as returned by ast\get_metadata(). + * @suppress PhanRedefineClassInternal + * @suppress PhanUnreferencedClass + */ + class Metadata + { + /** + * @var int AST node kind (one of the ast\AST_* constants). + * @suppress PhanUnreferencedPublicProperty + */ + public $kind; + + /** + * @var string Name of the node kind (e.g. "AST_NAME"). + * @suppress PhanUnreferencedPublicProperty + */ + public $name; + + /** + * @var list Array of supported flags. The flags are given as names of constants, such as + * "ast\flags\TYPE_STRING". + * @suppress PhanUnreferencedPublicProperty + */ + public $flags; + + /** + * @var bool Whether the flags are exclusive or combinable. Exclusive flags should be checked + * using ===, while combinable flags should be checked using &. + * @suppress PhanUnreferencedPublicProperty + */ + // phpcs:ignore Phan.NamingConventions.ValidUnderscoreVariableName.MemberVarNotUnderscore + public $flagsCombinable; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/UnionTypeVisitor.php b/bundled-libs/phan/phan/src/Phan/AST/UnionTypeVisitor.php new file mode 100644 index 000000000..d643af545 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/UnionTypeVisitor.php @@ -0,0 +1,4113 @@ +code_base = $code_base; + $this->context = $context; + + $this->should_catch_issue_exception = + $should_catch_issue_exception; + } + + /** + * @param CodeBase $code_base + * The code base within which we're operating + * + * @param Context $context + * The context of the parser at the node for which we'd + * like to determine a type + * + * @param Node|string|bool|int|float|null $node + * The node for which we'd like to determine its type + * + * @param bool $should_catch_issue_exception + * Set to true to cause loggable issues to be thrown + * instead + * + * @return UnionType + * The UnionType associated with the given node + * in the given Context within the given CodeBase + * + * @throws IssueException + * If $should_catch_issue_exception is false an IssueException may + * be thrown for optional issues. + */ + public static function unionTypeFromNode( + CodeBase $code_base, + Context $context, + $node, + bool $should_catch_issue_exception = true + ): UnionType { + if (!($node instanceof Node)) { + if ($node === null) { + // NOTE: Parameter default checks expect this to return empty + return UnionType::empty(); + } + return Type::fromObject($node)->asRealUnionType(); + } + $node_id = \spl_object_id($node); + + $cached_union_type = $context->getUnionTypeOfNodeIfCached($node_id, $should_catch_issue_exception); + if ($cached_union_type !== null) { + return $cached_union_type; + } + + if ($should_catch_issue_exception) { + try { + $union_type = (new self( + $code_base, + $context, + true + ))->{Element::VISIT_LOOKUP_TABLE[$node->kind] ?? 'visit'}($node); + $context->setCachedUnionTypeOfNode($node_id, $union_type, true); + return $union_type; + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $code_base, + $context, + $exception->getIssueInstance() + ); + return UnionType::empty(); + } + } + + $union_type = (new self( + $code_base, + $context, + false + ))->{Element::VISIT_LOOKUP_TABLE[$node->kind] ?? 'visit'}($node); + + $context->setCachedUnionTypeOfNode($node_id, $union_type, false); + return $union_type; + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node (@phan-unused-param) + * An AST node we'd like to determine the UnionType + * for + * + * @return UnionType + * The set of types associated with the given node + */ + public function visit(Node $node): UnionType + { + /* + throw new NodeException($node, + 'Visitor not implemented for node of type ' + . Debug::nodeName($node) + ); + */ + return UnionType::empty(); + } + + /** + * Visit a node with kind `\ast\AST_POST_INC` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitPostInc(Node $node): UnionType + { + // Real types aren't certain, since this doesn't throw even for object or array types + // TODO: Check if union type is sane (string/int) + return self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + )->asNonLiteralType(); + } + + /** + * Visit a node with kind `\ast\AST_POST_DEC` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitPostDec(Node $node): UnionType + { + // Real types aren't certain, since this doesn't throw even for object or array types + // TODO: Check if union type is sane (string/int) + return self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + )->asNonLiteralType(); + } + + /** + * Visit a node with kind `\ast\AST_PRE_DEC` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitPreDec(Node $node): UnionType + { + // Real types aren't certain, since this doesn't throw even for object or array types + // TODO: Check if union type is sane (string/int) + return self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + )->asNonLiteralType()->getTypeAfterIncOrDec(); + } + + /** + * Visit a node with kind `\ast\AST_PRE_INC` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * TODO: in PostOrderAnalysisVisitor, set the type to unknown for ++/-- + */ + public function visitPreInc(Node $node): UnionType + { + // Real types aren't certain, since this doesn't throw even for object or array types + // TODO: Check if union type is sane (string/int) + return self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + )->asNonLiteralType()->getTypeAfterIncOrDec(); + } + + /** + * Visit a node with kind `\ast\AST_CLONE` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitClone(Node $node): UnionType + { + // Phan checks elsewhere if union type is sane (Any object type) + $type = self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + )->objectTypes(); + if ($type->isEmpty()) { + return ObjectType::instance(false)->asRealUnionType(); + } + $type = $type->nonNullableClone(); + if (!$type->hasRealTypeSet()) { + $type = $type->withRealTypeSet([ObjectType::instance(false)]); + } + return $type; + } + + /** + * Visit a node with kind `\ast\AST_EMPTY` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitEmpty(Node $node): UnionType + { + return BoolType::instance(false)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_ISSET` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitIsset(Node $node): UnionType + { + return BoolType::instance(false)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_INCLUDE_OR_EVAL` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitIncludeOrEval(Node $node): UnionType + { + // require() can return arbitrary objects. Lets just + // say that we don't know what it is and move on + return UnionType::empty(); + } + + private static function literalIntUnionType(int $value): UnionType + { + return LiteralIntType::instanceForValue($value, false)->asRealUnionType(); + } + + private static function literalStringUnionType(string $value): UnionType + { + return LiteralStringType::instanceForValue($value, false)->asRealUnionType(); + } + + public const MAGIC_CONST_NAME_MAP = [ + ast\flags\MAGIC_LINE => '__LINE__', + ast\flags\MAGIC_FILE => '__FILE__', + ast\flags\MAGIC_DIR => '__DIR__', + ast\flags\MAGIC_NAMESPACE => '__NAME__', + ast\flags\MAGIC_FUNCTION => '__FUNCTION__', + ast\flags\MAGIC_METHOD => '__METHOD__', + ast\flags\MAGIC_CLASS => '__CLASS__', + ast\flags\MAGIC_TRAIT => '__TRAIT__', + ]; + + private function warnAboutUndeclaredMagicConstant(Node $node, string $details): void + { + $this->emitIssue( + Issue::UndeclaredMagicConstant, + $node->lineno, + self::MAGIC_CONST_NAME_MAP[$node->flags], + $details + ); + } + + /** + * Visit a node with kind `\ast\AST_MAGIC_CONST` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitMagicConst(Node $node): UnionType + { + $flags = $node->flags; + switch ($flags) { + case ast\flags\MAGIC_CLASS: + if ($this->context->isInClassScope()) { + // Works in classes, traits, and interfaces + return self::literalStringUnionType(\ltrim($this->context->getClassFQSEN()->__toString(), '\\')); + } + $this->warnAboutUndeclaredMagicConstant($node, 'used outside of classlike'); + break; + case ast\flags\MAGIC_FUNCTION: + if ($this->context->isInFunctionLikeScope()) { + $fqsen = $this->context->getFunctionLikeFQSEN(); + if ($fqsen instanceof FullyQualifiedMethodName) { + // For NS\MyClass::methodName, return 'methodName' + $value = $fqsen->getName(); + } else { + if ($fqsen->isClosure()) { + $this->emitIssue( + Issue::SuspiciousMagicConstant, + $node->lineno, + '__FUNCTION__', + "used inside of a closure instead of a function/method - the value is always '{closure}'" + ); + $value = '{closure}'; + } else { + // For \NS\my_function, return 'NS\my_function'. + $value = \ltrim($fqsen->__toString(), '\\'); + } + } + return self::literalStringUnionType($value); + } + $this->warnAboutUndeclaredMagicConstant($node, 'used outside of functionlike'); + break; + case ast\flags\MAGIC_METHOD: + if ($this->context->isInFunctionLikeScope()) { + // Emits method or function FQSEN. + $fqsen = $this->context->getFunctionLikeFQSEN(); + if (!$fqsen instanceof FullyQualifiedMethodName) { + $this->emitIssue( + Issue::SuspiciousMagicConstant, + $node->lineno, + '__METHOD__', + 'used inside of a function/closure instead of a method' + ); + } + return self::literalStringUnionType($fqsen->isClosure() ? '{closure}' : \ltrim($fqsen->__toString(), '\\')); + } + $this->warnAboutUndeclaredMagicConstant($node, 'used outside of a functionlike'); + break; + case ast\flags\MAGIC_DIR: + return self::literalStringUnionType(\dirname(Config::projectPath($this->context->getFile()))); + case ast\flags\MAGIC_FILE: + return self::literalStringUnionType(Config::projectPath($this->context->getFile())); + case ast\flags\MAGIC_LINE: + return self::literalIntUnionType($node->lineno); + case ast\flags\MAGIC_NAMESPACE: + return self::literalStringUnionType(\ltrim($this->context->getNamespace(), '\\')); + case ast\flags\MAGIC_TRAIT: + // TODO: Could check if in trait, low importance. + if (!$this->context->isInClassScope()) { + $this->warnAboutUndeclaredMagicConstant($node, 'used outside of a trait'); + break; + } + $fqsen = $this->context->getClassFQSEN(); + if ($this->code_base->hasClassWithFQSEN($fqsen)) { + if (!$this->code_base->getClassByFQSEN($fqsen)->isTrait()) { + $this->warnAboutUndeclaredMagicConstant($node, 'used in a classlike that wasn\'t a trait'); + break; + } + } + return self::literalStringUnionType(\ltrim($this->context->getClassFQSEN()->__toString(), '\\')); + default: + return StringType::instance(false)->asPHPDocUnionType(); + } + + return self::literalStringUnionType(''); + } + + /** + * Visit a node with kind `\ast\AST_ASSIGN_REF` + * @see self::visitAssign() + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitAssignRef(Node $node): UnionType + { + // TODO: Is there any way this should differ from analysis + // (e.g. should subsequent assignments affect the right-hand Node?) + return $this->visitAssign($node); + } + + /** + * Visit a node with kind `\ast\AST_SHELL_EXEC` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitShellExec(Node $node): UnionType + { + return StringType::instance(true)->asRealUnionType(); + } + + /** + * @throws IssueException if the parent type could not be resolved + */ + public static function findParentType(Context $context, CodeBase $code_base): ?Type + { + if (!$context->isInClassScope()) { + throw new IssueException( + Issue::fromType(Issue::ContextNotObject)( + $context->getFile(), + $context->getLineNumberStart(), + ['parent'] + ) + ); + } + $class = $context->getClassInScope($code_base); + + $parent_type_option = $class->getParentTypeOption(); + if ($parent_type_option->isDefined()) { + return $parent_type_option->get(); + } + + // Using `parent` in a class or interface without a parent is always invalid. + // Doing this in a trait may or not be valid. + if (!$class->isTrait()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::ParentlessClass, + $context->getLineNumberStart(), + (string)$class->getFQSEN() + ); + } + + return null; + } + + /** + * Visit a node with kind `\ast\AST_NAME` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitName(Node $node): UnionType + { + $name = $node->children['name']; + try { + if ($node->flags & \ast\flags\NAME_NOT_FQ) { + if (\strcasecmp('parent', $name) === 0) { + $parent_type = self::findParentType($this->context, $this->code_base); + return $parent_type ? $parent_type->asRealUnionType() : UnionType::empty(); + } + + return Type::fromStringInContext( + $name, + $this->context, + Type::FROM_NODE + )->asRealUnionType(); + } + + if ($node->flags & \ast\flags\NAME_RELATIVE) { // $x = new namespace\Foo(); + $name = \rtrim($this->context->getNamespace(), '\\') . '\\' . $name; + return Type::fromFullyQualifiedString( + $name + )->asRealUnionType(); + } + // Sometimes 0 for a fully qualified name? + + return Type::fromFullyQualifiedString( + '\\' . $name + )->asRealUnionType(); + } catch (FQSENException $e) { + $this->emitIssue( + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike, + $node->lineno, + $e->getFQSEN() + ); + return UnionType::empty(); + } + } + + /** + * Visit a node with kind `\ast\AST_TYPE` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws AssertionError if the type flags were unknown + */ + public function visitType(Node $node): UnionType + { + switch ($node->flags) { + case \ast\flags\TYPE_ARRAY: + return ArrayType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_BOOL: + return BoolType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_CALLABLE: + return CallableType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_DOUBLE: + return FloatType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_ITERABLE: + return IterableType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_LONG: + return IntType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_NULL: + return NullType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_OBJECT: + return ObjectType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_STRING: + return StringType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_VOID: + return VoidType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_FALSE: + return FalseType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_STATIC: + return StaticType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_MIXED: + return MixedType::instance(false)->asRealUnionType(); + default: + \Phan\Debug::printNode($node); + throw new AssertionError("All flags must match. Found ($node->flags) " + . Debug::astFlagDescription($node->flags ?? 0, $node->kind)); + } + } + + /** + * Visit a node with kind `\ast\AST_TYPE_UNION` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws AssertionError if the type flags were unknown + */ + public function visitTypeUnion(Node $node): UnionType + { + // TODO: Validate that there aren't any duplicates + if (\count($node->children) === 1) { + // Might be possible due to the polyfill in the future. + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + return $this->__invoke($node->children[0]); + } + $types = []; + foreach ($node->children as $c) { + if (!$c instanceof Node) { + throw new AssertionError("Saw non-node in union type"); + } + $kind = $c->kind; + if ($kind === ast\AST_TYPE) { + $types[] = $this->visitType($c); + } elseif ($kind === ast\AST_NAME) { + if ($this->context->getScope()->isInTraitScope()) { + $name = \strtolower($node->children['name']); + if ($name === 'self') { + $types[] = SelfType::instance(false)->asRealUnionType(); + continue; + } elseif ($name === 'static') { + $types[] = StaticType::instance(false)->asRealUnionType(); + continue; + } + } + $types[] = $this->visitName($c); + } else { + throw new AssertionError("Expected union type to be composed of types and names"); + } + } + $result = []; + foreach ($types as $union_type) { + foreach ($union_type->getTypeSet() as $type) { + $result[] = $type; + } + } + return UnionType::of($result, $result); + } + + /** + * Visit a node with kind `\ast\AST_NULLABLE_TYPE` representing + * a nullable type such as `?string`. + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitNullableType(Node $node): UnionType + { + // Get the type + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable other node kinds have nullable type + $union_type = $this->__invoke($node->children['type']); + + // Make each nullable + return $union_type->asMappedUnionType(static function (Type $type): Type { + return $type->withIsNullable(true); + }); + } + + /** + * @param int|float|string|Node $node + */ + public static function unionTypeFromLiteralOrConstant(CodeBase $code_base, Context $context, $node): ?UnionType + { + if ($node instanceof Node) { + // TODO: There are a lot more types of expressions that have known union types that this doesn't handle. + // Maybe callers should call something else if this fails (e.g. it's useful for them to know if an expression becomes a string) + if (\in_array($node->kind, [\ast\AST_CONST, \ast\AST_CLASS_CONST, \ast\AST_CLASS_NAME], true)) { + try { + return UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node, false); + } catch (IssueException $_) { + return null; + } + } + $result = (new ContextNode($code_base, $context, $node))->getEquivalentPHPValue(); + + if ($result instanceof Node) { + return null; + } + // XXX This isn't 100% accurate for constants that can have different definitions based on the environment, etc. + return Type::fromObjectExtended($result)->asRealUnionType(); + } + // Otherwise, this is an int/float/string. + if (!is_scalar($node)) { + throw new TypeError('node must be Node or scalar'); + } + return Type::fromObject($node)->asRealUnionType(); + } + + /** + * Returns the union type from a type in a parameter/return signature of a function-like. + * This preserves `self` and `static` + * @param Node $node + */ + public function fromTypeInSignature(Node $node): UnionType + { + $is_nullable = $node->kind === ast\AST_NULLABLE_TYPE; + if ($is_nullable) { + $node = $node->children['type']; + if (!$node instanceof Node) { + // Work around bug (in polyfill parser?) + return UnionType::empty(); + } + } + $kind = $node->kind; + if ($kind === ast\AST_TYPE) { + $result = $this->visitType($node); + } elseif ($kind === ast\AST_NAME) { + if ($this->context->getScope()->isInTraitScope()) { + $name = \strtolower($node->children['name']); + if ($name === 'self') { + return SelfType::instance($is_nullable)->asRealUnionType(); + } elseif ($name === 'static') { + return StaticType::instance($is_nullable)->asRealUnionType(); + } + } + $result = $this->visitName($node); + } elseif ($kind === ast\AST_TYPE_UNION) { + $result = $this->visitTypeUnion($node); + } else { + throw new AssertionError("Expected a type, union type, or a name in the signature: node: " . Debug::nodeToString($node)); + } + if ($is_nullable) { + return $result->nullableClone(); + } + return $result; + } + + /** + * @param int|float|string|Node $cond + */ + public static function checkCondUnconditionalTruthiness($cond): ?bool + { + if ($cond instanceof Node) { + if ($cond->kind === \ast\AST_CONST) { + switch (\strtolower($cond->children['name']->children['name'] ?? '')) { + case 'true': + return true; + case 'false': + return false; + case 'null': + return false; + default: + // Could add heuristics based on internal/user-defined constant values, but that is unreliable. + // (E.g. feature flags for an extension may be true or false, depending on the environment) + // (and Phan doesn't store constant values for user-defined constants, only the types) + return null; + } + } + return null; + } + // Otherwise, this is an int/float/string. + // Use the exact same truthiness rules as PHP to check if the conditional is truthy. + // (e.g. "0" and 0.0 and '' are false) + if (!is_scalar($cond)) { + // Phan should have emitted a PhanSyntaxError elsewhere + return null; + } + return (bool)$cond; + } + + /** + * Visit a node with kind `\ast\AST_CONDITIONAL` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitConditional(Node $node): UnionType + { + $cond_node = $node->children['cond']; + $cond_truthiness = self::checkCondUnconditionalTruthiness($cond_node); + // For the shorthand $a ?: $b, the cond node will be the truthy value. + // Note: an ast node will never be null(can be unset), it will be a const AST node with the name null. + $true_node = $node->children['true'] ?? $cond_node; + $false_node = $node->children['false']; + + // Rarely, a conditional will always be true or always be false. + if ($cond_truthiness !== null) { + // TODO: Add no-op checks in another PR, if they don't already exist for conditional. + if ($cond_truthiness) { + // The condition is unconditionally true + return UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $true_node + ); + } else { + // The condition is unconditionally false + + // Add the type for the 'false' side + return UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['false'] + ); + } + } + if ($true_node !== $cond_node) { + // Visit the condition to check for undefined variables. + UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $cond_node + ); + } + // TODO: emit no-op if $cond_node is a literal, such as `if (2)` + // - Also note that some things such as `true` and `false` are \ast\AST_NAME nodes. + + if ($cond_node instanceof Node) { + $base_context = $this->context; + // TODO: Use different contexts and merge those, in case there were assignments or assignments by reference in both sides of the conditional? + // Reuse the BranchScope (sort of unintuitive). The ConditionVisitor returns a clone and doesn't modify the original. + $base_context_scope = $this->context->getScope(); + if ($base_context_scope instanceof GlobalScope) { + $base_context = $base_context->withScope(new BranchScope($base_context_scope)); + } + // Doesn't seem to be necessary to run BlockAnalysisVisitor + // $base_context = (new BlockAnalysisVisitor($this->code_base, $base_context))->__invoke($cond_node); + $true_context = (new ConditionVisitor( + $this->code_base, + isset($node->children['true']) ? $base_context : $this->context // special case: $c = (($d = foo()) ?: 'fallback') + ))->__invoke($cond_node); + $false_context = (new NegatedConditionVisitor( + $this->code_base, + $base_context + ))->__invoke($cond_node); + + if (!isset($node->children['true'])) { + $true_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $true_context, + $true_node + ); + + $false_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $false_context, + $false_node + ); + if ($false_node instanceof Node && BlockExitStatusChecker::willUnconditionallyThrowOrReturn($false_node)) { + return $true_type->nonFalseyClone(); + } + + $true_type_is_empty = $true_type->isEmpty(); + if (!$false_type->isEmpty()) { + // E.g. `foo() ?: 2` where foo is nullable or possibly false. + if ($true_type->containsFalsey()) { + $true_type = $true_type->nonFalseyClone(); + } + } + + // Add the type for the 'true' side to the 'false' side + $union_type = $true_type->withUnionType($false_type); + + // If one side has an unknown type but the other doesn't + // we can't let the unseen type get erased. Unfortunately, + // we need to add 'mixed' in so that we know it could be + // anything at all. + // + // See Issue #104 + if ($true_type_is_empty xor $false_type->isEmpty()) { + $union_type = $union_type->withType( + MixedType::instance(false) + ); + } + + return $union_type; + } + } else { + $true_context = $this->context; + $false_context = $this->context; + } + // Postcondition: This is (cond_expr) ? (true_expr) : (false_expr) + + $true_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $true_context, + $true_node + ); + + $false_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $false_context, + $node->children['false'] + ); + if ($false_node instanceof Node && $false_node->kind === ast\AST_THROW) { + return $true_type; + } + if ($true_node instanceof Node && $true_node->kind === ast\AST_THROW) { + return $false_type; + } + + // Add the type for the 'true' side to the 'false' side + $union_type = $true_type->withUnionType($false_type); + + // If one side has an unknown type but the other doesn't + // we can't let the unseen type get erased. Unfortunately, + // we need to add 'mixed' in so that we know it could be + // anything at all. + // + // See Issue #104 + if ($true_type->isEmpty() xor $false_type->isEmpty()) { + $union_type = $union_type->withType( + MixedType::instance(false) + ); + } + + return $union_type; + } + + /** + * Visit a node with kind `\ast\AST_MATCH` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * @suppress PhanPossiblyUndeclaredProperty + */ + public function visitMatch(Node $node): UnionType + { + // TODO: Support inferring the type from the conditional + $union_types = []; + foreach ($node->children['stmts']->children as $arm_node) { + if (!BlockExitStatusChecker::willUnconditionallyThrowOrReturn($arm_node)) { + $union_types[] = UnionTypeVisitor::unionTypeFromNode($this->code_base, clone($this->context), $arm_node->children['expr']); + } + } + if (!$union_types) { + return VoidType::instance(false)->asRealUnionType(); + } + return UnionType::merge($union_types); + } + + /** + * Visit a node with kind `\ast\AST_ARRAY` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitArray(Node $node): UnionType + { + $children = $node->children; + if (\count($children) > 0) { + $key_set = $this->getEquivalentArraySet($node); + if (\is_array($key_set)) { + // XXX decide how to deal with array components when the top level array is real + return $this->createArrayShapeType($key_set)->asRealUnionType(); + } + + $value_types_builder = new UnionTypeBuilder(); + $real_value_types_builder = new UnionTypeBuilder(); + $record_real_union_type = static function (UnionType $union_type) use (&$real_value_types_builder): void { + if (!$real_value_types_builder) { + return; + } + $real_types = $union_type->getRealTypeSet(); + if (!$real_types) { + $real_value_types_builder = null; + return; + } + foreach ($real_types as $type) { + $real_value_types_builder->addType($type); + } + }; + + // XXX is this slow for extremely large arrays because of in_array check in UnionTypeBuilder? + $is_definitely_non_empty = false; + $has_key = false; + foreach ($children as $child) { + if (!($child instanceof Node)) { + // Skip this, we already emitted a syntax error. + $real_value_types_builder = null; + $has_key = true; + continue; + } + if ($child->kind === ast\AST_UNPACK) { + // Analyze PHP 7.4's array spread operator, e.g. `[$a, ...$array, $b]` + $new_union_type = $this->analyzeUnpack($child, true); + $value_types_builder->addUnionType($new_union_type); + $record_real_union_type($new_union_type); + continue; + } + $is_definitely_non_empty = true; + $value = $child->children['value']; + $has_key = $has_key || isset($child->children['key']); + if ($value instanceof Node) { + $element_value_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $value, + $this->should_catch_issue_exception + ); + if ($element_value_type->isEmpty()) { + $value_types_builder->addType(MixedType::instance(false)); + $real_value_types_builder = null; + } else { + if ($element_value_type->isVoidType()) { + $this->emitIssue( + Issue::TypeVoidExpression, + $node->lineno, + ASTReverter::toShortString($value) + ); + } + $value_types_builder->addUnionType($element_value_type); + $record_real_union_type($element_value_type); + } + } else { + $new_type = Type::fromObject($value); + $value_types_builder->addType($new_type); + if ($real_value_types_builder) { + $real_value_types_builder->addType($new_type); + } + } + } + // TODO: Normalize value_types, e.g. false+true=bool, array+array=array + + $key_type_enum = GenericArrayType::getKeyTypeOfArrayNode($this->code_base, $this->context, $node, $this->should_catch_issue_exception); + $result = $value_types_builder->getPHPDocUnionType(); + if ($has_key) { + $result = $result->asNonEmptyAssociativeArrayTypes($key_type_enum); + } else { + $result = $result->asNonEmptyListTypes(); + } + $result = $result->withRealTypeSet($this->arrayTypeFromRealTypeBuilder($real_value_types_builder, $node, $has_key)); + if ($is_definitely_non_empty) { + return $result->nonFalseyClone(); + } + return $result; + } + + // TODO: Also return types such as array? + // TODO: Fix or suppress false positives PhanTypeArraySuspicious caused by loops... + return ArrayShapeType::empty(false)->asRealUnionType(); + } + + /** + * @return list + */ + private function arrayTypeFromRealTypeBuilder(?UnionTypeBuilder $builder, Node $node, bool $has_key): array + { + // Here, we only check for the real type being an integer. + // Unknown strings such as '0' will cast to integers when used as array keys, + // and if we knew all of the array keys were literals we would have generated an array shape instead. + $has_int_keys = true; + if ($has_key) { + foreach ($node->children as $child_node) { + $key = $child_node->children['key'] ?? null; + if (!isset($key)) { + // unpacking an array with string keys is a runtime error so these must be ints. + continue; + } + $key_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $key); + if (!$key_type->getRealUnionType()->isIntTypeOrNull()) { + $has_int_keys = false; + break; + } + } + } + if (!$builder || $builder->isEmpty()) { + if (!$has_key) { + return UnionType::typeSetFromString('list'); + } + return UnionType::typeSetFromString($has_int_keys ? 'array' : 'array'); + } + $real_types = []; + foreach ($builder->getTypeSet() as $type) { + if ($has_key) { + // TODO: Could be more precise if all keys are known to be non-numeric strings or integers + $real_types[] = GenericArrayType::fromElementType( + $type, + false, + $has_int_keys ? GenericArrayType::KEY_INT : GenericArrayType::KEY_MIXED + ); + } else { + $real_types[] = ListType::fromElementType($type, false, GenericArrayType::KEY_MIXED); + } + } + return $real_types; + } + + /** + * Visit a node with kind `\ast\AST_YIELD` + * + * @param Node $node @unused-param + * A yield node. Does not affect the union type + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitYield(Node $node): UnionType + { + $context = $this->context; + if (!$context->isInFunctionLikeScope()) { + return UnionType::empty(); + } + + // Get the method/function/closure we're in + $method = $context->getFunctionLikeInScope($this->code_base); + $method_generator_type = $method->getReturnTypeAsGeneratorTemplateType(); + $type_list = $method_generator_type->getTemplateParameterTypeList(); + if (\count($type_list) < 3 || \count($type_list) > 4) { + return UnionType::empty(); + } + // Return TSend of Generator + return $type_list[2]; + } + + /** + * @return ?array + * Caller should check if the result size is too small and handle it (for duplicate keys) + * Returns null if one or more keys could not be resolved + * + * @see ContextNode::getEquivalentPHPArrayElements() + */ + private function getEquivalentArraySet(Node $node): ?array + { + $elements = []; + $context_node = null; + foreach ($node->children as $child_node) { + if (!($child_node instanceof Node)) { + ContextNode::warnAboutEmptyArrayElements($this->code_base, $this->context, $node); + continue; + } + if ($child_node->kind === ast\AST_UNPACK) { + if ($this->getPackedArrayFieldTypes($child_node->children['expr']) !== null) { + // This is a placeholder of a deliberately - the caller checks that the count of elements matches the count of AST child nodes. + // TODO: Refactor to handle edge cases such as `[...[1], 0 => 2]` + $elements[] = $child_node; + continue; + } + return null; + } + + $key_node = $child_node->children['key']; + // NOTE: this has some overlap with DuplicateKeyPlugin + if ($key_node === null) { + $elements[] = $child_node; + } elseif (is_scalar($key_node)) { + $elements[$key_node] = $child_node; // Check for float? + } else { + if ($context_node === null) { + $context_node = new ContextNode($this->code_base, $this->context, null); + } + $key = $context_node->getEquivalentPHPValueForNode($key_node, ContextNode::RESOLVE_CONSTANTS); + if (is_scalar($key)) { + $elements[$key] = $child_node; + } else { + return null; + } + } + } + return $elements; + } + + /** + * @param Node|mixed $expr + * @return ?list the type of $x in ...$x, provided that it's a packed array (with keys 0, 1, ...) + */ + private function getPackedArrayFieldTypes($expr): ?array + { + if (!$expr instanceof Node) { + // TODO: Warn if non-array + return null; + } + // e.g. `[$x, ...$array]` in PHP 7.4 + // TODO: Support array expressions when their value is constant + $union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr); + // TODO: Warn if non-array + + if ($union_type->typeCount() === 1 && $union_type->hasTopLevelArrayShapeTypeInstances() && !$union_type->hasTopLevelNonArrayShapeTypeInstances()) { + $type_set = $union_type->getTypeSet(); + $type = \reset($type_set); + // TODO: Warn if the keys aren't consecutive 0-based integers + $expected = 0; + if (!$type instanceof ArrayShapeType) { + return null; + } + $field_types = $type->getFieldTypes(); + if ($expr->kind !== ast\AST_ARRAY && \count($field_types) >= self::ARRAY_UNPACK_COUNT_THRESHOLD) { + return null; + } + foreach ($field_types as $i => $type) { + if ($i !== $expected || $type->isPossiblyUndefined()) { + return null; + } + $expected++; + } + return $field_types; + } + return null; + } + + /** + * @param array $key_set + */ + private function createArrayShapeType(array $key_set): ArrayShapeType + { + $field_types = []; + + foreach ($key_set as $key => $child) { + // Keep iteration over $children and key_set in sync + if ($child->kind === ast\AST_UNPACK) { + // handle [other_expr, ...expr, other_exprs] + $element_value_type = $this->getPackedArrayFieldTypes($child->children['expr']); + if (!\is_array($element_value_type)) { + // impossible + continue; + } + foreach ($element_value_type as $type) { + $field_types[] = $type; + } + continue; + } + $value = $child->children['value']; + + if ($value instanceof Node) { + $element_value_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $value, + $this->should_catch_issue_exception + ); + if ($element_value_type->isEmpty()) { + $element_value_type = MixedType::instance(false)->asPHPDocUnionType(); + } else { + $element_value_type = $element_value_type->convertUndefinedToNullable(); + } + } else { + $element_value_type = Type::fromObject($value)->asRealUnionType(); + } + if ($child->children['key'] === null) { + $field_types[] = $element_value_type; + } else { + $field_types[$key] = $element_value_type; + } + } + return ArrayShapeType::fromFieldTypes($field_types, false); + } + + /** + * Visit a node with kind `\ast\AST_BINARY_OP` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitBinaryOp(Node $node): UnionType + { + return (new BinaryOperatorFlagVisitor( + $this->code_base, + $this->context, + $this->should_catch_issue_exception + ))->__invoke($node); + } + + /** + * Visit a node with kind `\ast\AST_ASSIGN_OP` (E.g. $x .= 'suffix') + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitAssignOp(Node $node): UnionType + { + return (new AssignOperatorFlagVisitor( + $this->code_base, + $this->context + ))->__invoke($node); + } + + /** + * Visit a node with kind `\ast\AST_CAST` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws NodeException if the flags are a value we aren't expecting + */ + public function visitCast(Node $node): UnionType + { + // This calls unionTypeFromNode to trigger any warnings + // TODO: Check if the cast would throw an error at runtime, based on the type (e.g. casting object to string/int) + + // RedundantConditionCallPlugin contains unrelated checks of whether this is redundant. + $expr = $node->children['expr']; + $expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr); + if ($expr_type->isVoidType()) { + $this->emitIssue( + Issue::TypeVoidExpression, + $expr->lineno ?? $node->lineno, + ASTReverter::toShortString($expr) + ); + } + switch ($node->flags) { + case \ast\flags\TYPE_NULL: + return NullType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_BOOL: + return $expr_type->applyBoolCast(); + // TODO: Warn about invalid casts (#2806) + case \ast\flags\TYPE_LONG: + return IntType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_DOUBLE: + return FloatType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_STRING: + return StringType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_ARRAY: + return ArrayType::instance(false)->asRealUnionType(); + case \ast\flags\TYPE_OBJECT: + return $this->typeAfterCastToObject($expr_type); + default: + throw new NodeException( + $node, + 'Unknown type (' . $node->flags . ') in cast' + ); + } + } + + /** + * @suppress PhanThrowTypeAbsentForCall + */ + private static function typeAfterCastToObject(UnionType $expr_type): UnionType + { + static $stdclass; + if ($stdclass === null) { + $stdclass = Type::fromFullyQualifiedString('\stdClass'); + } + $has_array = $expr_type->hasArray(); + if ($has_array) { + if ($expr_type->isExclusivelyArray()) { + return $stdclass->asRealUnionType(); + } + } + $expr_type = $expr_type->objectTypes(); + if ($expr_type->isEmpty()) { + return ObjectType::instance(false)->asRealUnionType(); + } + $expr_type = $expr_type->nonNullableClone(); + if ($has_array) { + $expr_type = $expr_type->withType($stdclass); + if ($expr_type->hasRealTypeSet()) { + return $expr_type->withRealTypeSet(\array_merge($expr_type->getRealTypeSet(), [$stdclass])); + } else { + return $expr_type->withRealType(ObjectType::instance(false)); + } + } + if (!$expr_type->hasRealTypeSet()) { + return $expr_type->withRealType(ObjectType::instance(false)); + } + return $expr_type; + } + + /** + * Visit a node with kind `\ast\AST_NEW` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitNew(Node $node): UnionType + { + static $object_type; + if ($object_type === null) { + $object_type = ObjectType::instance(false); + } + $class_node = $node->children['class']; + if (!($class_node instanceof Node)) { + $this->emitIssue( + Issue::InvalidNode, + $node->lineno, + "Invalid ClassName for new ClassName()" + ); + return $object_type->asRealUnionType(); + } + $union_type = $this->visitClassNameNode($class_node); + if ($union_type->isEmpty()) { + return $object_type->asRealUnionType(); + } + + // TODO: re-use the underlying type set in the common case + // Maybe UnionType::fromMap + + // For any types that are templates, map them to concrete + // types based on the parameters passed in. + $type_set = \array_map(function (Type $type) use ($node): Type { + + // Get a fully qualified name for the type + // TODO: Add a test of `new $closure()` warning. + $fqsen = FullyQualifiedClassName::fromType($type); + + // If we don't have the class, we'll catch that problem + // elsewhere + if (!$this->code_base->hasClassWithFQSEN($fqsen)) { + return $type; + } + + $class = $this->code_base->getClassByFQSEN($fqsen); + + // If this class doesn't have any generics on it, we're + // fine as we are with this Type + if (!$class->isGeneric()) { + return $type; + } + + // Now things are interesting. We need to map the + // arguments to the generic types and return a special + // kind of type. + + // Map each argument to its type + /** @param Node|string|int|float $arg_node */ + $arg_type_list = \array_map(function ($arg_node): UnionType { + return UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $arg_node + ); + }, $node->children['args']->children); + + // Get closures to extract template types based on the types of the constructor + // so that we can figure out what template types we're going to be mapping + $template_type_resolvers = $class->getGenericConstructorBuilder($this->code_base); + + // And use those closures to infer the (possibly transformed) types + $template_type_list = []; + foreach ($template_type_resolvers as $template_type_resolver) { + $template_type_list[] = $template_type_resolver($arg_type_list, $this->context); + } + + // Create a new type that assigns concrete + // types to template type identifiers. + return Type::fromType($type, $template_type_list); + }, $union_type->getTypeSet()); + + if (!$type_set) { + return $object_type->asRealUnionType(); + } + + if ($class_node->kind === ast\AST_NAME) { + $real_type_set = $type_set; + } else { + $real_type_set = [$object_type]; + } + + return UnionType::of($type_set, $real_type_set); + } + + /** + * Visit a node with kind `\ast\AST_INSTANCEOF` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitInstanceOf(Node $node): UnionType + { + $code_base = $this->code_base; + $context = $this->context; + // Check to make sure the left side is valid + UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node->children['expr']); + // Get the type that we're checking it against, check if it is valid. + $class_node = $node->children['class']; + if (!($class_node instanceof Node)) { + return BoolType::instance(false)->asRealUnionType(); + } + $type = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $class_node + ); + // TODO: Unify UnionTypeVisitor, AssignmentVisitor, and PostOrderAnalysisVisitor + if (!$type->isEmpty() && $type->objectTypesWithKnownFQSENs()->isEmpty()) { + if ($class_node->kind === \ast\AST_NAME || !$type->hasStringType()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeInvalidInstanceof, + $context->getLineNumberStart(), + ASTReverter::toShortString($class_node), + (string)$type + ); + } + } + + return BoolType::instance(false)->asRealUnionType(); + } + + /** @internal - Duplicated for performance. Use the constant from PhanAnnotationAdder instead */ + private const FLAG_IGNORE_NULLABLE = 1 << 29; + + /** + * Visit a node with kind `\ast\AST_DIM` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws IssueException + * if the dimension access is invalid + */ + public function visitDim(Node $node, bool $treat_undef_as_nullable = false): UnionType + { + $union_type = self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'], + $this->should_catch_issue_exception + )->withStaticResolvedInContext($this->context); + + if ($union_type->isEmpty()) { + return UnionType::empty(); + } + + // If none of the types we found were arrays with elements, + // then check for ArrayAccess + static $array_access_type; + static $simple_xml_element_type; // SimpleXMLElement doesn't `implement` ArrayAccess, but can be accessed that way. See #542 + static $null_type; + static $string_type; + static $string_union_type; + static $int_union_type; + static $int_or_string_union_type; + + if ($array_access_type === null) { + // array offsets work on strings, unfortunately + // Double check that any classes in the type don't + // have ArrayAccess + $array_access_type = + Type::fromNamespaceAndName('\\', 'ArrayAccess', false); + $simple_xml_element_type = + Type::fromNamespaceAndName('\\', 'SimpleXMLElement', false); + $null_type = NullType::instance(false); + $string_type = StringType::instance(false); + $string_union_type = $string_type->asPHPDocUnionType(); + $int_union_type = IntType::instance(false)->asPHPDocUnionType(); + $int_or_string_union_type = UnionType::fromFullyQualifiedPHPDocString('int|string'); + } + + if (self::hasArrayShapeOrList($union_type)) { + $element_type = $this->resolveArrayShapeElementTypes($node, $union_type); + if ($element_type !== null) { + if ($element_type->isPossiblyUndefined() && !($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF)) { + $this->emitIssue( + Issue::TypePossiblyInvalidDimOffset, + $node->lineno, + ASTReverter::toShortString($node->children['dim']), + ASTReverter::toShortString($node->children['expr']), + $union_type + ); + if ($treat_undef_as_nullable || Config::getValue('convert_possibly_undefined_offset_to_nullable')) { + return $element_type->nullableClone()->withIsPossiblyUndefined(false); + } + return $element_type->withNullableRealTypes()->withIsPossiblyUndefined(false); + } + // echo "Returning $element_type for {$union_type->getDebugRepresentation()}\n"; + return $element_type; + } + } + + $dim_type = self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['dim'], + true + ); + + // Figure out what the types of accessed array + // elements would be. + $generic_types = $union_type->genericArrayElementTypes(true); + + // If we have generics, we're all set + if (!$generic_types->isEmpty()) { + $generic_types = $generic_types->asNormalizedTypes(); + if (!($node->flags & self::FLAG_IGNORE_NULLABLE) && $union_type->containsNonMixedNullable()) { + $this->emitIssue( + Issue::TypeArraySuspiciousNullable, + $node->lineno, + ASTReverter::toShortString($node->children['expr']), + (string)$union_type + ); + } + + if (!$dim_type->isEmpty()) { + try { + $should_check = !$union_type->hasMixedType() && !$union_type->asExpandedTypes($this->code_base)->hasArrayAccess(); + } catch (RecursionDepthException $_) { + $should_check = false; + } + if ($should_check) { + if (Config::getValue('scalar_array_key_cast')) { + $expected_key_type = $int_or_string_union_type; + } else { + $expected_key_type = GenericArrayType::unionTypeForKeyType( + GenericArrayType::keyTypeFromUnionTypeKeys($union_type), + GenericArrayType::CONVERT_KEY_MIXED_TO_INT_OR_STRING_UNION_TYPE + ); + } + + if (!$dim_type->canCastToUnionType($expected_key_type)) { + $issue_type = Issue::TypeMismatchDimFetch; + + if ($dim_type->containsNullable() && $dim_type->nonNullableClone()->canCastToUnionType($expected_key_type)) { + $issue_type = Issue::TypeMismatchDimFetchNullable; + } + + if ($this->should_catch_issue_exception) { + $this->emitIssue( + $issue_type, + $node->lineno, + (string)$union_type, + (string)$dim_type, + (string)$expected_key_type + ); + return $generic_types; + } + + throw new IssueException( + Issue::fromType($issue_type)( + $this->context->getFile(), + $node->lineno, + [(string)$union_type, (string)$dim_type, (string)$expected_key_type] + ) + ); + } + } + } + return $generic_types; + } + + // If the only type is null, we don't know what + // accessed items will be + if ($union_type->isType($null_type)) { + if (!($node->flags & self::FLAG_IGNORE_NULLABLE)) { + $this->emitIssue( + Issue::TypeArraySuspiciousNull, + $node->lineno, + ASTReverter::toShortString($node->children['expr']) + ); + } + if ($union_type->getRealUnionType()->isNull()) { + return NullType::instance(false)->asRealUnionType(); + } + return NullType::instance(false)->asPHPDocUnionType(); + } + + $element_types = UnionType::empty(); + + // You can access string characters via array index, + // so we'll add the string type to the result if we're + // indexing something that could be a string + if ($union_type->isNonNullStringType() + || ($union_type->canCastToUnionType($string_union_type) && !$union_type->hasMixedType()) + ) { + if (Config::get_closest_minimum_target_php_version_id() < 70100 && $union_type->isNonNullStringType()) { + $this->analyzeNegativeStringOffsetCompatibility($node, $dim_type); + } + $this->checkIsValidStringOffset($union_type, $node, $dim_type); + + if (!$dim_type->isEmpty() && !$dim_type->canCastToUnionType($int_union_type)) { + // TODO: Efficient implementation of asExpandedTypes()->hasArrayAccess()? + if (!$union_type->isEmpty() && !$union_type->asExpandedTypes($this->code_base)->hasArrayLike()) { + $this->emitIssue( + Issue::TypeMismatchDimFetch, + $node->lineno, + $union_type, + (string)$dim_type, + $int_union_type + ); + } + } + $element_types = $element_types->withType($string_type); + if ($union_type->hasRealTypeSet()) { + // @phan-suppress-next-line PhanAccessMethodInternal + $element_types = $element_types->withRealTypeSet(UnionType::computeRealElementTypesForDimAccess($union_type->getRealTypeSet())); + } + } + + if ($element_types->isEmpty()) { + // Hunt for any types that are viable class names and + // see if they inherit from ArrayAccess + try { + foreach ($union_type->asClassList($this->code_base, $this->context) as $class) { + $expanded_types = $class->getUnionType()->asExpandedTypes($this->code_base); + if ($expanded_types->hasType($array_access_type) || + $expanded_types->hasType($simple_xml_element_type) + ) { + return $element_types; + } + } + } catch (CodeBaseException | RecursionDepthException $_) { + // ignore + } + + if (!$union_type->hasArrayLike() && !$union_type->hasMixedType()) { + $this->emitIssue( + Issue::TypeArraySuspicious, + $node->lineno, + ASTReverter::toShortString($node->children['expr']), + (string)$union_type + ); + return $element_types; + } + if (!($node->flags & self::FLAG_IGNORE_NULLABLE) && $union_type->containsNullable()) { + $this->emitIssue( + Issue::TypeArraySuspiciousNullable, + $node->lineno, + ASTReverter::toShortString($node->children['expr']), + (string)$union_type + ); + } + } + + return $element_types; + } + + /** + * Check for invalid string offsets, e.g. `'val'[3]`, `''[$i]`, etc. + * + * @param UnionType $union_type the union type of the expression + * @param UnionType $dim_type the union type of the dimension being accessed on the expression + */ + private function checkIsValidStringOffset(UnionType $union_type, Node $node, UnionType $dim_type): void + { + $max_len = -1; + foreach ($union_type->getRealTypeSet() as $type) { + if ($type instanceof StringType) { + if ($type instanceof LiteralStringType) { + $max_len = \max($max_len, \strlen($type->getValue())); + continue; + } + return; + } elseif ($type instanceof IterableType) { + return; + } + } + if ($max_len < 0) { + return; + } + if ($max_len > 0) { + $dim_value = $dim_type->asSingleScalarValueOrNullOrSelf(); + if (\is_object($dim_value)) { + return; + } + $dim_value_as_int = (int)$dim_value; + if ($dim_value_as_int < 0) { + // Convert -1 to 0, etc. + $dim_value_as_int = ~$dim_value_as_int; + } + if ($dim_value_as_int < $max_len) { + return; + } + } + $exception = new IssueException( + Issue::fromType(Issue::TypeInvalidDimOffset)( + $this->context->getFile(), + $node->children['dim']->lineno ?? $node->lineno, + [ + $dim_type, + ASTReverter::toShortString($node->children['expr']), + (string)$union_type + ] + ) + ); + if ($this->should_catch_issue_exception) { + Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); + return; + } else { + throw $exception; + } + } + + private static function hasArrayShapeOrList(UnionType $union_type): bool + { + foreach ($union_type->getTypeSet() as $type) { + if ($type instanceof ArrayShapeType || $type instanceof ListType) { + return true; + } + } + return false; + } + + /** + * Return the union type that's the result of accessing the node's dimension on the node's expression $union_type. + * + * Precondition: $union_type has array shape types or list types. + */ + private function resolveArrayShapeElementTypes(Node $node, UnionType $union_type): ?UnionType + { + $dim_node = $node->children['dim']; + $dim_value = $dim_node instanceof Node ? (new ContextNode($this->code_base, $this->context, $dim_node))->getEquivalentPHPScalarValue() : $dim_node; + // TODO: detect and warn about null + $has_non_empty_array = false; + $check_invalid_dim = !($node->flags & self::FLAG_IGNORE_NULLABLE); + if ($check_invalid_dim && $union_type->hasRealTypeSet()) { + foreach ($union_type->getRealTypeSet() as $type) { + if (!$type->isPossiblyTruthy()) { + if ($type instanceof LiteralStringType && \strlen($type->getValue()) > 0) { + $has_non_empty_array = true; + break; + } + continue; + } + if ($type instanceof IterableType || $type instanceof MixedType) { + $has_non_empty_array = true; + break; + } + } + if (!$has_non_empty_array) { + $exception = new IssueException( + Issue::fromType(Issue::TypeInvalidDimOffset)( + $this->context->getFile(), + $dim_node->lineno ?? $node->lineno, + [ + is_scalar($dim_value) ? StringUtil::jsonEncode($dim_value) : ASTReverter::toShortString($dim_value), + ASTReverter::toShortString($node->children['expr']), + (string)$union_type + ] + ) + ); + if ($this->should_catch_issue_exception) { + Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); + return null; + } else { + throw $exception; + } + } + } + if (!is_scalar($dim_value)) { + return null; + } + + $resulting_element_type = self::resolveArrayShapeElementTypesForOffset($union_type, $dim_value); + + if ($resulting_element_type === null) { + return null; + } + if ($resulting_element_type === false) { + // XXX not sure what to do here. For now, just return null and only warn in cases where requested to. + if ($check_invalid_dim) { + $exception = new IssueException( + Issue::fromType(Issue::TypeInvalidDimOffset)( + $this->context->getFile(), + $dim_node->lineno ?? $node->lineno, + [StringUtil::jsonEncode($dim_value), ASTReverter::toShortString($node->children['expr']), (string)$union_type] + ) + ); + if ($this->should_catch_issue_exception) { + Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); + } else { + throw $exception; + } + } + // $union_type is exclusively array shape types, but those don't contain the field $dim_value. + // It's undefined (which becomes null) + if (self::couldRealTypesHaveKey($union_type->getRealTypeSet(), $dim_value)) { + return NullType::instance(false)->asPHPDocUnionType(); + } + return NullType::instance(false)->asRealUnionType(); + } + return $resulting_element_type; + } + + /** + * @param list $real_type_set + * @param int|string|float $dim_value + */ + private static function couldRealTypesHaveKey(array $real_type_set, $dim_value): bool + { + foreach ($real_type_set as $type) { + if ($type instanceof ArrayShapeType) { + if (\array_key_exists($dim_value, $type->getFieldTypes())) { + return true; + } + } elseif ($type instanceof ListType) { + $filtered = \is_int($dim_value) ? $dim_value : \filter_var($dim_value, \FILTER_VALIDATE_INT); + if (\is_int($filtered) && $filtered >= 0) { + return true; + } + } else { + return true; + } + } + return \count($real_type_set) === 0; + } + + /** + * @param UnionType $union_type a union type with at least one top-level array shape type + * @param int|string|float|bool $dim_value a scalar dimension. TODO: Warn about null? + * @return ?UnionType|?false + * returns false if there the offset was invalid and there are no ways to get that offset + * returns null if the dim_value offset could not be found, but there were other generic array types + */ + public static function resolveArrayShapeElementTypesForOffset(UnionType $union_type, $dim_value, bool $is_computing_real_type_set = false) + { + /** + * @var bool $has_non_array_shape_type this will be true if there are types that support array access + * but have unknown array shapes in $union_type + */ + $has_generic_array = false; + $has_string = false; + $resulting_element_type = null; + foreach ($union_type->getTypeSet() as $type) { + if (!($type instanceof ArrayShapeType)) { + if ($type instanceof StringType) { + $has_string = true; + if (\is_int($dim_value) || \filter_var($dim_value, \FILTER_VALIDATE_INT) !== false) { + // If we request a string offset from a string, that's not valid. Only accept integer dimensions as valid. + // in php, indices of strings can be negative + if ($resulting_element_type instanceof UnionType) { + $resulting_element_type = $resulting_element_type->withType(StringType::instance(false)); + } else { + $resulting_element_type = StringType::instance(false)->asPHPDocUnionType(); + } + } else { + // TODO: Warn about string indices of strings? + } + } elseif ($type->isArrayLike() || $type->isObject() || $type instanceof MixedType) { + if ($type instanceof ListType && (!\is_numeric($dim_value) || $dim_value < 0)) { + continue; + } + if ($is_computing_real_type_set) { + // Avoid false positives for real type checking. + // TODO: Improve handling for GenericArrayType, strings, etc. + return null; + } + // TODO: Could be more precise about check for ArrayAccess + $has_generic_array = true; + continue; + } + continue; + } + $element_type = $type->getFieldTypes()[$dim_value] ?? null; + if ($element_type !== null) { + // $element_type may be non-null but $element_type->isEmpty() may be true. + // So, we use null to indicate failure below + if ($resulting_element_type instanceof UnionType) { + $resulting_element_type = $resulting_element_type->withUnionType($element_type); + } else { + $resulting_element_type = $element_type; + } + } + } + if ($resulting_element_type === null) { + if (!$has_string && !$has_generic_array) { + // This is exclusively array shape types. + // Return false to indicate that the offset doesn't exist in any of those array shape types. + return false; + } + return null; + } + if ($has_string || $has_generic_array) { + if ($has_string && $has_generic_array) { + return null; + } + if ($resulting_element_type->hasRealTypeSet()) { + $resulting_element_type = UnionType::of( + $resulting_element_type->getTypeSet(), + \array_map(static function (Type $type): Type { + return $type->withIsNullable(true); + }, $resulting_element_type->getRealTypeSet()) + ); + } + } + if (!$resulting_element_type->containsNullableOrUndefined() && $union_type->containsNullableOrUndefined()) { + // Here, this uses Foo|null instead of ?Foo to only warn when strict types are used. + $resulting_element_type = $resulting_element_type->withType(NullType::instance(false)); + } + if (!$is_computing_real_type_set) { + $resulting_real_element_type = self::resolveArrayShapeElementTypesForOffset($union_type->getRealUnionType(), $dim_value, true); + return $resulting_element_type->withRealTypeSet( + \is_object($resulting_real_element_type) ? $resulting_real_element_type->getRealTypeSet() : [] + ); + } + return $resulting_element_type; + } + + /** + * Visit a node with kind `\ast\AST_UNPACK` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws IssueException + * if the unpack is on an invalid expression + * @suppress PhanUndeclaredProperty + */ + public function visitUnpack(Node $node): UnionType + { + return $this->analyzeUnpack($node, isset($node->is_in_array)); + } + + /** + * Visit a node with kind `\ast\AST_UNPACK` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @param bool $is_array_spread + * If true, this is the array spread operator, + * which tolerates integers that aren't consecutive. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws IssueException + * if the unpack is on an invalid expression + */ + private function analyzeUnpack(Node $node, bool $is_array_spread): UnionType + { + $union_type = self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'], + $this->should_catch_issue_exception + )->withStaticResolvedInContext($this->context); + + if ($union_type->isEmpty()) { + return $union_type; + } + + // Figure out what the types of accessed array + // elements would be + // TODO: Account for Traversable once there are generics for Traversable + // TODO: Warn about possibly invalid unpack (e.g. nullable) + $generic_types = $union_type->iterableValueUnionType($this->code_base); + + // If we have generics, we're all set + try { + if ($generic_types->isEmpty()) { + if (!$union_type->asExpandedTypes($this->code_base)->hasIterable() && !$union_type->hasTypeMatchingCallback(static function (Type $type): bool { + return !$type->isNullableLabeled() && $type instanceof MixedType; + })) { + throw new IssueException( + Issue::fromType(Issue::TypeMismatchUnpackValue)( + $this->context->getFile(), + $node->lineno, + [(string)$union_type] + ) + ); + } + return $generic_types; + } + $this->checkInvalidUnpackKeyType($node, $union_type, $is_array_spread); + } catch (IssueException $exception) { + Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); + } + return $generic_types; + } + + private function checkInvalidUnpackKeyType(Node $node, UnionType $union_type, bool $is_array_spread): void + { + $is_invalid_because_associative = false; + if (!$is_array_spread) { + foreach ($union_type->getTypeSet() as $type) { + if ($type->isIterable()) { + if ($type instanceof AssociativeArrayType) { + $is_invalid_because_associative = true; + } else { + $is_invalid_because_associative = false; + break; + } + } + } + } + $key_type = $union_type->iterableKeyUnionType($this->code_base); + // Check that this is possibly valid, e.g. array, Generator, or iterable + // TODO: Warn if key_type contains nullable types (excluding VoidType) + // TODO: Warn about union types that are partially invalid. + if ($is_invalid_because_associative || !$key_type->isEmpty() && !$key_type->hasTypeMatchingCallback(static function (Type $type): bool { + return $type instanceof IntType || $type instanceof MixedType; + }) + ) { + throw new IssueException( + Issue::fromType($is_array_spread ? Issue::TypeMismatchUnpackKeyArraySpread : Issue::TypeMismatchUnpackKey)( + $this->context->getFile(), + $node->lineno, + [(string)$union_type, $key_type] + ) + ); + } + } + + /** + * Visit a node with kind `\ast\AST_CLOSURE` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitClosure(Node $node): UnionType + { + // The type of a closure is the fqsen pointing + // at its definition + $closure_fqsen = + FullyQualifiedFunctionName::fromClosureInContext( + $this->context, + $node + ); + + if ($this->code_base->hasFunctionWithFQSEN($closure_fqsen)) { + $func = $this->code_base->getFunctionByFQSEN($closure_fqsen); + } else { + $func = null; + } + + return ClosureType::instanceWithClosureFQSEN( + $closure_fqsen, + $func + )->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_ARROW_FUNC` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitArrowFunc(Node $node): UnionType + { + return $this->visitClosure($node); + } + + /** + * Visit a node with kind `\ast\AST_VAR` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws IssueException + * if variable is undefined and being fetched + */ + public function visitVar(Node $node): UnionType + { + // $$var or ${...} (whose idea was that anyway?) + $name_node = $node->children['name']; + if (($name_node instanceof Node)) { + // This is nonsense. Give up. + $name_node_type = $this->__invoke($name_node); + static $int_or_string_type; + if ($int_or_string_type === null) { + $int_or_string_type = UnionType::fromFullyQualifiedPHPDocString('int|string|null'); + } + if (!$name_node_type->canCastToUnionType($int_or_string_type)) { + Issue::maybeEmit($this->code_base, $this->context, Issue::TypeSuspiciousIndirectVariable, $name_node->lineno, (string)$name_node_type); + return MixedType::instance(false)->asPHPDocUnionType(); + } + $name_node = $name_node_type->asSingleScalarValueOrNull(); + if ($name_node === null) { + return MixedType::instance(false)->asPHPDocUnionType(); + } + // fall through + } + + // foo(${42}) is technically valid PHP code, avoid TypeError + $variable_name = + (string)$name_node; + + if ($this->context->getScope()->hasVariableWithName($variable_name)) { + $variable = $this->context->getScope()->getVariableByName( + $variable_name + ); + $union_type = $variable->getUnionType(); + if ($union_type->isPossiblyUndefined()) { + if ($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF) { + if ($this->context->isInGlobalScope()) { + $union_type = $union_type->eraseRealTypeSet(); + } + return $union_type->convertUndefinedToNullable(); + } + if ($this->context->isInGlobalScope()) { + $union_type = $union_type->eraseRealTypeSet(); + if ($this->should_catch_issue_exception) { + if (!Config::getValue('ignore_undeclared_variables_in_global_scope')) { + $this->emitIssue( + Issue::PossiblyUndeclaredGlobalVariable, + $node->lineno, + $variable_name + ); + } + } + } else { + if ($this->should_catch_issue_exception) { + $this->emitIssue( + Issue::PossiblyUndeclaredVariable, + $node->lineno, + $variable_name + ); + } + } + } + + return $union_type; + } + if (Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())) { + // @phan-suppress-next-line PhanTypeMismatchReturnNullable variable existence was checked + return Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name); + } + if ($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF) { + if (!$this->context->isInGlobalScope()) { + if ($this->should_catch_issue_exception && !(($node->flags & PhanAnnotationAdder::FLAG_INITIALIZES) && $this->context->isInLoop())) { + // Warn about `$var ??= expr;`, except when it's done in a loop. + $this->emitIssueWithSuggestion( + Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name), + $node->lineno, + [$variable_name], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name) + ); + } + if ($variable_name === 'this') { + return ObjectType::instance(false)->asRealUnionType(); + } + return NullType::instance(false)->asRealUnionType(); + } + if ($variable_name === 'this') { + return ObjectType::instance(false)->asRealUnionType(); + } + return NullType::instance(false)->asPHPDocUnionType(); + } + + if (!($this->context->isInGlobalScope() && Config::getValue('ignore_undeclared_variables_in_global_scope'))) { + if (!$this->should_catch_issue_exception) { + throw new IssueException( + Issue::fromType(Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name))( + $this->context->getFile(), + $node->lineno, + [$variable_name], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name) + ) + ); + } + Issue::maybeEmitWithParameters( + $this->code_base, + $this->context, + Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name), + $node->lineno, + [$variable_name], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name) + ); + } + if ($variable_name === 'this') { + return ObjectType::instance(false)->asRealUnionType(); + } + + if (!$this->context->isInGlobalScope()) { + if (!$this->context->isInLoop()) { + return NullType::instance(false)->asRealUnionType()->withIsDefinitelyUndefined(); + } + return NullType::instance(false)->asRealUnionType(); + } + + return UnionType::empty(); + } + + /** + * Visit a node with kind `\ast\AST_ENCAPS_LIST` + * + * @param Node $node (@phan-unused-param) + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitEncapsList(Node $node): UnionType + { + $result = ''; + foreach ($node->children as $part) { + $part_string = $part instanceof Node ? UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $part + )->asSingleScalarValueOrNullOrSelf() : $part; + if (\is_object($part_string)) { + return StringType::instance(false)->asRealUnionType(); + } + $result .= $part_string; + } + return LiteralStringType::instanceForValue($result, false)->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_CONST` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitConst(Node $node): UnionType + { + // Figure out the name of the constant if it's + // a string. + $constant_name = $node->children['name']->children['name'] ?? ''; + + // If the constant is referring to the current + // class, return that as a type + if (Type::isSelfTypeString($constant_name) || Type::isStaticTypeString($constant_name)) { + return Type::fromStringInContext($constant_name, $this->context, Type::FROM_NODE)->asRealUnionType(); + } + + try { + $constant = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getConst(); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + return UnionType::empty(); + } + + return $constant->getUnionType(); + } + + /** + * @throws UnanalyzableException + */ + public function visitClass(Node $node): UnionType + { + if ($node->flags & ast\flags\CLASS_ANONYMOUS) { + $class_name = + (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getUnqualifiedNameForAnonymousClass(); + } else { + $class_name = (string)$node->children['name']; + } + + if ($class_name === '') { + // Should only occur with --use-fallback-parser + throw new UnanalyzableException($node, "Class name cannot be empty"); + } + + // @phan-suppress-next-line PhanThrowTypeMismatchForCall + return FullyQualifiedClassName::fromStringInContext( + $class_name, + $this->context + )->asType()->asRealUnionType(); + } + + /** + * Visit a node with kind `\ast\AST_CLASS_CONST` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws IssueException + * An exception is thrown if we can't find the constant + */ + public function visitClassConst(Node $node): UnionType + { + try { + $constant = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getClassConst(); + $union_type = $constant->getUnionType(); + $class_node = $node->children['class']; + if (!$class_node instanceof Node || $class_node->kind !== ast\AST_NAME) { + // ignore nonsense like (0)::class, and dynamic accesses such as $var::CLASS + return $union_type->eraseRealTypeSet(); + } + if (\strcasecmp($class_node->children['name'], 'static') === 0) { + if ($this->context->isInClassScope() && $this->context->getClassInScope($this->code_base)->isFinal()) { + // static::X should be treated like self::X in a final class. + return $union_type; + } + return $union_type->eraseRealTypeSet(); + } + return $union_type; + } catch (NodeException $_) { + // ignore, this should warn elsewhere + } + + return UnionType::empty(); + } + + /** + * Visit a node with kind `\ast\AST_CLASS_NAME` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws IssueException + * An exception is thrown if we can't find the constant + */ + public function visitClassName(Node $node): UnionType + { + $class_node = $node->children['class']; + try { + $class_list = (new ContextNode( + $this->code_base, + $this->context, + $class_node + ))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME); + } catch (IssueException $exception) { + if ($this->should_catch_issue_exception) { + Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); + return ClassStringType::instance(false)->asRealUnionType(); + } + throw $exception; + } catch (CodeBaseException $exception) { + $exception_fqsen = $exception->getFQSEN(); + // We might still be in the parse phase. + // Throw the same IssueException that would be thrown in Phan 1 and let the caller decide how to handle this. + $new_exception = new IssueException( + Issue::fromType(Issue::UndeclaredClassReference)( + $this->context->getFile(), + $node->lineno, + [(string)$exception_fqsen], + IssueFixSuggester::suggestSimilarClassForGenericFQSEN($this->code_base, $this->context, $exception_fqsen) + ) + ); + if ($this->should_catch_issue_exception) { + Issue::maybeEmitInstance($this->code_base, $this->context, $new_exception->getIssueInstance()); + return LiteralStringType::instanceForValue( + \ltrim($exception_fqsen->__toString(), '\\'), + false + )->asRealUnionType(); + } + throw $new_exception; + } + if (!$class_list) { + return ClassStringType::instance(false)->asRealUnionType(); + } + // Return the first class FQSEN + $types = []; + $name = $class_node->children['name'] ?? null; + foreach ($class_list as $class) { + $types[] = LiteralStringType::instanceForValue( + \ltrim($class->getFQSEN()->__toString(), '\\'), + false + ); + } + if (\is_string($name) && \strcasecmp($name, 'static') === 0 && (!isset($class) || !$class->isFinal())) { + return UnionType::of($types, [ClassStringType::instance(false)]); + } + return UnionType::of($types, $types); + } + + /** + * Visit a node with kind `\ast\AST_NULLSAFE_PROP` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * @override + */ + public function visitNullsafeProp(Node $node): UnionType + { + $expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr'])->getRealUnionType(); + $result = $this->analyzeProp($node, false); + if ($expr_type->isEmpty()) { + return $result->nullableClone(); + } + if ($expr_type->isNull()) { + return NullType::instance(false)->asRealUnionType(); + } + if ($expr_type->containsNullableOrUndefined()) { + return $result->nullableClone(); + } + return $result; + } + + /** + * Visit a node with kind `\ast\AST_PROP` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitProp(Node $node): UnionType + { + return $this->analyzeProp($node, false); + } + + /** + * Analyzes a node with kind `\ast\AST_PROP` or `\ast\AST_STATIC_PROP` + * + * @param Node $node + * The instance/static property access node. + * + * @param bool $is_static + * True if this is a static property fetch, + * false if this is an instance property fetch. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + private function analyzeProp(Node $node, bool $is_static): UnionType + { + // Either expr(instance) or class(static) is set + $expr_node = $node->children['expr'] ?? null; + try { + $property = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getProperty($is_static); + + if ($property->isWriteOnly()) { + $this->emitIssue( + $property->isFromPHPDoc() ? Issue::AccessWriteOnlyMagicProperty : Issue::AccessWriteOnlyProperty, + $node->lineno, + $property->asPropertyFQSENString(), + $property->getContext()->getFile(), + $property->getContext()->getLineNumberStart() + ); + } + + if ($expr_node instanceof Node && + $expr_node->kind === ast\AST_VAR && + $expr_node->children['name'] === 'this' + ) { + $override_union_type = $this->context->getThisPropertyIfOverridden($property->getName()); + if ($override_union_type) { + $this->warnIfPossiblyUndefinedProperty($node, $property->getName(), $override_union_type); + // There was an earlier assignment in scope such as `$this->prop = 2;` + return $override_union_type; + } + } + + $union_type = $property->getUnionType()->withStaticResolvedInContext($property->getContext()); + + // Map template types to concrete types + if ($union_type->hasTemplateTypeRecursive()) { + // Get the type of the object calling the property + $expression_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $expr_node + ); + + $union_type = $union_type->withTemplateParameterTypeMap( + $expression_type->getTemplateParameterTypeMap($this->code_base) + ); + + return $union_type; + } elseif (!$is_static) { + // Inherit any new additional inferred union types from the declaring class, + // unless the property type has template types. + $defining_fqsen = $property->getDefiningFQSEN(); + if ($property->getFQSEN() !== $defining_fqsen) { + if ($this->code_base->hasPropertyWithFQSEN($defining_fqsen)) { + $declaring_union_type = $this->code_base->getPropertyByFQSEN($defining_fqsen)->getUnionType(); + if ($declaring_union_type !== $union_type && !$declaring_union_type->hasTemplateTypeRecursive()) { + $union_type = $union_type->withUnionType($declaring_union_type); + } + } + } + } + + if ($union_type->isEmptyArrayShape() && $property->getPHPDocUnionType()->isEmpty()) { + return UnionType::of( + [ArrayType::instance($union_type->containsNullable())], + $property->getRealUnionType()->getTypeSet() + ); + } + return $union_type; + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (CodeBaseException $exception) { + $exception_fqsen = $exception->getFQSEN(); + $suggestion = null; + $property_name = $node->children['prop']; + if ($exception_fqsen instanceof FullyQualifiedClassName && $this->code_base->hasClassWithFQSEN($exception_fqsen)) { + $suggestion_class = $this->code_base->getClassByFQSEN($exception_fqsen); + $suggestion = IssueFixSuggester::suggestSimilarProperty( + $this->code_base, + $this->context, + $suggestion_class, + $property_name, + false + ); + } + $this->emitIssueWithSuggestion( + Issue::UndeclaredProperty, + $node->lineno, + ["{$exception_fqsen}->{$property_name}"], + $suggestion + ); + } catch (UnanalyzableMagicPropertyException $exception) { + $class = $exception->getClass(); + return $class->getMethodByName($this->code_base, '__get')->getUnionType(); + } catch (NodeException $_) { + // Swallow it. There are some constructs that we + // just can't figure out. + } + $property_name = $property_name ?? $node->children['prop']; + if (\is_string($property_name) && $expr_node instanceof Node && + $expr_node->kind === ast\AST_VAR && + $expr_node->children['name'] === 'this' + ) { + $override_union_type = $this->context->getThisPropertyIfOverridden($property_name); + if ($override_union_type) { + $this->warnIfPossiblyUndefinedProperty($node, $property_name, $override_union_type); + // There was an earlier expression such as `$this->prop = 2;` + // fwrite(STDERR, "Saw override '$override_union_type' for $property\n"); + return $override_union_type; + } + } + + return UnionType::empty(); + } + + private function warnIfPossiblyUndefinedProperty(Node $node, string $prop_name, UnionType $union_type): void + { + if (!$union_type->isPossiblyUndefined()) { + return; + } + $this->emitIssue( + Issue::PossiblyUnsetPropertyOfThis, + $node->lineno, + '$this->' . $prop_name + ); + } + + /** + * Visit a node with kind `\ast\AST_STATIC_PROP` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitStaticProp(Node $node): UnionType + { + return $this->analyzeProp($node, true); + } + + + /** + * Visit a node with kind `\ast\AST_CALL` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws FQSENException if the fqsen for the called function is empty/invalid + */ + public function visitCall(Node $node): UnionType + { + $expression = $node->children['expr']; + $function_list_generator = (new ContextNode( + $this->code_base, + $this->context, + $expression + ))->getFunctionFromNode(true); + + $possible_types = null; + foreach ($function_list_generator as $function) { + $function->analyzeReturnTypes($this->code_base); // For daemon/server mode, call this to consistently ensure accurate return types. + + if ($function->hasDependentReturnType()) { + $function_types = $function->getDependentReturnType($this->code_base, $this->context, $node->children['args']->children); + } else { + $function_types = $function->getUnionType(); + } + if ($possible_types) { + '@phan-var UnionType $possible_types'; + $possible_types = $possible_types->withUnionType($function_types); + } else { + $possible_types = $function_types; + } + } + + return $possible_types ?? UnionType::empty(); + } + + /** + * Visit a node with kind `\ast\AST_STATIC_CALL` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitStaticCall(Node $node): UnionType + { + return $this->visitMethodCall($node); + } + + /** + * Visit a node with kind `\ast\AST_NULLSAFE_METHOD_CALL` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitNullsafeMethodCall(Node $node): UnionType + { + $expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr'])->getRealUnionType(); + $result = $this->visitMethodCall($node); + if ($result->isEmpty()) { + return $result->nullableClone(); + } + if ($expr_type->isNull()) { + return NullType::instance(false)->asRealUnionType(); + } + if ($expr_type->containsNullableOrUndefined()) { + return $result->nullableClone(); + } + return $result; + } + + /** + * Visit a node with kind `\ast\AST_METHOD_CALL` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitMethodCall(Node $node): UnionType + { + $method_name = $node->children['method'] ?? ''; + + // Give up on any complicated nonsense where the + // method name is a variable such as in + // `$variable->$function_name()`. + if ($method_name instanceof Node) { + $method_name = $this->__invoke($method_name)->asSingleScalarValueOrNullOrSelf(); + if (!is_string($method_name)) { + return UnionType::empty(); + } + } + + // Method names can some times turn up being + // other method calls. + if (!is_string($method_name)) { + $method_name = (string)$method_name; + } + + try { + $static_class_node = $node->children['class'] ?? null; + $class_node = $static_class_node ?? $node->children['expr']; + if (!($class_node instanceof Node)) { + // E.g. `'string_literal'->method()` + // Other places will also emit NonClassMethodCall for the same node + $this->emitIssue( + Issue::NonClassMethodCall, + $node->lineno, + $method_name, + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $class_node) + ); + return UnionType::empty(); + } + $combined_union_type = null; + foreach ($this->classListFromNode($class_node) as $class) { + if (!$class->hasMethodWithName($this->code_base, $method_name, true)) { + continue; + } + + try { + $method = $class->getMethodByName( + $this->code_base, + $method_name + ); + $method->analyzeReturnTypes($this->code_base); // For daemon/server mode, call this to consistently ensure accurate return types. + + if ($method->hasTemplateType()) { + try { + $method = $method->resolveTemplateType( + $this->code_base, + UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $class_node) + ); + } catch (RecursionDepthException $_) { + } + } + + if ($method->hasDependentReturnType()) { + $union_type = $method->getDependentReturnType($this->code_base, $this->context, $node->children['args']->children); + } else { + $union_type = $method->getUnionType(); + } + + // Map template types to concrete types + // TODO: When the template types are part of the method doc comment, don't look it up in the class union type + if (isset($node->children['expr']) && $union_type->hasTemplateTypeRecursive()) { + // Get the type of the object calling the method + $expression_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + + // Map template types to concrete types + $union_type = $union_type->withTemplateParameterTypeMap( + $expression_type->getTemplateParameterTypeMap($this->code_base) + ); + } + + // Resolve any references to `static` or `static[]` + if ($this->context->isInClassScope() && + $static_class_node instanceof Node && + $static_class_node->kind === ast\AST_NAME && + \strcasecmp($static_class_node->children['name'], 'parent') === 0) { + // If parent::foo() returns `static`, then use the current class instead of the parent class + $union_type = $union_type->withStaticResolvedInContext($this->context); + } else { + $union_type = $union_type->withStaticResolvedInContext($class->getInternalContext()); + } + + if ($combined_union_type) { + '@phan-var UnionType $combined_union_type'; + $combined_union_type = $combined_union_type->withUnionType($union_type); + } else { + $combined_union_type = $union_type; + } + } catch (IssueException $_) { + continue; + } + } + } catch (IssueException $_) { + // Swallow it + } catch (CodeBaseException $exception) { + $exception_fqsen = $exception->getFQSEN(); + $this->emitIssueWithSuggestion( + Issue::UndeclaredClassMethod, + $node->lineno, + [$method_name, (string)$exception->getFQSEN()], + ($exception_fqsen instanceof FullyQualifiedClassName + ? IssueFixSuggester::suggestSimilarClassForMethod($this->code_base, $this->context, $exception_fqsen, $method_name, $node->kind === \ast\AST_STATIC_CALL) + : null) + ); + } + + return $combined_union_type ?? UnionType::empty(); + } + + /** + * Visit a node with kind `\ast\AST_ASSIGN` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitAssign(Node $node): UnionType + { + // XXX typed properties/references will change the type of the result from the right hand side + return self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + } + + /** + * Visit a node with kind `\ast\AST_UNARY_OP` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + */ + public function visitUnaryOp(Node $node): UnionType + { + $result = self::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + + // Shortcut some easy operators + $flags = $node->flags; + if ($flags === \ast\flags\UNARY_BOOL_NOT) { + return $result->applyUnaryNotOperator(); + } + + if ($flags === \ast\flags\UNARY_MINUS) { + $this->warnAboutInvalidUnaryOp( + $node, + static function (Type $type): bool { + return $type->isValidNumericOperand(); + }, + $result, + '-', + Issue::TypeInvalidUnaryOperandNumeric + ); + $new_result = $result->applyUnaryMinusOperator(); + return $new_result; + } elseif ($flags === \ast\flags\UNARY_PLUS) { + $this->warnAboutInvalidUnaryOp( + $node, + static function (Type $type): bool { + // NOTE: Don't be as strict because this is a way to cast to a number + return $type->isValidNumericOperand() || \get_class($type) === StringType::class; + }, + $result, + '+', + Issue::TypeInvalidUnaryOperandNumeric + ); + return $result->applyUnaryPlusOperator(); + } elseif ($flags === \ast\flags\UNARY_BITWISE_NOT) { + $this->warnAboutInvalidUnaryOp( + $node, + static function (Type $type): bool { + // Adding $type instanceof StringType in case it becomes necessary later + // @phan-suppress-next-line PhanAccessMethodInternal + return ($type->isValidNumericOperand() && $type->isValidBitwiseOperand()) || $type instanceof StringType; + }, + $result, + '~', + Issue::TypeInvalidUnaryOperandBitwiseNot + ); + return $result->applyUnaryBitwiseNotOperator(); + } + // UNARY_SILENCE + return $result; + } + + /** + * @param Node $node with type AST_BINARY_OP + * @param Closure(Type):bool $is_valid_type + */ + private function warnAboutInvalidUnaryOp( + Node $node, + Closure $is_valid_type, + UnionType $type, + string $operator, + string $issue_type + ): void { + if ($type->isEmpty()) { + return; + } + if (!$type->hasTypeMatchingCallback($is_valid_type)) { + $this->emitIssue( + $issue_type, + $node->children['left']->lineno ?? $node->lineno, + $operator, + $type + ); + } + } + + /** + * `print($str)` always returns 1. + * See https://secure.php.net/manual/en/function.print.php#refsect1-function.print-returnvalues + * @param Node $node @phan-unused-param + */ + public function visitPrint(Node $node): UnionType + { + return LiteralIntType::instanceForValue(1, false)->asRealUnionType(); + } + + /** + * @param Node $node + * A node holding a class name + * + * @return UnionType + * The set of types that are possibly produced by the + * given node + * + * @throws IssueException + * An exception is thrown if we can't find a class for + * the given type + */ + private function visitClassNameNode(Node $node): UnionType + { + $kind = $node->kind; + // Anonymous class of form `new class { ... }` + if ($kind === \ast\AST_CLASS + && ($node->flags & \ast\flags\CLASS_ANONYMOUS) + ) { + // Generate a stable name for the anonymous class + $anonymous_class_name = + (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getUnqualifiedNameForAnonymousClass(); + + // Turn that into a fully qualified name, and that into a union type + // @phan-suppress-next-line PhanThrowTypeMismatchForCall + $fqsen = FullyQualifiedClassName::fromStringInContext( + $anonymous_class_name, + $this->context + ); + + // Turn that into a union type + return $fqsen->asType()->asRealUnionType(); + } + + // Things of the form `new $className()`, `new $obj()`, `new (foo())()`, etc. + if ($kind !== \ast\AST_NAME) { + return $this->classTypesForNonName($node); + } + + // Get the name of the class + $class_name = $node->children['name']; + + // If this is a straight-forward class name, recurse into the + // class node and get its type + if (Type::isStaticTypeString($class_name)) { + return StaticType::instance(false)->asRealUnionType(); + } + if (!Type::isSelfTypeString($class_name)) { + // @phan-suppress-next-line PhanThrowTypeMismatchForCall + return self::unionTypeFromClassNode( + $this->code_base, + $this->context, + $node + ); + } + + // This node references `self` or `static` + if (!$this->context->isInClassScope()) { + $this->emitIssue( + Issue::ContextNotObject, + $node->lineno, + $class_name + ); + + return UnionType::empty(); + } + + // Reference to a parent class + if (\strcasecmp($class_name, 'parent') === 0) { + $class = $this->context->getClassInScope( + $this->code_base + ); + + $parent_type_option = $class->getParentTypeOption(); + if (!$parent_type_option->isDefined()) { + $this->emitIssue( + Issue::ParentlessClass, + $node->lineno, + (string)$class->getFQSEN() + ); + + return UnionType::empty(); + } + + return $parent_type_option->get()->asRealUnionType(); + } + + return $this->context->getClassFQSEN()->asType()->asRealUnionType(); + } + + /** + * @param Node $node @phan-unused-param + * A node containing a throw expression. + * + * @return UnionType + * `void` is as close as possible to `no-return` or `never` for types currently available in Phan. + */ + public function visitThrow(Node $node): UnionType + { + return VoidType::instance(false)->asRealUnionType(); + } + + private function classTypesForNonName(Node $node): UnionType + { + $node_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node + ); + if ($node_type->isEmpty()) { + return UnionType::empty(); + } + $result = UnionType::empty(); + $is_valid = true; + foreach ($node_type->getTypeSet() as $sub_type) { + if ($sub_type instanceof LiteralStringType) { + $value = $sub_type->getValue(); + if (!\preg_match('/\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\]*/', $value)) { + $is_valid = false; + continue; + } + try { + $fqsen = FullyQualifiedClassName::fromFullyQualifiedString($value); + } catch (FQSENException $e) { + $this->emitIssue( + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike, + $node->lineno, + $e->getFQSEN() + ); + continue; + } + if (!$this->code_base->hasClassWithFQSEN($fqsen)) { + $is_valid = false; + continue; + } + $result = $result->withType($fqsen->asType()); + } elseif (\get_class($sub_type) === Type::class || $sub_type instanceof ClosureType || $sub_type instanceof StaticType) { + $result = $result->withType($sub_type); + } else { + if ($sub_type instanceof StringType) { + if ($sub_type instanceof ClassStringType) { + $result = $result->withUnionType($sub_type->getClassUnionType()); + } + continue; + } + if (!($sub_type instanceof MixedType)) { + $is_valid = false; + } + } + } + if ($result->isEmpty() && !$is_valid) { + // See https://github.com/phan/phan/issues/1926 - `new $obj()` is valid PHP and documented in the manual. + $this->emitIssue( + Issue::TypeExpectedObjectOrClassName, + $node->lineno, + ASTReverter::toShortString($node), + $node_type + ); + } + return $result; + } + + /** + * @param CodeBase $code_base + * The code base within which we're operating + * + * @param Context $context + * The context of the parser at the node for which we'd + * like to determine a type + * + * @param Node|mixed $node + * The node which we'd like to determine the type of. + * + * @return UnionType + * The UnionType of the class for the node representing + * a usage of a class in the given Context within the given CodeBase + * + * @throws IssueException + * An exception is thrown if we can't find a class for + * the given type + * + * @throws FQSENException + * An exception is thrown if we can find a class name, + * but it is empty/invalid + */ + public static function unionTypeFromClassNode( + CodeBase $code_base, + Context $context, + $node + ): UnionType { + // If this is a list, build a union type by + // recursively visiting the child nodes + if ($node instanceof Node + && $node->kind === \ast\AST_NAME_LIST + ) { + $union_type = UnionType::empty(); + foreach ($node->children as $child_node) { + $union_type = $union_type->withUnionType( + self::unionTypeFromClassNode( + $code_base, + $context, + $child_node + ) + ); + } + return $union_type; + } + + // For simple nodes or very complicated nodes, + // recurse + if (!($node instanceof Node) + || $node->kind !== \ast\AST_NAME + ) { + return self::unionTypeFromNode( + $code_base, + $context, + $node + ); + } + + $class_name = (string)$node->children['name']; + + if (\strcasecmp('parent', $class_name) === 0) { + if (!$context->isInClassScope()) { + throw new IssueException( + Issue::fromType(Issue::ContextNotObject)( + $context->getFile(), + $node->lineno ?? $context->getLineNumberStart(), + [$class_name] + ) + ); + } + + $class = $context->getClassInScope($code_base); + + if ($class->isTrait()) { + throw new IssueException( + Issue::fromType(Issue::TraitParentReference)( + $context->getFile(), + $node->lineno ?? $context->getLineNumberStart(), + [(string)$context->getClassFQSEN() ] + ) + ); + } + + if (!$class->hasParentType()) { + throw new IssueException( + Issue::fromType(Issue::ParentlessClass)( + $context->getFile(), + $node->lineno ?? $context->getLineNumberStart(), + [ (string)$context->getClassFQSEN() ] + ) + ); + } + + $parent_class_fqsen = $class->getParentClassFQSEN(); + + if (!$code_base->hasClassWithFQSEN($parent_class_fqsen)) { + throw new IssueException( + Issue::fromType(Issue::UndeclaredClass)( + $context->getFile(), + $node->lineno ?? $context->getLineNumberStart(), + [ (string)$parent_class_fqsen ], + IssueFixSuggester::suggestSimilarClass($code_base, $context, $parent_class_fqsen) + ) + ); + } else { + $parent_class = $code_base->getClassByFQSEN( + $parent_class_fqsen + ); + + return $parent_class->getUnionType(); + } + } + + // We're going to convert the class reference to a type + + // Check to see if the name is fully qualified + if ($node->flags & \ast\flags\NAME_NOT_FQ) { + self::checkValidClassFQSEN($context, $node, $class_name); + $type = Type::fromStringInContext( + $class_name, + $context, + Type::FROM_NODE + ); + } elseif ($node->flags & \ast\flags\NAME_RELATIVE) { + // Relative to current namespace + if (0 !== \strpos($class_name, '\\')) { + $class_name = '\\' . $class_name; + } + + $type = Type::fromFullyQualifiedString( + $context->getNamespace() . $class_name + ); + } else { + // Fully qualified + if (0 !== \strpos($class_name, '\\')) { + $class_name = '\\' . $class_name; + } + + self::checkValidClassFQSEN($context, $node, $class_name); + $type = Type::fromFullyQualifiedString( + $class_name + ); + } + + return $type->asRealUnionType(); + } + + /** + * @throws FQSENException if invalid + */ + private static function checkValidClassFQSEN(Context $context, Node $node, string $class_name): void + { + // @phan-suppress-next-line PhanAccessClassConstantInternal + if (\preg_match(FullyQualifiedGlobalStructuralElement::VALID_STRUCTURAL_ELEMENT_REGEX, $class_name)) { + return; + } + throw new IssueException( + Issue::fromType($class_name === '\\' ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike)( + $context->getFile(), + $node->lineno, + [ $class_name ] + ) + ); + } + + /** + * @return \Generator|Clazz[] + */ + public static function classListFromNodeAndContext(CodeBase $code_base, Context $context, Node $node) + { + return (new UnionTypeVisitor($code_base, $context, true))->classListFromNode($node); + } + + /** + * @phan-return \Generator + * @return \Generator|Clazz[] + * A list of classes associated with the given node + * + * @throws IssueException + * An exception is thrown if we can't find a class for + * the given type + */ + private function classListFromNode(Node $node): \Generator + { + // Get the types associated with the node + $union_type = self::unionTypeFromNode( + $this->code_base, + $this->context, + $node + )->withStaticResolvedInContext($this->context); + + // Iterate over each viable class type to see if any + // have the constant we're looking for + foreach ($union_type->nonNativeTypes()->getTypeSet() as $class_type) { + // Get the class FQSEN + try { + $class_fqsen = FullyQualifiedClassName::fromType($class_type); + } catch (InvalidFQSENException $e) { + throw new IssueException( + Issue::fromType($e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike)( + $this->context->getFile(), + $node->lineno, + [ (string)$class_type ] + ) + ); + } + + // See if the class exists + if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) { + throw new IssueException( + Issue::fromType(Issue::UndeclaredClassReference)( + $this->context->getFile(), + $node->lineno, + [ (string)$class_fqsen ] + ) + ); + } + + yield $this->code_base->getClassByFQSEN($class_fqsen); + } + } + + /** + * @param CodeBase $code_base + * @param Context $context + * @param int|string|float|Node $node the node to fetch CallableType instances for. + * @param bool $log_error whether or not to log errors while searching @phan-unused-param + * @return list + * TODO: use log_error + */ + public static function functionLikeListFromNodeAndContext(CodeBase $code_base, Context $context, $node, bool $log_error): array + { + try { + $function_fqsens = (new UnionTypeVisitor($code_base, $context, true))->functionLikeFQSENListFromNode($node); + } catch (FQSENException $e) { + Issue::maybeEmit( + $code_base, + $context, + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInCallable : Issue::InvalidFQSENInCallable, + $context->getLineNumberStart(), + $e->getFQSEN() + ); + return []; + } catch (\InvalidArgumentException $_) { + Issue::maybeEmit( + $code_base, + $context, + Issue::InvalidFQSENInCallable, + $context->getLineNumberStart(), + '(unknown)' + ); + return []; + } + $functions = []; + foreach ($function_fqsens as $fqsen) { + if ($fqsen instanceof FullyQualifiedMethodName) { + if (!$code_base->hasMethodWithFQSEN($fqsen)) { + // TODO: error PhanArrayMapClosure + continue; + } + $functions[] = $code_base->getMethodByFQSEN($fqsen); + } else { + if (!($fqsen instanceof FullyQualifiedFunctionName)) { + throw new TypeError('Expected fqsen to be a FullyQualifiedFunctionName or FullyQualifiedMethodName'); + } + if (!$code_base->hasFunctionWithFQSEN($fqsen)) { + // TODO: error PhanArrayMapClosure + continue; + } + $functions[] = $code_base->getFunctionByFQSEN($fqsen); + } + } + return $functions; + } + + /** + * Fetch known classes for a place where a class name was provided as a string or string expression. + * Warn if this is an invalid class name. + * @param \ast\Node|string|int|float $node + * @return list + */ + public static function classListFromClassNameNode(CodeBase $code_base, Context $context, $node): array + { + $results = []; + $strings = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node)->asStringScalarValues(); + foreach ($strings as $string) { + try { + $fqsen = FullyQualifiedClassName::fromFullyQualifiedString($string); + } catch (FQSENException $e) { + Issue::maybeEmit( + $code_base, + $context, + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike, + $context->getLineNumberStart(), + $e->getFQSEN() + ); + continue; + } catch (\InvalidArgumentException $_) { + Issue::maybeEmit( + $code_base, + $context, + Issue::InvalidFQSENInClasslike, + $context->getLineNumberStart(), + '(unknown)' + ); + continue; + } + if (!$code_base->hasClassWithFQSEN($fqsen)) { + // TODO: Different issue type? + Issue::maybeEmit( + $code_base, + $context, + Issue::UndeclaredClassReference, + $context->getLineNumberStart(), + (string)$fqsen + ); + continue; + } + $results[] = $code_base->getClassByFQSEN($fqsen); + } + return $results; + } + + /** + * @param CodeBase $code_base + * @param Context $context + * @param string|Node $node the node to fetch CallableType instances for. + * @return list + * @suppress PhanUnreferencedPublicMethod may be used in the future. + */ + public static function functionLikeFQSENListFromNodeAndContext(CodeBase $code_base, Context $context, $node): array + { + return (new UnionTypeVisitor($code_base, $context, true))->functionLikeFQSENListFromNode($node); + } + + /** + * @param string|Node $class_or_expr + * @param string $method_name + * + * @return list + * A list of CallableTypes associated with the given node + */ + private function methodFQSENListFromObjectAndMethodName($class_or_expr, string $method_name): array + { + $code_base = $this->code_base; + $context = $this->context; + + $union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $class_or_expr); + if ($union_type->isEmpty()) { + return []; + } + $object_types = $union_type->objectTypes(); + if ($object_types->isEmpty()) { + if (!$union_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) { + $this->emitIssue( + Issue::TypeInvalidCallableObjectOfMethod, + $context->getLineNumberStart(), + (string)$union_type, + $method_name + ); + } + return []; + } + $result_types = []; + $class = null; + foreach ($object_types->getTypeSet() as $object_type) { + // TODO: support templates here. + if ($object_type instanceof ObjectType || $object_type instanceof TemplateType) { + continue; + } + $class_fqsen = FullyQualifiedClassName::fromType($object_type); + if ($object_type instanceof StaticOrSelfType) { + if (!$context->isInClassScope()) { + $this->emitIssue( + Issue::ContextNotObjectInCallable, + $context->getLineNumberStart(), + (string)$class_fqsen, + "$class_fqsen::$method_name" + ); + continue; + } + $class_fqsen = $context->getClassFQSEN(); + } + if (!$code_base->hasClassWithFQSEN($class_fqsen)) { + $this->emitIssue( + Issue::UndeclaredClassInCallable, + $context->getLineNumberStart(), + (string)$class_fqsen, + "$class_fqsen::$method_name" + ); + continue; + } + $class = $code_base->getClassByFQSEN($class_fqsen); + if (!$class->hasMethodWithName($code_base, $method_name, true)) { + // emit error below + continue; + } + $method_fqsen = FullyQualifiedMethodName::make( + $class_fqsen, + $method_name + ); + $result_types[] = $method_fqsen; + } + if (\count($result_types) === 0 && $class instanceof Clazz) { + // TODO: Include suggestion for method name + $this->emitIssue( + Issue::UndeclaredMethodInCallable, + $context->getLineNumberStart(), + $method_name, + (string)$union_type + ); + } + return $result_types; + } + + /** + * @param string $class_name (may also be 'self', 'parent', or 'static') + * @throws FQSENException + */ + private function lookupClassOfCallableByName(string $class_name): ?FullyQualifiedClassName + { + switch (\strtolower($class_name)) { + case 'self': + case 'static': + $context = $this->context; + if (!$context->isInClassScope()) { + $this->emitIssue( + Issue::ContextNotObject, + $context->getLineNumberStart(), + \strtolower($class_name) + ); + return null; + } + return $context->getClassFQSEN(); + case 'parent': + $context = $this->context; + if (!$context->isInClassScope()) { + $this->emitIssue( + Issue::ContextNotObject, + $context->getLineNumberStart(), + \strtolower($class_name) + ); + return null; + } + $class = $context->getClassInScope($this->code_base); + if ($class->isTrait()) { + $this->emitIssue( + Issue::TraitParentReference, + $context->getLineNumberStart(), + (string)$class->getFQSEN() + ); + return null; + } + if (!$class->hasParentType()) { + $this->emitIssue( + Issue::ParentlessClass, + $context->getLineNumberStart(), + (string)$class->getFQSEN() + ); + return null; + } + return $class->getParentClassFQSEN(); // may or may not exist. + default: + // TODO: Reject invalid/empty class names earlier + return FullyQualifiedClassName::fromFullyQualifiedString($class_name); + } + } + + private function emitNonObjectContextInCallableIssue(string $class_name, string $method_name): void + { + $this->emitIssue( + Issue::ContextNotObjectInCallable, + $this->context->getLineNumberStart(), + $class_name, + "$class_name::$method_name" + ); + } + + /** + * @param string|Node $class_or_expr + * @param string|Node $method_name + * + * @return list + * A list of `FullyQualifiedMethodName`s associated with the given node + */ + private function methodFQSENListFromParts($class_or_expr, $method_name): array + { + $code_base = $this->code_base; + $context = $this->context; + + if (!is_string($method_name)) { + if (!($method_name instanceof Node)) { + $method_name = UnionTypeVisitor::anyStringLiteralForNode($this->code_base, $this->context, $method_name); + } + $method_name = (new ContextNode($code_base, $context, $method_name))->getEquivalentPHPScalarValue(); + if (!is_string($method_name)) { + $method_name_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $method_name); + if (!$method_name_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeInvalidCallableMethodName, + $method_name->lineno ?? $this->context->getLineNumberStart(), + $method_name_type + ); + } + return []; + } + } + try { + if (is_string($class_or_expr)) { + if (\in_array(\strtolower($class_or_expr), ['static', 'self', 'parent'], true)) { + // Allow 'static' but not '\static' + if (!$context->isInClassScope()) { + $this->emitNonObjectContextInCallableIssue($class_or_expr, $method_name); + return []; + } + $class_fqsen = $context->getClassFQSEN(); + } else { + $class_fqsen = $this->lookupClassOfCallableByName($class_or_expr); + if (!$class_fqsen) { + return []; + } + } + } else { + $class_fqsen = (new ContextNode($code_base, $context, $class_or_expr))->resolveClassNameInContext(); + if (!$class_fqsen) { + return $this->methodFQSENListFromObjectAndMethodName($class_or_expr, $method_name); + } + if (\in_array(\strtolower($class_fqsen->getName()), ['static', 'self', 'parent'], true)) { + if (!$context->isInClassScope()) { + $this->emitNonObjectContextInCallableIssue((string)$class_fqsen, $method_name); + return []; + } + $class_fqsen = $context->getClassFQSEN(); + } + } + } catch (FQSENException $e) { + $this->emitIssue( + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike, + $context->getLineNumberStart(), + $e->getFQSEN() + ); + return []; + } + if (!$code_base->hasClassWithFQSEN($class_fqsen)) { + $this->emitIssue( + Issue::UndeclaredClassInCallable, + $context->getLineNumberStart(), + (string)$class_fqsen, + "$class_fqsen::$method_name" + ); + return []; + } + $class = $code_base->getClassByFQSEN($class_fqsen); + if (!$class->hasMethodWithName($code_base, $method_name, true)) { + $this->emitIssue( + Issue::UndeclaredStaticMethodInCallable, + $context->getLineNumberStart(), + "$class_fqsen::$method_name" + ); + return []; + } + $method = $class->getMethodByName($code_base, $method_name); + if (!$method->isStatic()) { + $this->emitIssue( + Issue::StaticCallToNonStatic, + $context->getLineNumberStart(), + (string)$method->getFQSEN(), + $method->getFileRef()->getFile(), + (string)$method->getFileRef()->getLineNumberStart() + ); + } + return [$method->getFQSEN()]; + } + + /** + * @see ContextNode::getFunction() for a similar function + * @return list + */ + private function functionFQSENListFromFunctionName(string $function_name): array + { + // TODO: Catch invalid code such as call_user_func('\\\\x\\\\y') + try { + $function_fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString($function_name); + } catch (FQSENException $e) { + $this->emitIssue( + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInCallable : Issue::InvalidFQSENInCallable, + $this->context->getLineNumberStart(), + $function_name + ); + return []; + } + if (!$this->code_base->hasFunctionWithFQSEN($function_fqsen)) { + $this->emitIssue( + Issue::UndeclaredFunctionInCallable, + $this->context->getLineNumberStart(), + $function_name + ); + return []; + } + return [$function_fqsen]; + } + + /** + * @param string|Node $node + * + * @return list + * A list of `FullyQualifiedFunctionLikeName`s associated with the given node + * + * @throws IssueException + * An exception is thrown if we can't find a class for + * the given type + */ + private function functionLikeFQSENListFromNode($node): array + { + $orig_node = $node; + if ($node instanceof Node) { + $node = (new ContextNode($this->code_base, $this->context, $node))->getEquivalentPHPValue(); + } + if (is_string($node)) { + if (\strpos($node, '::') !== false) { + [$class_name, $method_name] = \explode('::', $node, 2); + return $this->methodFQSENListFromParts($class_name, $method_name); + } + return $this->functionFQSENListFromFunctionName($node); + } + if (\is_array($node)) { + if (\count($node) !== 2) { + $this->emitIssue( + Issue::TypeInvalidCallableArraySize, + $orig_node->lineno ?? $this->context->getLineNumberStart(), + \count($node) + ); + return []; + } + $i = 0; + foreach ($node as $key => $_) { + if ($key !== $i) { + $this->emitIssue( + Issue::TypeInvalidCallableArrayKey, + $orig_node->lineno ?? $this->context->getLineNumberStart(), + $i + ); + return []; + } + $i++; + } + return $this->methodFQSENListFromParts($node[0], $node[1]); + } + if (!($node instanceof Node)) { + // TODO: Warn? + return []; + } + + // Get the types associated with the node + $union_type = self::unionTypeFromNode( + $this->code_base, + $this->context, + $node + ); + + $closure_types = []; + foreach ($union_type->getTypeSet() as $type) { + if ($type instanceof ClosureType && $type->hasKnownFQSEN()) { + // TODO: Support class instances with __invoke() + $fqsen = $type->asFQSEN(); + if (!($fqsen instanceof FullyQualifiedFunctionLikeName)) { + throw new AssertionError('Expected fqsen of closure to be a FullyQualifiedFunctionLikeName'); + } + $closure_types[] = $fqsen; + } + } + return $closure_types; + } + + /** + * @param CodeBase $code_base + * @param Context $context + * @param Node|string|float|int $node + * + * @return ?UnionType (Returns null when mixed) + * TODO: Add an equivalent for Traversable and subclasses, once we have template support for Traversable + */ + public static function unionTypeOfArrayKeyForNode(CodeBase $code_base, Context $context, $node): ?UnionType + { + $arg_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node); + return self::arrayKeyUnionTypeOfUnionType($arg_type); + } + + /** + * @return ?UnionType (Returns null when mixed) + * TODO: Add an equivalent for Traversable and subclasses, once we have template support for Traversable + * TODO: Move into UnionType? + */ + public static function arrayKeyUnionTypeOfUnionType(UnionType $union_type): ?UnionType + { + if ($union_type->isEmpty()) { + return null; + } + static $int_type; + static $string_type; + static $int_or_string_type; + if ($int_type === null) { + $int_type = IntType::instance(false)->asPHPDocUnionType(); + $string_type = StringType::instance(false)->asPHPDocUnionType(); + $int_or_string_type = UnionType::fromFullyQualifiedPHPDocString('int|string'); + } + $key_enum_type = GenericArrayType::keyTypeFromUnionTypeKeys($union_type); + switch ($key_enum_type) { + case GenericArrayType::KEY_INT: + return $int_type; + case GenericArrayType::KEY_STRING: + return $string_type; + default: + foreach ($union_type->getTypeSet() as $type) { + // The exact class Type is potentially invalid (includes objects) but not the subclass NativeType. + // The subclass IterableType of Native type is invalid, but ArrayType is a valid subclass of IterableType. + // And we just ignore scalars. + // And mixed could be a Traversable. + // So, don't infer anything if the union type contains any instances of the four classes. + // TODO: Check the expanded union type instead of anything with a class of exactly Type, searching for Traversable? + if (\in_array(\get_class($type), [Type::class, IterableType::class, TemplateType::class, MixedType::class, NonEmptyMixedType::class], true)) { + return null; + } + } + return $int_or_string_type; + } + } + + /** + * @param Node|array|string|bool|float|int|null $node + * @return ?string - One of the values for the LiteralStringType, or null + */ + public static function anyStringLiteralForNode( + CodeBase $code_base, + Context $context, + $node + ): ?string { + if (!($node instanceof Node)) { + return is_string($node) ? $node : null; + } + $node_type = self::unionTypeFromNode( + $code_base, + $context, + $node + ); + foreach ($node_type->getTypeSet() as $type) { + if ($type instanceof LiteralStringType) { + // Arbitrarily return only the first value. + // TODO: Rewrite code using this to work with lists of possible values? + return $type->getValue(); + } + } + return null; + } + + private function analyzeNegativeStringOffsetCompatibility(Node $node, UnionType $dim_type): void + { + $dim_value = $dim_type->asSingleScalarValueOrNull(); + if (!\is_int($dim_value) || $dim_value >= 0) { + return; + } + $this->emitIssue( + Issue::CompatibleNegativeStringOffset, + $node->children['dim']->lineno ?? $node->lineno + ); + } + + /** + * Returns the union of all union types of expressions in this expression list (ast\AST_EXPR_LIST). + * + * This is useful for match arm conditions. + * + * For other use cases, get the union type of the last node (if one exists) instead. + * + * @override + */ + public function visitExprList(Node $node): UnionType + { + $types = []; + foreach ($node->children as $child_node) { + $types[] = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $child_node); + } + return UnionType::merge($types); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/Visitor/Element.php b/bundled-libs/phan/phan/src/Phan/AST/Visitor/Element.php new file mode 100644 index 000000000..db007d242 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/Visitor/Element.php @@ -0,0 +1,405 @@ +kind, Node->flags for a specific kind, etc) + * + * For performance, many callers manually inline the implementation of these methods + */ +class Element +{ + use \Phan\Profile; + + /** + * @var Node The node which this Visitor will have $this->visit*() called on. + */ + private $node; + + /** + * @param Node $node + * Any AST node. + */ + public function __construct(Node $node) + { + $this->node = $node; + } + + // TODO: Revert this change back to the switch statement + // when php 7.2 is released and Phan supports php 7.2. + // TODO: Also look into initializing mappings of ast\Node->kind to ReflectionMethod->getClosure for those methods, + // it may be more efficient. + // See https://github.com/php/php-src/pull/2427/files + // This decreased the duration of running phan by about 4% + public const VISIT_LOOKUP_TABLE = [ + ast\AST_ARG_LIST => 'visitArgList', + ast\AST_ARRAY => 'visitArray', + ast\AST_ARRAY_ELEM => 'visitArrayElem', + ast\AST_ARROW_FUNC => 'visitArrowFunc', + ast\AST_ASSIGN => 'visitAssign', + ast\AST_ASSIGN_OP => 'visitAssignOp', + ast\AST_ASSIGN_REF => 'visitAssignRef', + ast\AST_ATTRIBUTE => 'visitAttribute', + ast\AST_ATTRIBUTE_LIST => 'visitAttributeList', + ast\AST_ATTRIBUTE_GROUP => 'visitAttributeGroup', + ast\AST_BINARY_OP => 'visitBinaryOp', + ast\AST_BREAK => 'visitBreak', + ast\AST_CALL => 'visitCall', + ast\AST_CAST => 'visitCast', + ast\AST_CATCH => 'visitCatch', + ast\AST_CLASS => 'visitClass', + ast\AST_CLASS_CONST => 'visitClassConst', + ast\AST_CLASS_CONST_DECL => 'visitClassConstDecl', + ast\AST_CLASS_CONST_GROUP => 'visitClassConstGroup', + ast\AST_CLASS_NAME => 'visitClassName', + ast\AST_CLOSURE => 'visitClosure', + ast\AST_CLOSURE_USES => 'visitClosureUses', + ast\AST_CLOSURE_VAR => 'visitClosureVar', + ast\AST_CONST => 'visitConst', + ast\AST_CONST_DECL => 'visitConstDecl', + ast\AST_CONST_ELEM => 'visitConstElem', + ast\AST_DECLARE => 'visitDeclare', + ast\AST_DIM => 'visitDim', + ast\AST_DO_WHILE => 'visitDoWhile', + ast\AST_ECHO => 'visitEcho', + ast\AST_EMPTY => 'visitEmpty', + ast\AST_ENCAPS_LIST => 'visitEncapsList', + ast\AST_EXIT => 'visitExit', + ast\AST_EXPR_LIST => 'visitExprList', + ast\AST_FOREACH => 'visitForeach', + ast\AST_FUNC_DECL => 'visitFuncDecl', + ast\AST_ISSET => 'visitIsset', + ast\AST_GLOBAL => 'visitGlobal', + ast\AST_GROUP_USE => 'visitGroupUse', + ast\AST_IF => 'visitIf', + ast\AST_IF_ELEM => 'visitIfElem', + ast\AST_INSTANCEOF => 'visitInstanceof', + ast\AST_MAGIC_CONST => 'visitMagicConst', + ast\AST_MATCH => 'visitMatch', + ast\AST_MATCH_ARM => 'visitMatchArm', + ast\AST_MATCH_ARM_LIST => 'visitMatchArmList', + ast\AST_METHOD => 'visitMethod', + ast\AST_METHOD_CALL => 'visitMethodCall', + ast\AST_NAME => 'visitName', + ast\AST_NAMED_ARG => 'visitNamedArg', + ast\AST_NAMESPACE => 'visitNamespace', + ast\AST_NEW => 'visitNew', + ast\AST_NULLSAFE_METHOD_CALL => 'visitNullsafeMethodCall', + ast\AST_NULLSAFE_PROP => 'visitNullsafeProp', + ast\AST_PARAM => 'visitParam', + ast\AST_PARAM_LIST => 'visitParamList', + ast\AST_PRE_INC => 'visitPreInc', + ast\AST_PRINT => 'visitPrint', + ast\AST_PROP => 'visitProp', + ast\AST_PROP_DECL => 'visitPropDecl', + ast\AST_PROP_ELEM => 'visitPropElem', + ast\AST_PROP_GROUP => 'visitPropGroup', + ast\AST_RETURN => 'visitReturn', + ast\AST_STATIC => 'visitStatic', + ast\AST_STATIC_CALL => 'visitStaticCall', + ast\AST_STATIC_PROP => 'visitStaticProp', + ast\AST_STMT_LIST => 'visitStmtList', + ast\AST_SWITCH => 'visitSwitch', + ast\AST_SWITCH_CASE => 'visitSwitchCase', + ast\AST_SWITCH_LIST => 'visitSwitchList', + ast\AST_TYPE => 'visitType', + ast\AST_TYPE_UNION => 'visitTypeUnion', + ast\AST_NULLABLE_TYPE => 'visitNullableType', + ast\AST_UNARY_OP => 'visitUnaryOp', + ast\AST_USE => 'visitUse', + ast\AST_USE_ELEM => 'visitUseElem', + ast\AST_USE_TRAIT => 'visitUseTrait', + ast\AST_VAR => 'visitVar', + ast\AST_WHILE => 'visitWhile', + ast\AST_CATCH_LIST => 'visitCatchList', + ast\AST_CLONE => 'visitClone', + ast\AST_CONDITIONAL => 'visitConditional', + ast\AST_CONTINUE => 'visitContinue', + ast\AST_FOR => 'visitFor', + ast\AST_GOTO => 'visitGoto', + ast\AST_HALT_COMPILER => 'visitHaltCompiler', + ast\AST_INCLUDE_OR_EVAL => 'visitIncludeOrEval', + ast\AST_LABEL => 'visitLabel', + ast\AST_METHOD_REFERENCE => 'visitMethodReference', + ast\AST_NAME_LIST => 'visitNameList', + ast\AST_POST_DEC => 'visitPostDec', + ast\AST_POST_INC => 'visitPostInc', + ast\AST_PRE_DEC => 'visitPreDec', + ast\AST_REF => 'visitRef', + ast\AST_SHELL_EXEC => 'visitShellExec', + ast\AST_THROW => 'visitThrow', + ast\AST_TRAIT_ADAPTATIONS => 'visitTraitAdaptations', + ast\AST_TRAIT_ALIAS => 'visitTraitAlias', + ast\AST_TRAIT_PRECEDENCE => 'visitTraitPrecedence', + ast\AST_TRY => 'visitTry', + ast\AST_UNPACK => 'visitUnpack', + ast\AST_UNSET => 'visitUnset', + ast\AST_YIELD => 'visitYield', + ast\AST_YIELD_FROM => 'visitYieldFrom', + ]; + + /** + * Accepts a visitor that differentiates on the kind value + * of the AST node. + * + * NOTE: This was turned into a static method for performance + * because it was called extremely frequently. + * + * @return mixed - The type depends on the subclass of KindVisitor being used. + * @suppress PhanUnreferencedPublicMethod Phan's code inlines this, but may be useful for some plugins + */ + public static function acceptNodeAndKindVisitor(Node $node, KindVisitor $visitor) + { + $fn_name = self::VISIT_LOOKUP_TABLE[$node->kind] ?? null; + if (\is_string($fn_name)) { + return $visitor->{$fn_name}($node); + } else { + Debug::printNode($node); + throw new AssertionError('All node kinds must match'); + } + } + + public const VISIT_BINARY_LOOKUP_TABLE = [ + 252 => 'visitBinaryConcat', // ZEND_PARENTHESIZED_CONCAT is returned instead of ZEND_CONCAT in earlier php-ast versions in PHP 7.4. This is fixed in php-ast 1.0.2 + flags\BINARY_ADD => 'visitBinaryAdd', + flags\BINARY_BITWISE_AND => 'visitBinaryBitwiseAnd', + flags\BINARY_BITWISE_OR => 'visitBinaryBitwiseOr', + flags\BINARY_BITWISE_XOR => 'visitBinaryBitwiseXor', + flags\BINARY_BOOL_XOR => 'visitBinaryBoolXor', + flags\BINARY_CONCAT => 'visitBinaryConcat', + flags\BINARY_DIV => 'visitBinaryDiv', + flags\BINARY_IS_EQUAL => 'visitBinaryIsEqual', + flags\BINARY_IS_IDENTICAL => 'visitBinaryIsIdentical', + flags\BINARY_IS_NOT_EQUAL => 'visitBinaryIsNotEqual', + flags\BINARY_IS_NOT_IDENTICAL => 'visitBinaryIsNotIdentical', + flags\BINARY_IS_SMALLER => 'visitBinaryIsSmaller', + flags\BINARY_IS_SMALLER_OR_EQUAL => 'visitBinaryIsSmallerOrEqual', + flags\BINARY_MOD => 'visitBinaryMod', + flags\BINARY_MUL => 'visitBinaryMul', + flags\BINARY_POW => 'visitBinaryPow', + flags\BINARY_SHIFT_LEFT => 'visitBinaryShiftLeft', + flags\BINARY_SHIFT_RIGHT => 'visitBinaryShiftRight', + flags\BINARY_SPACESHIP => 'visitBinarySpaceship', + flags\BINARY_SUB => 'visitBinarySub', + flags\BINARY_BOOL_AND => 'visitBinaryBoolAnd', + flags\BINARY_BOOL_OR => 'visitBinaryBoolOr', + flags\BINARY_COALESCE => 'visitBinaryCoalesce', + flags\BINARY_IS_GREATER => 'visitBinaryIsGreater', + flags\BINARY_IS_GREATER_OR_EQUAL => 'visitBinaryIsGreaterOrEqual', + ]; + + /** + * Accepts a visitor that differentiates on the flag value + * of the AST node of kind ast\AST_BINARY_OP. + * @return mixed - The type depends on the subclass of FlagVisitor + */ + public static function acceptBinaryFlagVisitor(Node $node, FlagVisitor $visitor) + { + $fn_name = self::VISIT_BINARY_LOOKUP_TABLE[$node->flags] ?? null; + if (\is_string($fn_name)) { + return $visitor->{$fn_name}($node); + } else { + Debug::printNode($node); + throw new AssertionError("All flags must match. Found " . self::flagDescription($node)); + } + } + + /** + * Accepts a visitor that differentiates on the flag value + * of the AST node of kind ast\AST_CLASS. + * + * @return mixed - The type depends on the subclass of FlagVisitor + * @suppress PhanUnreferencedPublicMethod + */ + public function acceptClassFlagVisitor(FlagVisitor $visitor) + { + switch ($this->node->flags) { + case flags\CLASS_ABSTRACT: + return $visitor->visitClassAbstract($this->node); + case flags\CLASS_FINAL: + return $visitor->visitClassFinal($this->node); + case flags\CLASS_INTERFACE: + return $visitor->visitClassInterface($this->node); + case flags\CLASS_TRAIT: + return $visitor->visitClassTrait($this->node); + case flags\CLASS_ANONYMOUS: + return $visitor->visitClassAnonymous($this->node); + default: + throw new AssertionError("All flags must match. Found " . self::flagDescription($this->node)); + } + } + + /** + * Accepts a visitor that differentiates on the flag value + * of the AST node of kind ast\AST_NAME. + * + * @return mixed - The type depends on the subclass of FlagVisitor + * @suppress PhanUnreferencedPublicMethod + */ + public function acceptNameFlagVisitor(FlagVisitor $visitor) + { + switch ($this->node->flags) { + case flags\NAME_FQ: + return $visitor->visitNameFq($this->node); + case flags\NAME_NOT_FQ: + return $visitor->visitNameNotFq($this->node); + case flags\NAME_RELATIVE: + return $visitor->visitNameRelative($this->node); + default: + throw new AssertionError("All flags must match. Found " . self::flagDescription($this->node)); + } + } + + /** + * Accepts a visitor that differentiates on the flag value + * of the AST node of kind ast\AST_TYPE. + * + * @return mixed - The type depends on the subclass of FlagVisitor + * @suppress PhanUnreferencedPublicMethod + */ + public function acceptTypeFlagVisitor(FlagVisitor $visitor) + { + switch ($this->node->flags) { + case flags\TYPE_ARRAY: + return $visitor->visitUnionTypeArray($this->node); + case flags\TYPE_BOOL: + return $visitor->visitUnionTypeBool($this->node); + case flags\TYPE_CALLABLE: + return $visitor->visitUnionTypeCallable($this->node); + case flags\TYPE_DOUBLE: + return $visitor->visitUnionTypeDouble($this->node); + case flags\TYPE_LONG: + return $visitor->visitUnionTypeLong($this->node); + case flags\TYPE_NULL: + return $visitor->visitUnionTypeNull($this->node); + case flags\TYPE_OBJECT: + return $visitor->visitUnionTypeObject($this->node); + case flags\TYPE_STRING: + return $visitor->visitUnionTypeString($this->node); + case flags\TYPE_FALSE: + return $visitor->visitUnionTypeFalse($this->node); + case flags\TYPE_STATIC: + return $visitor->visitUnionTypeStatic($this->node); + default: + throw new AssertionError("All flags must match. Found " . self::flagDescription($this->node)); + } + } + + /** + * Accepts a visitor that differentiates on the flag value + * of the AST node of type ast\AST_UNARY_OP. + * + * @return mixed - The type depends on the subclass of FlagVisitor + * @suppress PhanUnreferencedPublicMethod + */ + public function acceptUnaryFlagVisitor(FlagVisitor $visitor) + { + switch ($this->node->flags) { + case flags\UNARY_BITWISE_NOT: + return $visitor->visitUnaryBitwiseNot($this->node); + case flags\UNARY_BOOL_NOT: + return $visitor->visitUnaryBoolNot($this->node); + case flags\UNARY_SILENCE: + return $visitor->visitUnarySilence($this->node); + default: + throw new AssertionError("All flags must match. Found " . self::flagDescription($this->node)); + } + } + + /** + * Accepts a visitor that differentiates on the flag value + * of the AST node of kind ast\AST_INCLUDE_OR_EVAL. + * + * @return mixed - The type depends on the subclass of FlagVisitor + * @suppress PhanUnreferencedPublicMethod + */ + public function acceptExecFlagVisitor(FlagVisitor $visitor) + { + switch ($this->node->flags) { + case flags\EXEC_EVAL: + return $visitor->visitExecEval($this->node); + case flags\EXEC_INCLUDE: + return $visitor->visitExecInclude($this->node); + case flags\EXEC_INCLUDE_ONCE: + return $visitor->visitExecIncludeOnce($this->node); + case flags\EXEC_REQUIRE: + return $visitor->visitExecRequire($this->node); + case flags\EXEC_REQUIRE_ONCE: + return $visitor->visitExecRequireOnce($this->node); + default: + throw new AssertionError("All flags must match. Found " . self::flagDescription($this->node)); + } + } + + /** + * Accepts a visitor that differentiates on the flag value + * of the AST node of kind ast\AST_MAGIC_CONST. + * + * @return mixed - The type depends on the subclass of FlagVisitor + * @suppress PhanUnreferencedPublicMethod + */ + public function acceptMagicFlagVisitor(FlagVisitor $visitor) + { + switch ($this->node->flags) { + case flags\MAGIC_CLASS: + return $visitor->visitMagicClass($this->node); + case flags\MAGIC_DIR: + return $visitor->visitMagicDir($this->node); + case flags\MAGIC_FILE: + return $visitor->visitMagicFile($this->node); + case flags\MAGIC_FUNCTION: + return $visitor->visitMagicFunction($this->node); + case flags\MAGIC_LINE: + return $visitor->visitMagicLine($this->node); + case flags\MAGIC_METHOD: + return $visitor->visitMagicMethod($this->node); + case flags\MAGIC_NAMESPACE: + return $visitor->visitMagicNamespace($this->node); + case flags\MAGIC_TRAIT: + return $visitor->visitMagicTrait($this->node); + default: + throw new AssertionError("All flags must match. Found " . self::flagDescription($this->node)); + } + } + + /** + * Accepts a visitor that differentiates on the flag value + * of the AST node of kind ast\AST_USE. + * + * @return mixed - The type depends on the subclass of FlagVisitor + * @suppress PhanUnreferencedPublicMethod + */ + public function acceptUseFlagVisitor(FlagVisitor $visitor) + { + switch ($this->node->flags) { + case flags\USE_CONST: + return $visitor->visitUseConst($this->node); + case flags\USE_FUNCTION: + return $visitor->visitUseFunction($this->node); + case flags\USE_NORMAL: + return $visitor->visitUseNormal($this->node); + default: + throw new AssertionError("All flags must match. Found " . self::flagDescription($this->node)); + } + } + + /** + * Helper method to get a tag describing the flags for a given Node kind. + */ + public static function flagDescription(Node $node): string + { + return Debug::astFlagDescription($node->flags ?? 0, $node->kind); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/Visitor/FlagVisitor.php b/bundled-libs/phan/phan/src/Phan/AST/Visitor/FlagVisitor.php new file mode 100644 index 000000000..4b6201629 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/Visitor/FlagVisitor.php @@ -0,0 +1,378 @@ +visit($node); + } + + public function visitBinaryBitwiseAnd(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryBitwiseOr(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryBitwiseXor(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryBoolXor(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryConcat(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryDiv(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryIsEqual(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryIsIdentical(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryIsNotEqual(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryIsNotIdentical(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryIsSmaller(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryIsSmallerOrEqual(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryMod(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryMul(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryPow(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryShiftLeft(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryShiftRight(Node $node) + { + return $this->visit($node); + } + + public function visitBinarySpaceship(Node $node) + { + return $this->visit($node); + } + + public function visitBinarySub(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryBoolAnd(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryBoolOr(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryCoalesce(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryIsGreater(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryIsGreaterOrEqual(Node $node) + { + return $this->visit($node); + } + + public function visitClassAbstract(Node $node) + { + return $this->visit($node); + } + + public function visitClassFinal(Node $node) + { + return $this->visit($node); + } + + public function visitClassInterface(Node $node) + { + return $this->visit($node); + } + + public function visitClassTrait(Node $node) + { + return $this->visit($node); + } + + public function visitModifierAbstract(Node $node) + { + return $this->visit($node); + } + + public function visitModifierFinal(Node $node) + { + return $this->visit($node); + } + + public function visitModifierPrivate(Node $node) + { + return $this->visit($node); + } + + public function visitModifierProtected(Node $node) + { + return $this->visit($node); + } + + public function visitModifierPublic(Node $node) + { + return $this->visit($node); + } + + public function visitModifierStatic(Node $node) + { + return $this->visit($node); + } + + public function visitNameFq(Node $node) + { + return $this->visit($node); + } + + public function visitNameNotFq(Node $node) + { + return $this->visit($node); + } + + public function visitNameRelative(Node $node) + { + return $this->visit($node); + } + + public function visitParamRef(Node $node) + { + return $this->visit($node); + } + + public function visitParamVariadic(Node $node) + { + return $this->visit($node); + } + + public function visitReturnsRef(Node $node) + { + return $this->visit($node); + } + + public function visitUnionTypeArray(Node $node) + { + return $this->visit($node); + } + + public function visitUnionTypeBool(Node $node) + { + return $this->visit($node); + } + + public function visitUnionTypeCallable(Node $node) + { + return $this->visit($node); + } + + public function visitUnionTypeDouble(Node $node) + { + return $this->visit($node); + } + + public function visitUnionTypeLong(Node $node) + { + return $this->visit($node); + } + + public function visitUnionTypeNull(Node $node) + { + return $this->visit($node); + } + + public function visitUnionTypeFalse(Node $node) + { + return $this->visit($node); + } + + public function visitUnionTypeStatic(Node $node) + { + return $this->visit($node); + } + + + public function visitUnionTypeObject(Node $node) + { + return $this->visit($node); + } + + public function visitUnionTypeString(Node $node) + { + return $this->visit($node); + } + + public function visitUnaryBitwiseNot(Node $node) + { + return $this->visit($node); + } + + public function visitUnaryBoolNot(Node $node) + { + return $this->visit($node); + } + + public function visitClassAnonymous(Node $node) + { + return $this->visit($node); + } + + public function visitExecEval(Node $node) + { + return $this->visit($node); + } + + public function visitExecInclude(Node $node) + { + return $this->visit($node); + } + + public function visitExecIncludeOnce(Node $node) + { + return $this->visit($node); + } + + public function visitExecRequire(Node $node) + { + return $this->visit($node); + } + + public function visitExecRequireOnce(Node $node) + { + return $this->visit($node); + } + + public function visitMagicClass(Node $node) + { + return $this->visit($node); + } + + public function visitMagicDir(Node $node) + { + return $this->visit($node); + } + + public function visitMagicFile(Node $node) + { + return $this->visit($node); + } + + public function visitMagicFunction(Node $node) + { + return $this->visit($node); + } + + public function visitMagicLine(Node $node) + { + return $this->visit($node); + } + + public function visitMagicMethod(Node $node) + { + return $this->visit($node); + } + + public function visitMagicNamespace(Node $node) + { + return $this->visit($node); + } + + public function visitMagicTrait(Node $node) + { + return $this->visit($node); + } + + /** + * Visit a node with kind `ast\AST_UNARY_OP` and flags `ast\flags\UNARY_MINUS` + */ + public function visitUnaryMinus(Node $node) + { + return $this->visit($node); + } + + /** + * Visit a node with kind `ast\AST_UNARY_OP` and flags `ast\flags\UNARY_PLUS` + */ + public function visitUnaryPlus(Node $node) + { + return $this->visit($node); + } + + /** + * Visit a node with kind `ast\AST_UNARY_OP` and flags `ast\flags\UNARY_SILENCE` + */ + public function visitUnarySilence(Node $node) + { + return $this->visit($node); + } + + public function visitUseConst(Node $node) + { + return $this->visit($node); + } + + public function visitUseFunction(Node $node) + { + return $this->visit($node); + } + + public function visitUseNormal(Node $node) + { + return $this->visit($node); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/AST/Visitor/KindVisitor.php b/bundled-libs/phan/phan/src/Phan/AST/Visitor/KindVisitor.php new file mode 100644 index 000000000..7193d1f61 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/AST/Visitor/KindVisitor.php @@ -0,0 +1,536 @@ +kind] ?? 'handleMissingNodeKind'; + return $this->{$fn_name}($node); + } + + /** + * @suppress PhanUnreferencedPublicMethod + * @suppress PhanPluginRemoveDebugAny deliberate warning for unhandled node kind + */ + public function handleMissingNodeKind(Node $node) + { + \fprintf(STDERR, "Unexpected Node kind. Node:\n%s\n", Debug::nodeToString($node)); + throw new AssertionError('All node kinds must match'); + } + + public function visitArgList(Node $node) + { + return $this->visit($node); + } + + public function visitArray(Node $node) + { + return $this->visit($node); + } + + public function visitArrayElem(Node $node) + { + return $this->visit($node); + } + + public function visitArrowFunc(Node $node) + { + return $this->visit($node); + } + + public function visitAssign(Node $node) + { + return $this->visit($node); + } + + public function visitAssignOp(Node $node) + { + return $this->visit($node); + } + + public function visitAssignRef(Node $node) + { + return $this->visit($node); + } + + // Attributes require AST version 80 + public function visitAttribute(Node $node) + { + return $this->visit($node); + } + + public function visitAttributeList(Node $node) + { + return $this->visit($node); + } + + public function visitAttributeGroup(Node $node) + { + return $this->visit($node); + } + + public function visitBinaryOp(Node $node) + { + return $this->visit($node); + } + + public function visitBreak(Node $node) + { + return $this->visit($node); + } + + public function visitCall(Node $node) + { + return $this->visit($node); + } + + public function visitCast(Node $node) + { + return $this->visit($node); + } + + public function visitCatch(Node $node) + { + return $this->visit($node); + } + + public function visitClass(Node $node) + { + return $this->visit($node); + } + + public function visitClassConst(Node $node) + { + return $this->visit($node); + } + + public function visitClassConstDecl(Node $node) + { + return $this->visit($node); + } + + public function visitClassConstGroup(Node $node) + { + return $this->visit($node); + } + + public function visitClassName(Node $node) + { + return $this->visit($node); + } + + public function visitClosure(Node $node) + { + return $this->visit($node); + } + + public function visitClosureUses(Node $node) + { + return $this->visit($node); + } + + public function visitClosureVar(Node $node) + { + return $this->visit($node); + } + + public function visitConst(Node $node) + { + return $this->visit($node); + } + + public function visitConstDecl(Node $node) + { + return $this->visit($node); + } + + public function visitConstElem(Node $node) + { + return $this->visit($node); + } + + public function visitDeclare(Node $node) + { + return $this->visit($node); + } + + public function visitDim(Node $node) + { + return $this->visit($node); + } + + public function visitDoWhile(Node $node) + { + return $this->visit($node); + } + + public function visitEcho(Node $node) + { + return $this->visit($node); + } + + public function visitEmpty(Node $node) + { + return $this->visit($node); + } + + public function visitEncapsList(Node $node) + { + return $this->visit($node); + } + + public function visitExit(Node $node) + { + return $this->visit($node); + } + + public function visitExprList(Node $node) + { + return $this->visit($node); + } + + public function visitForeach(Node $node) + { + return $this->visit($node); + } + + public function visitFuncDecl(Node $node) + { + return $this->visit($node); + } + + public function visitIsset(Node $node) + { + return $this->visit($node); + } + + public function visitGlobal(Node $node) + { + return $this->visit($node); + } + + public function visitGroupUse(Node $node) + { + return $this->visit($node); + } + + public function visitIf(Node $node) + { + return $this->visit($node); + } + + public function visitIfElem(Node $node) + { + return $this->visit($node); + } + + public function visitInstanceof(Node $node) + { + return $this->visit($node); + } + + public function visitMagicConst(Node $node) + { + return $this->visit($node); + } + + public function visitMethod(Node $node) + { + return $this->visit($node); + } + + public function visitMethodCall(Node $node) + { + return $this->visit($node); + } + + public function visitName(Node $node) + { + return $this->visit($node); + } + + public function visitNamedArg(Node $node) + { + return $this->visit($node); + } + + public function visitNamespace(Node $node) + { + return $this->visit($node); + } + + public function visitNew(Node $node) + { + return $this->visit($node); + } + + public function visitNullsafeMethodCall(Node $node) + { + return $this->visit($node); + } + + public function visitNullsafeProp(Node $node) + { + return $this->visit($node); + } + + public function visitParam(Node $node) + { + return $this->visit($node); + } + + public function visitParamList(Node $node) + { + return $this->visit($node); + } + + public function visitPreInc(Node $node) + { + return $this->visit($node); + } + + public function visitPrint(Node $node) + { + return $this->visit($node); + } + + public function visitProp(Node $node) + { + return $this->visit($node); + } + + public function visitPropGroup(Node $node) + { + return $this->visit($node); + } + + public function visitPropDecl(Node $node) + { + return $this->visit($node); + } + + public function visitPropElem(Node $node) + { + return $this->visit($node); + } + + public function visitReturn(Node $node) + { + return $this->visit($node); + } + + public function visitStatic(Node $node) + { + return $this->visit($node); + } + + public function visitStaticCall(Node $node) + { + return $this->visit($node); + } + + public function visitStaticProp(Node $node) + { + return $this->visit($node); + } + + public function visitStmtList(Node $node) + { + return $this->visit($node); + } + + public function visitSwitch(Node $node) + { + return $this->visit($node); + } + + public function visitSwitchCase(Node $node) + { + return $this->visit($node); + } + + public function visitSwitchList(Node $node) + { + return $this->visit($node); + } + + public function visitMatch(Node $node) + { + return $this->visit($node); + } + + public function visitMatchArm(Node $node) + { + return $this->visit($node); + } + + public function visitMatchArmList(Node $node) + { + return $this->visit($node); + } + + public function visitType(Node $node) + { + return $this->visit($node); + } + + public function visitTypeUnion(Node $node) + { + return $this->visit($node); + } + + public function visitNullableType(Node $node) + { + return $this->visit($node); + } + + public function visitUnaryOp(Node $node) + { + return $this->visit($node); + } + + public function visitUse(Node $node) + { + return $this->visit($node); + } + + public function visitUseElem(Node $node) + { + return $this->visit($node); + } + + public function visitUseTrait(Node $node) + { + return $this->visit($node); + } + + public function visitVar(Node $node) + { + return $this->visit($node); + } + + public function visitWhile(Node $node) + { + return $this->visit($node); + } + + public function visitCatchList(Node $node) + { + return $this->visit($node); + } + + public function visitClone(Node $node) + { + return $this->visit($node); + } + + public function visitConditional(Node $node) + { + return $this->visit($node); + } + + public function visitContinue(Node $node) + { + return $this->visit($node); + } + + public function visitFor(Node $node) + { + return $this->visit($node); + } + + public function visitGoto(Node $node) + { + return $this->visit($node); + } + + public function visitHaltCompiler(Node $node) + { + return $this->visit($node); + } + + public function visitIncludeOrEval(Node $node) + { + return $this->visit($node); + } + + public function visitLabel(Node $node) + { + return $this->visit($node); + } + + public function visitMethodReference(Node $node) + { + return $this->visit($node); + } + + public function visitNameList(Node $node) + { + return $this->visit($node); + } + + public function visitPostDec(Node $node) + { + return $this->visit($node); + } + + public function visitPostInc(Node $node) + { + return $this->visit($node); + } + + public function visitPreDec(Node $node) + { + return $this->visit($node); + } + + public function visitRef(Node $node) + { + return $this->visit($node); + } + + public function visitShellExec(Node $node) + { + return $this->visit($node); + } + + public function visitThrow(Node $node) + { + return $this->visit($node); + } + + public function visitTraitAdaptations(Node $node) + { + return $this->visit($node); + } + + public function visitTraitAlias(Node $node) + { + return $this->visit($node); + } + + public function visitTraitPrecedence(Node $node) + { + return $this->visit($node); + } + + public function visitTry(Node $node) + { + return $this->visit($node); + } + + public function visitUnpack(Node $node) + { + return $this->visit($node); + } + + public function visitUnset(Node $node) + { + return $this->visit($node); + } + + public function visitYield(Node $node) + { + return $this->visit($node); + } + + public function visitYieldFrom(Node $node) + { + return $this->visit($node); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis.php b/bundled-libs/phan/phan/src/Phan/Analysis.php new file mode 100644 index 000000000..9b4729293 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis.php @@ -0,0 +1,589 @@ +setCurrentParsedFile($file_path); + if ($is_php_internal_stub) { + /** @see \Phan\Language\FileRef::isPHPInternal() */ + $file_path = 'internal'; + } + $context = (new Context())->withFile($file_path); + + // Convert the file to an Abstract Syntax Tree + // before passing it on to the recursive version + // of this method + + $real_file_path = Config::projectPath($original_file_path); + if (\is_string($override_contents)) { + // TODO: Make $override_contents a persistent entry in FileCache, make Request and language server manage this + $cache_entry = FileCache::addEntry($real_file_path, $override_contents); + } else { + $cache_entry = FileCache::getOrReadEntry($real_file_path); + } + $file_contents = $cache_entry->getContents(); + if ($file_contents === '') { + if ($is_php_internal_stub) { + throw new InvalidArgumentException("Unexpected empty php file for autoload_internal_extension_signatures: path=" . StringUtil::jsonEncode($original_file_path)); + } + // php-ast would return null for 0 byte files as an implementation detail. + // Make Phan consistently emit this warning. + Issue::maybeEmit( + $code_base, + $context, + Issue::EmptyFile, + 0, + $original_file_path + ); + + return $context; + } + // TODO: Figure out why Phan doesn't suggest combining these catches except in language server mode + try { + $node = Parser::parseCode($code_base, $context, $request, $file_path, $file_contents, $suppress_parse_errors); + } catch (ParseError | CompileError | ParseException $_) { + return $context; + } + + if (Config::getValue('dump_ast')) { + // @phan-file-suppress PhanPluginRemoveDebugEcho + echo $file_path . "\n" + . \str_repeat("\u{00AF}", strlen($file_path)) + . "\n"; + Debug::printNode($node); + return $context; + } + + if (Config::getValue('simplify_ast')) { + try { + // Transform the original AST, and if successful, then analyze the new AST instead of the original + $node = ASTSimplifier::applyStatic($node); + } catch (\Exception $e) { + Issue::maybeEmit( + $code_base, + $context, + Issue::SyntaxError, // Not the right kind of error. I don't think it would throw, anyway. + $e->getLine(), + $e->getMessage() + ); + } + } + + $context = self::parseNodeInContext( + $code_base, + $context, + $node + ); + // @phan-suppress-next-line PhanAccessMethodInternal + $code_base->addParsedNamespaceMap($context->getFile(), $context->getNamespace(), $context->getNamespaceId(), $context->getNamespaceMap()); + return $context; + } + + /** + * Parse the given node in the given context populating + * the code base within the context as a side effect. The + * returned context is the new context from within the + * given node. + * + * + * @param CodeBase $code_base + * The global code base in which we store all + * state + * + * @param Context $context + * The context in which this node exists + * + * @param Node $node + * A node to parse and scan for errors + * + * @return Context + * The context from within the node is returned + * @suppress PhanPluginCanUseReturnType + * NOTE: This is called extremely frequently, so the real signature types were omitted for performance. + */ + public static function parseNodeInContext(CodeBase $code_base, Context $context, Node $node) + { + $kind = $node->kind; + $context->setLineNumberStart($node->lineno); + + // Visit the given node populating the code base + // with anything we learn and get a new context + // indicating the state of the world within the + // given node. + // NOTE: This is called extremely frequently + // (E.g. on a large number of the analyzed project's vendor dependencies, + // proportionally to the node count in the files), so code style was sacrificed for performance. + // Equivalent to (new ParseVisitor(...))($node), which uses ParseVisitor->__invoke + $inner_context = (new ParseVisitor( + $code_base, + $context + ))->{Element::VISIT_LOOKUP_TABLE[$kind] ?? 'handleMissingNodeKind'}($node); + + // ast\AST_GROUP_USE has ast\AST_USE as a child. + // We don't want to use block twice in the parse phase. + // (E.g. `use MyNS\{const A, const B}` would lack the MyNs part if this were to recurse. + // And ast\AST_DECLARE has AST_CONST_DECL as a child, so don't parse a constant declaration either. + if ($kind === ast\AST_GROUP_USE) { + return $inner_context; + } + if ($kind === ast\AST_DECLARE) { + // Check for class declarations, etc. within the statements of a declare directive. + $child_node = $node->children['stmts']; + if (\is_object($child_node)) { + // Step into each child node and get an + // updated context for the node + return self::parseNodeInContext($code_base, $inner_context, $child_node); + } + return $inner_context; + } + + // Recurse into each child node + $child_context = $inner_context; + foreach ($node->children as $child_node) { + // Skip any non Node children. + if (\is_object($child_node)) { + // Step into each child node and get an + // updated context for the node + $child_context = self::parseNodeInContext($code_base, $child_context, $child_node); + } + } + + switch ($kind) { + case ast\AST_CLASS: + case ast\AST_METHOD: + case ast\AST_FUNC_DECL: + case ast\AST_ARROW_FUNC: + case ast\AST_CLOSURE: + // For closed context elements (that have an inner scope) + // return the outer context instead of their inner context + // after we finish parsing their children. + return $context; + case ast\AST_STMT_LIST: + // Workaround that ensures that the context from namespace blocks gets passed to the caller. + return $child_context; + default: + // Pass the context back up to our parent + return $inner_context; + } + } + + /** + * Take a pass over all functions verifying various states. + * + * @param ?array $file_filter if non-null, limit analysis to functions and methods declared in this array + */ + public static function analyzeFunctions(CodeBase $code_base, array $file_filter = null): void + { + $plugin_set = ConfigPluginSet::instance(); + $has_function_or_method_plugins = $plugin_set->hasAnalyzeFunctionPlugins() || $plugin_set->hasAnalyzeMethodPlugins(); + $show_progress = CLI::shouldShowProgress(); + $analyze_function_or_method = static function (FunctionInterface $function_or_method) use ( + $code_base, + $plugin_set, + $has_function_or_method_plugins, + $file_filter + ): void { + if ($function_or_method->isPHPInternal()) { + return; + } + // Phan always has to call this, to add default values to types of parameters. + $function_or_method->ensureScopeInitialized($code_base); + + // If there is an array limiting the set of files, skip this file if it's not in the list. + if (\is_array($file_filter) && !isset($file_filter[$function_or_method->getContext()->getFile()])) { + return; + } + + DuplicateFunctionAnalyzer::analyzeDuplicateFunction( + $code_base, + $function_or_method + ); + + // This is the most time consuming step. + // Can probably apply this to other functions, but this was the slowest. + ParameterTypesAnalyzer::analyzeParameterTypes( + $code_base, + $function_or_method + ); + + // Infer more accurate return types + // For daemon mode/the language server, we also call this whenever we use the return type of a function/method. + $function_or_method->analyzeReturnTypes($code_base); + + ThrowsTypesAnalyzer::analyzeThrowsTypes( + $code_base, + $function_or_method + ); + // Let any plugins analyze the methods or functions + // XXX: Add a way to run plugins on all functions/methods, this was limited for speed. + // Assumes that the given plugins will emit an issue in the same file as the function/method, + // which isn't necessarily the case. + // 0.06 + if ($has_function_or_method_plugins) { + if ($function_or_method instanceof Func) { + $plugin_set->analyzeFunction( + $code_base, + $function_or_method + ); + } elseif ($function_or_method instanceof Method) { + $plugin_set->analyzeMethod( + $code_base, + $function_or_method + ); + } + } + }; + + // Analyze user-defined method declarations. + // Plugins may also analyze user-defined methods here. + $i = 0; + CLI::progress('function', 0.0, null); + $function_map = $code_base->getFunctionMap(); + foreach ($function_map as $function) { // iterate, ignoring $fqsen + if ($show_progress) { + CLI::progress('function', (++$i) / (count($function_map)), $function); + } + $analyze_function_or_method($function); + } + + // Analyze user-defined method declarations. + // Plugins may also analyze user-defined methods here. + $i = 0; + $method_set = $code_base->getMethodSet(); + CLI::progress('method', 0.0, null); + foreach ($method_set as $method) { + if ($show_progress) { + // I suspect that method analysis is hydrating some of the classes, + // adding even more inherited methods to the end of the set. + // This recalculation is needed so that the progress bar is accurate. + CLI::progress('method', (++$i) / (count($method_set)), $method); + } + $analyze_function_or_method($method); + } + } + + /** + * Loads extra logic for analyzing function and method calls. + * @suppress PhanPluginUnknownObjectMethodCall TODO: Fix for ArrayObject + */ + public static function loadMethodPlugins(CodeBase $code_base): void + { + $plugin_set = ConfigPluginSet::instance(); + $return_type_overrides = $plugin_set->getReturnTypeOverrides($code_base); + $return_type_override_fqsen_strings = []; + foreach ($return_type_overrides as $fqsen_string => $unused_closure) { + if (\strpos($fqsen_string, '::') !== false) { + try { + $fqsen = FullyQualifiedMethodName::fromFullyQualifiedString($fqsen_string); + } catch (FQSENException | InvalidArgumentException $e) { + // @phan-suppress-next-line PhanPluginRemoveDebugCall + \fprintf(STDERR, "getReturnTypeOverrides returned an invalid FQSEN %s: %s\n", $fqsen_string, $e->getMessage()); + continue; + } + + // The FQSEN that's actually in the code base is allowed to differ from what the plugin used as an array key. + // Thus, we use $fqsen->__toString() rather than $fqsen_string. + $return_type_override_fqsen_strings[$fqsen->__toString()] = true; + } + } + + $methods_by_defining_fqsen = null; + foreach ($return_type_overrides as $fqsen_string => $closure) { + try { + if (\strpos($fqsen_string, '::') !== false) { + $fqsen = FullyQualifiedMethodName::fromFullyQualifiedString($fqsen_string); + $class_fqsen = $fqsen->getFullyQualifiedClassName(); + // We have to call hasClassWithFQSEN before calling hasMethodWithFQSEN in order to autoload the internal function signatures. + // TODO: Move class autoloading into hasMethodWithFQSEN()? + if ($code_base->hasClassWithFQSEN($class_fqsen)) { + // This is an override of a method. + if ($code_base->hasMethodWithFQSEN($fqsen)) { + // The $fqsen_string from a plugin can refer to a method + // that is not the original definition. + $method = $code_base->getMethodByFQSEN($fqsen); + $method->setDependentReturnTypeClosure($closure); + + $methods_by_defining_fqsen = $methods_by_defining_fqsen ?? $code_base->getMethodsMapGroupedByDefiningFQSEN(); + if (!$methods_by_defining_fqsen->offsetExists($fqsen)) { + continue; + } + + // 1) The FQSEN that's actually in the code base is allowed to differ from what the plugin used as an array key. + // Thus, we use $fqsen->__toString() rather than $fqsen_string. + // 2) The parent method is included in this list, i.e. the parent method is its own defining method. + foreach ($methods_by_defining_fqsen->offsetGet($fqsen) as $child_method) { + if ($child_method->getFQSEN() !== $fqsen && + isset($return_type_override_fqsen_strings[$child_method->getFQSEN()->__toString()]) + ) { + // An override closure targeting SubClass::foo should take precedence over BaseClass::foo + // even if the definition was BaseClass::foo + continue; + } + $child_method->setDependentReturnTypeClosure($closure); + } + } + } + } else { + // This is an override of a function. + $fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString($fqsen_string); + if ($code_base->hasFunctionWithFQSEN($fqsen)) { + $function = $code_base->getFunctionByFQSEN($fqsen); + $function->setDependentReturnTypeClosure($closure); + } + } + } catch (FQSENException | InvalidArgumentException $e) { + // @phan-suppress-next-line PhanPluginRemoveDebugCall + \fprintf(STDERR, "getReturnTypeOverrides returned an invalid FQSEN %s: %s\n", $fqsen_string, $e->getMessage()); + } + } + + foreach ($plugin_set->getAnalyzeFunctionCallClosures($code_base) as $fqsen_string => $closure) { + try { + if (\strpos($fqsen_string, '::') !== false) { + // This is an override of a method. + [$class, $method_name] = \explode('::', $fqsen_string, 2); + $class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString($class); + if (!$code_base->hasClassWithFQSEN($class_fqsen)) { + continue; + } + $class = $code_base->getClassByFQSEN($class_fqsen); + // Note: This is used because it will create methods such as __construct if they do not exist. + if ($class->hasMethodWithName($code_base, $method_name, false)) { + $method = $class->getMethodByName($code_base, $method_name); + $method->addFunctionCallAnalyzer($closure); + + $methods_by_defining_fqsen = $methods_by_defining_fqsen ?? $code_base->getMethodsMapGroupedByDefiningFQSEN(); + $fqsen = FullyQualifiedMethodName::fromFullyQualifiedString($fqsen_string); + if (!$methods_by_defining_fqsen->offsetExists($fqsen)) { + continue; + } + + foreach ($methods_by_defining_fqsen->offsetGet($fqsen) as $child_method) { + $child_method->addFunctionCallAnalyzer($closure); + } + } + } else { + // This is an override of a function. + $fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString($fqsen_string); + if ($code_base->hasFunctionWithFQSEN($fqsen)) { + $function = $code_base->getFunctionByFQSEN($fqsen); + $function->setFunctionCallAnalyzer($closure); + } + } + } catch (FQSENException | InvalidArgumentException $e) { + // @phan-suppress-next-line PhanPluginRemoveDebugCall + \fprintf(STDERR, "getAnalyzeFunctionCallClosures returned an invalid FQSEN %s: %s\n", $fqsen_string, $e->getMessage()); + } + } + } + + /** + * Take a pass over all classes/traits/interfaces + * verifying various states. + * + * @param ?array $path_filter if non-null, limit analysis to classes in this array + */ + public static function analyzeClasses(CodeBase $code_base, array $path_filter = null): void + { + CLI::progress('classes', 0.0, null); + $classes = $code_base->getUserDefinedClassMap(); + if (\is_array($path_filter)) { + // If a list of files is provided, then limit analysis to classes defined in those files. + $old_classes = $classes; + $classes = []; + foreach ($old_classes as $class) { + if (isset($path_filter[$class->getContext()->getFile()])) { + $classes[] = $class; + } + } + } + $i = 0; + foreach ($classes as $class) { + CLI::progress('classes', $i++ / count($classes), null); + try { + $class->analyze($code_base); + } catch (RecursionDepthException $_) { + continue; + } + } + CLI::progress('classes', 1.0, null); + } + + /** + * Take a look at all globally accessible elements and see if + * we can find any dead code that is never referenced + */ + public static function analyzeDeadCode(CodeBase $code_base): void + { + // Check to see if dead code detection is enabled. Keep + // in mind that the results here are just a guess and + // we can't tell with certainty that anything is + // definitely unreferenced. + if (!Config::getValue('dead_code_detection')) { + return; + } + + ReferenceCountsAnalyzer::analyzeReferenceCounts($code_base); + } + + /** + * Once we know what the universe looks like we + * can scan for more complicated issues. + * + * @param CodeBase $code_base + * The global code base holding all state + * + * @param ?Request $request + * A daemon mode request if in daemon mode. May affect the parser used for $file_path + * + * @param ?string $override_contents + * If this is not null, this function will act as if $file_path's contents + * were $override_contents + */ + public static function analyzeFile( + CodeBase $code_base, + string $file_path, + ?Request $request, + string $override_contents = null + ): Context { + // Set the file on the context + $context = (new Context())->withFile($file_path); + // @phan-suppress-next-line PhanAccessMethodInternal + $context->importNamespaceMapFromParsePhase($code_base); + + $code_base->setCurrentAnalyzedFile($file_path); + + // Convert the file to an Abstract Syntax Tree + // before passing it on to the recursive version + // of this method + try { + $real_file_path = Config::projectPath($file_path); + if (\is_string($override_contents)) { + $cache_entry = FileCache::addEntry($real_file_path, $override_contents); + } else { + $cache_entry = FileCache::getOrReadEntry($real_file_path); + } + $file_contents = $cache_entry->getContents(); + if ($file_contents === '') { + // php-ast would return null for 0 byte files as an implementation detail. + // Make Phan consistently emit this warning. + Issue::maybeEmit( + $code_base, + $context, + Issue::EmptyFile, + 0, + $file_path + ); + + return $context; + } + $node = Parser::parseCode($code_base, $context, $request, $file_path, $file_contents, false); + } catch (ParseException | ParseError | CompileError $_) { + // Issue::SyntaxError was already emitted. + return $context; + } + + if (Config::getValue('simplify_ast')) { + try { + // Transform the original AST, and if successful, then analyze the new AST instead of the original + $node = ASTSimplifier::applyStatic($node); + } catch (\Exception $e) { + // Not the right kind of Issue to show to the user. I don't think it would throw, anyway. + self::emitSyntaxError($code_base, $context, $e); + } + } + PhanAnnotationAdder::applyFull($node); + + ConfigPluginSet::instance()->beforeAnalyzeFile($code_base, $context, $file_contents, $node); + + $context = (new BlockAnalysisVisitor($code_base, $context))($node); + // @phan-suppress-next-line PhanAccessMethodInternal + $context->warnAboutUnusedUseElements($code_base); + + ConfigPluginSet::instance()->afterAnalyzeFile($code_base, $context, $file_contents, $node); + return $context; + } + + private static function emitSyntaxError( + CodeBase $code_base, + Context $context, + Throwable $e + ): void { + Issue::maybeEmit( + $code_base, + $context, + Issue::SyntaxError, + $e->getLine(), + $e->getMessage() + ); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/AbstractMethodAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/AbstractMethodAnalyzer.php new file mode 100644 index 000000000..8045ccb47 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/AbstractMethodAnalyzer.php @@ -0,0 +1,78 @@ +isPHPInternal()) { + return; + } + // Don't worry about traits or abstract classes, those can have abstract methods + if ($class->isAbstract() || $class->isTrait() || $class->isInterface()) { + return; + } + foreach ($class->getMethodMap($code_base) as $method) { + if ($method->isAbstract()) { + if ($method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $class->getContext(), + Issue::ClassContainsAbstractMethodInternal, + $class->getFileRef()->getLineNumberStart(), + (string)$class->getFQSEN(), + self::toRealSignature($method) + ); + } else { + Issue::maybeEmit( + $code_base, + $class->getContext(), + Issue::ClassContainsAbstractMethod, + $class->getFileRef()->getLineNumberStart(), + (string)$class->getFQSEN(), + self::toRealSignature($method), + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } + } + } + } + + private static function toRealSignature(Method $method): string + { + $fqsen = $method->getDefiningFQSEN(); + $result = \sprintf( + "%s::%s%s(%s)", + (string) $fqsen->getFullyQualifiedClassName(), + $method->returnsRef() ? '&' : '', + $fqsen->getName(), + $method->getRealParameterStubText() + ); + $return_type = $method->getRealReturnType(); + if (!$return_type->isEmpty()) { + $result .= ': ' . $return_type; + } + + return $result; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/Analyzable.php b/bundled-libs/phan/phan/src/Phan/Analysis/Analyzable.php new file mode 100644 index 000000000..f7fc5f6ce --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/Analyzable.php @@ -0,0 +1,164 @@ +node = $node; + } + + /** + * @return bool + * True if we have a node defined on this object + */ + public function hasNode(): bool + { + return $this->node !== null; + } + + /** + * @return Node + * The AST node associated with this object + * NOTE: This is non-null if hasNode is true + * @suppress PhanTypeMismatchDeclaredReturnNullable + */ + public function getNode(): ?Node + { + return $this->node; + } + + /** + * Clears the node so that it won't be used for analysis. + * @suppress PhanTypeMismatchPropertyProbablyReal + */ + protected function clearNode(): void + { + $this->node = null; + } + + /** + * Ensure that annotations about what flags a function declaration has have been added + * @suppress PhanUndeclaredProperty deliberately using dynamic properties + */ + public static function ensureDidAnnotate(Node $node): void + { + if (!isset($node->did_annotate_node)) { + // Set this to true to indicate that this node has already + // been annotated with any extra information + // from the class. + // (Nodes for a FunctionInterface can be both from the parse phase and the analysis phase) + $node->did_annotate_node = true; + PhanAnnotationAdder::applyToScope($node); + } + } + + /** + * @return Context + * Analyze the node associated with this object + * in the given context + * @suppress PhanUnreferencedPublicMethod phan has issues with dead code detection with traits and interfaces + */ + public function analyze(Context $context, CodeBase $code_base): Context + { + // Don't do anything if we care about being + // fast + if (Config::get_quick_mode()) { + return $context; + } + + $definition_node = $this->node; + if (!$definition_node) { + return $context; + } + self::ensureDidAnnotate($definition_node); + + // Closures depend on the context surrounding them such + // as for getting `use(...)` variables. Since we don't + // have them, we can't re-analyze them until we change + // that. + // + // TODO: Store the parent context on Analyzable objects + if ($definition_node->kind === \ast\AST_CLOSURE) { + // TODO: Pick up 'uses' when this is a closure invoked inline (e.g. array_map(function($x) use($localVar) {...}, args + // TODO: Investigate replacing the types of these with 'mixed' for quick mode re-analysis, or checking if the type will never vary. + if (isset($definition_node->children['uses'])) { + return $context; + } + } + // Stop upon reaching the maximum depth + if (self::$recursion_depth >= self::getMaxRecursionDepth()) { + return $context; + } + + self::$recursion_depth++; + + try { + // Analyze the node in a cloned context so that we + // don't overwrite anything + return (new BlockAnalysisVisitor($code_base, clone($context)))->__invoke( + $definition_node + ); + } finally { + self::$recursion_depth--; + } + } + + /** + * Gets the recursion depth. Starts at 0, increases the deeper the recursion goes + */ + public function getRecursionDepth(): int + { + return self::$recursion_depth; + } + + /** + * Gets the maximum recursion depth. + */ + public static function getMaxRecursionDepth(): int + { + return Config::getValue('maximum_recursion_depth'); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ArgumentType.php b/bundled-libs/phan/phan/src/Phan/Analysis/ArgumentType.php new file mode 100644 index 000000000..7b17587b0 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ArgumentType.php @@ -0,0 +1,1470 @@ +kind === ast\AST_STATIC_CALL && $method instanceof Method) { + if ($method->isAbstract() && $method->isStatic()) { + self::checkAbstractStaticMethodCall($method, $node, $context, $code_base); + } + } + self::checkIsDeprecatedOrInternal($code_base, $context, $method); + if ($method->hasFunctionCallAnalyzer()) { + try { + $method->analyzeFunctionCall($code_base, $context->withLineNumberStart($node->lineno), $node->children['args']->children, $node); + } catch (StopParamAnalysisException $_) { + return; + } + } + + // Emit an issue if this is an externally accessed internal method + $arglist = $node->children['args']; + $argcount = \count($arglist->children); + + // Make sure we have enough arguments + if ($argcount < $method->getNumberOfRequiredParameters() && !self::isUnpack($arglist->children)) { + $alternate_found = false; + foreach ($method->alternateGenerator($code_base) as $alternate_method) { + $alternate_found = $alternate_found || ( + $argcount >= + $alternate_method->getNumberOfRequiredParameters() + ); + } + + if (!$alternate_found) { + if ($method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::ParamTooFewInternal, + $node->lineno, + $argcount, + $method->getRepresentationForIssue(true), + $method->getNumberOfRequiredParameters() + ); + } else { + Issue::maybeEmit( + $code_base, + $context, + Issue::ParamTooFew, + $node->lineno ?? 0, + $argcount, + $method->getRepresentationForIssue(true), + $method->getNumberOfRequiredParameters(), + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } + } + } + + // Make sure we don't have too many arguments + if ($argcount > $method->getNumberOfParameters() && !self::isVarargs($code_base, $method)) { + $alternate_found = false; + foreach ($method->alternateGenerator($code_base) as $alternate_method) { + if ($argcount <= $alternate_method->getNumberOfParameters()) { + $alternate_found = true; + break; + } + } + + if (!$alternate_found) { + self::emitParamTooMany($code_base, $context, $method, $node, $argcount); + } + } + + // Check the parameter types + self::analyzeParameterList( + $code_base, + $method, + $arglist, + $context + ); + } + + /** + * @param FunctionInterface $method + * The function/method we're analyzing arguments for + * + * @param Node $node + * The node of kind AST_STATIC_CALL holding the method call we're looking at + * + * @param Context $context + * The context in which we see the call + * + * @param CodeBase $code_base + * The global code base + */ + private static function checkAbstractStaticMethodCall( + FunctionInterface $method, + Node $node, + Context $context, + CodeBase $code_base + ): void { + $class_node = $node->children['class']; + $issue_type = Issue::AbstractStaticMethodCall; + if ($class_node->kind === ast\AST_NAME) { + if ($context->isInMethodScope()) { + $caller = $context->getFunctionLikeInScope($code_base); + $name = $class_node->children['name']; + if (\is_string($name)) { + switch (\strtolower($name)) { + case 'static': + if (!$caller->isStatic()) { + return; + } + $issue_type = Issue::AbstractStaticMethodCallInStatic; + // fallthrough + case 'self': + // Note: an abstract class can use a trait, so self::abstractMethod() can still be abstract within instance methods. + if ($caller instanceof Method) { + $class = $caller->getClass($code_base); + if ($class->isTrait()) { + $issue_type = Issue::AbstractStaticMethodCallInTrait; + } + } + break; + } + } + } + } else { + try { + $class_type = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $class_node + ); + } catch (Exception $_) { + return; + } + if ($class_type->isEmpty() || $class_type->hasPossiblyObjectTypes()) { + return; + } + } + Issue::maybeEmit( + $code_base, + $context, + $issue_type, + $node->lineno, + $method->getRepresentationForIssue(), + ASTReverter::toShortString($node) + ); + } + + private static function emitParamTooMany( + CodeBase $code_base, + Context $context, + FunctionInterface $method, + Node $node, + int $argcount + ): void { + $max = $method->getNumberOfParameters(); + $caused_by_variadic = $argcount === $max + 1 && (\end($node->children['args']->children)->kind ?? null) === ast\AST_UNPACK; + if ($method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $context, + $caused_by_variadic ? Issue::ParamTooManyUnpackInternal : Issue::ParamTooManyInternal, + $node->lineno ?? 0, + $caused_by_variadic ? $max : $argcount, + $method->getRepresentationForIssue(true), + $max + ); + } else { + Issue::maybeEmit( + $code_base, + $context, + $caused_by_variadic ? Issue::ParamTooManyUnpack : Issue::ParamTooMany, + $node->lineno ?? 0, + $caused_by_variadic ? $max : $argcount, + $method->getRepresentationForIssue(true), + $max, + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } + } + + private static function checkIsDeprecatedOrInternal(CodeBase $code_base, Context $context, FunctionInterface $method): void + { + // Special common cases where we want slightly + // better multi-signature error messages + if ($method->isPHPInternal()) { + // Emit an error if this internal method is marked as deprecated + if ($method->isDeprecated()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::DeprecatedFunctionInternal, + $context->getLineNumberStart(), + $method->getRepresentationForIssue(), + $method->getDeprecationReason() + ); + } + } else { + // Emit an error if this user-defined method is marked as deprecated + if ($method->isDeprecated()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::DeprecatedFunction, + $context->getLineNumberStart(), + $method->getRepresentationForIssue(), + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart(), + $method->getDeprecationReason() + ); + } + } + + // Emit an issue if this is an externally accessed internal method + if ($method->isNSInternal($code_base) + && !$method->isNSInternalAccessFromContext( + $code_base, + $context + ) + ) { + Issue::maybeEmit( + $code_base, + $context, + Issue::AccessMethodInternal, + $context->getLineNumberStart(), + $method->getRepresentationForIssue(), + $method->getElementNamespace() ?: '\\', + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart(), + ($context->getNamespace()) ?: '\\' + ); + } + } + + private static function isVarargs(CodeBase $code_base, FunctionInterface $method): bool + { + foreach ($method->alternateGenerator($code_base) as $alternate_method) { + foreach ($alternate_method->getParameterList() as $parameter) { + if ($parameter->isVariadic()) { + return true; + } + } + } + return false; + } + + /** + * Figure out if any of the arguments are a call to unpack() + * @param array $children + */ + private static function isUnpack(array $children): bool + { + foreach ($children as $child) { + if ($child instanceof Node) { + if ($child->kind === ast\AST_UNPACK) { + return true; + } + } + } + return false; + } + + /** + * @param FunctionInterface $method + * The function/method we're analyzing arguments for + * + * @param list $arg_nodes $node + * The node holding the arguments of the call we're looking at + * + * @param Context $context + * The context in which we see the call + * + * @param CodeBase $code_base + * The global code base + * + * @param Closure $get_argument_type (Node|string|int $node, int $i) -> UnionType + * Fetches the types of individual arguments. + */ + public static function analyzeForCallback( + FunctionInterface $method, + array $arg_nodes, + Context $context, + CodeBase $code_base, + Closure $get_argument_type + ): void { + // Special common cases where we want slightly + // better multi-signature error messages + self::checkIsDeprecatedOrInternal($code_base, $context, $method); + // TODO: analyzeInternalArgumentType + + $argcount = \count($arg_nodes); + + // Make sure we have enough arguments + if ($argcount < $method->getNumberOfRequiredParameters() && !self::isUnpack($arg_nodes)) { + $alternate_found = false; + foreach ($method->alternateGenerator($code_base) as $alternate_method) { + if ($argcount >= $alternate_method->getNumberOfRequiredParameters()) { + $alternate_found = true; + break; + } + } + + if (!$alternate_found) { + Issue::maybeEmit( + $code_base, + $context, + Issue::ParamTooFewCallable, + $context->getLineNumberStart(), + $argcount, + $method->getRepresentationForIssue(true), + $method->getNumberOfRequiredParameters(), + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } + } + + // Make sure we don't have too many arguments + if ($argcount > $method->getNumberOfParameters() && !self::isVarargs($code_base, $method)) { + $alternate_found = false; + foreach ($method->alternateGenerator($code_base) as $alternate_method) { + if ($argcount <= $alternate_method->getNumberOfParameters()) { + $alternate_found = true; + break; + } + } + + if (!$alternate_found) { + $max = $method->getNumberOfParameters(); + Issue::maybeEmit( + $code_base, + $context, + Issue::ParamTooManyCallable, + $context->getLineNumberStart(), + $argcount, + $method->getRepresentationForIssue(), + $max, + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } + } + + // Check the parameter types + self::analyzeParameterListForCallback( + $code_base, + $method, + $arg_nodes, + $context, + $get_argument_type + ); + } + + /** + * @param CodeBase $code_base + * The global code base + * + * @param FunctionInterface $method + * The method we're analyzing arguments for + * + * @param list $arg_nodes $node + * The node holding the arguments of the call we're looking at + * + * @param Context $context + * The context in which we see the call + * + * @param Closure $get_argument_type (Node|string|int $node, int $i) -> UnionType + */ + private static function analyzeParameterListForCallback( + CodeBase $code_base, + FunctionInterface $method, + array $arg_nodes, + Context $context, + Closure $get_argument_type + ): void { + // There's nothing reasonable we can do here + if ($method instanceof Method) { + if ($method->isMagicCall() || $method->isMagicCallStatic()) { + return; + } + } + $positions_used = null; + + foreach ($arg_nodes as $original_i => $argument) { + if (!\is_int($original_i)) { + throw new AssertionError("Expected argument index to be an integer"); + } + $i = $original_i; + if ($argument instanceof Node && $argument->kind === ast\AST_NAMED_ARG) { + ['name' => $argument_name, 'expr' => $argument_expression] = $argument->children; + if ($argument_expression === null) { + throw new AssertionError("Expected argument to have an expression"); + } + $found = false; + // TODO: Could optimize for long lists by precomputing a map, probably not worth it + foreach ($method->getRealParameterList() as $j => $parameter) { + if ($parameter->getName() === $argument_name) { + if ($parameter->isVariadic()) { + self::emitSuspiciousNamedArgumentForVariadic($code_base, $context, $method, $argument); + } + $found = true; + $i = $j; + break; + } + } + if (!isset($parameter)) { + self::emitUndeclaredNamedArgument($code_base, $context, $method, $argument); + continue; + } + + if (!$found) { + if (!$parameter->isVariadic()) { + self::emitUndeclaredNamedArgument($code_base, $context, $method, $argument); + } elseif ($method->isPHPInternal()) { + self::emitSuspiciousNamedArgumentVariadicInternal($code_base, $context, $method, $argument); + } + continue; + } + if (!\is_array($positions_used)) { + $positions_used = \array_slice($arg_nodes, 0, $original_i); + } + } else { + // Get the parameter associated with this argument + // FIXME: Use the real parameter name all the time for named arguments if it exists + $parameter = $method->getParameterForCaller($i); + $argument_expression = $argument; + } + if (\is_array($positions_used)) { + $reused_argument = $positions_used[$i] ?? null; + if ($reused_argument !== null && $parameter && !$parameter->isVariadic()) { + if ($method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::DuplicateNamedArgumentInternal, + $argument->lineno ?? $context->getLineNumberStart(), + ASTReverter::toShortString($argument), + ASTReverter::toShortString($reused_argument), + $method->getRepresentationForIssue(true) + ); + } else { + Issue::maybeEmit( + $code_base, + $context, + Issue::DuplicateNamedArgument, + $argument->lineno ?? $context->getLineNumberStart(), + ASTReverter::toShortString($argument), + ASTReverter::toShortString($reused_argument), + $method->getRepresentationForIssue(true), + $method->getContext()->getFile(), + $method->getContext()->getLineNumberStart() + ); + } + } else { + $positions_used[$i] = $argument; + } + } + + // This issue should be caught elsewhere + if (!$parameter) { + continue; + } + + // TODO: Warnings about call-by-reference are different for array_map, etc. + + // Get the type of the argument. We'll check it against + // the parameter in a moment + try { + $argument_type = $get_argument_type($argument, $i); + } catch (IssueException $e) { + Issue::maybeEmitInstance($code_base, $context, $e->getIssueInstance()); + continue; + } + $lineno = $argument->lineno ?? $context->getLineNumberStart(); + self::analyzeParameter( + $code_base, + $context, + $method, + $argument_type, + $lineno, + $i, + $argument, + new ast\Node(ast\AST_ARG_LIST, 0, $arg_nodes, $lineno) + ); + if ($parameter->isPassByReference()) { + if ($argument instanceof Node) { + // @phan-suppress-next-line PhanUndeclaredProperty this is added for analyzers + $argument->is_reference = true; + } + } + } + if (\is_array($positions_used)) { + self::checkAllNamedArgumentsPassed($code_base, $context, $context->getLineNumberStart(), $method, $positions_used); + } + } + + /** + * These node types are guaranteed to be usable as references + * @internal + */ + public const REFERENCE_NODE_KINDS = [ + ast\AST_VAR, + ast\AST_DIM, + ast\AST_PROP, + ast\AST_STATIC_PROP, + ]; + + /** + * @param CodeBase $code_base + * The global code base + * + * @param FunctionInterface $method + * The method we're analyzing arguments for + * + * @param Node $node + * The node holding the arguments of the function/method call we're looking at + * + * @param Context $context + * The context in which we see the call + */ + private static function analyzeParameterList( + CodeBase $code_base, + FunctionInterface $method, + Node $node, + Context $context + ): void { + // There's nothing reasonable we can do here + if ($method instanceof Method) { + if ($method->isMagicCall() || $method->isMagicCallStatic()) { + return; + } + } + $positions_used = null; + + foreach ($node->children as $original_i => $argument) { + if (!\is_int($original_i)) { + throw new AssertionError("Expected argument index to be an integer"); + } + $i = $original_i; + if ($argument instanceof Node && $argument->kind === ast\AST_NAMED_ARG) { + ['name' => $argument_name, 'expr' => $argument_expression] = $argument->children; + if ($argument_expression === null) { + throw new AssertionError("Expected argument to have an expression"); + } + $found = false; + // TODO: Could optimize for long lists by precomputing a map, probably not worth it + foreach ($method->getRealParameterList() as $j => $parameter) { + if ($parameter->getName() === $argument_name) { + if ($parameter->isVariadic()) { + self::emitSuspiciousNamedArgumentForVariadic($code_base, $context, $method, $argument); + } + $found = true; + $i = $j; + break; + } + } + + if (!isset($parameter)) { + self::emitUndeclaredNamedArgument($code_base, $context, $method, $argument); + continue; + } + if (!$found) { + if (!$parameter->isVariadic()) { + self::emitUndeclaredNamedArgument($code_base, $context, $method, $argument); + } elseif ($method->isPHPInternal()) { + self::emitSuspiciousNamedArgumentVariadicInternal($code_base, $context, $method, $argument); + } + continue; + } + if (!\is_array($positions_used)) { + $positions_used = \array_slice($node->children, 0, $original_i); + } + } else { + // Get the parameter associated with this argument + // FIXME: Use the real parameter name all the time for named arguments if it exists + $parameter = $method->getParameterForCaller($i); + $argument_expression = $argument; + } + if (\is_array($positions_used)) { + $reused_argument = $positions_used[$i] ?? null; + if ($reused_argument !== null && $parameter && !$parameter->isVariadic()) { + if ($method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::DuplicateNamedArgumentInternal, + $argument->lineno ?? $node->lineno, + ASTReverter::toShortString($argument), + ASTReverter::toShortString($reused_argument), + $method->getRepresentationForIssue(true) + ); + } else { + Issue::maybeEmit( + $code_base, + $context, + Issue::DuplicateNamedArgument, + $argument->lineno ?? $node->lineno, + ASTReverter::toShortString($argument), + ASTReverter::toShortString($reused_argument), + $method->getRepresentationForIssue(true), + $method->getContext()->getFile(), + $method->getContext()->getLineNumberStart() + ); + } + } else { + $positions_used[$i] = $argument; + } + } + + + // This issue should be caught elsewhere + if (!$parameter) { + $argument_type = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $argument_expression, + true + ); + if ($argument_type->isVoidType()) { + self::warnVoidTypeArgument($code_base, $context, $argument_expression, $node); + } + continue; + } + + $argument_kind = $argument->kind ?? 0; + + // If this is a pass-by-reference parameter, make sure + // we're passing an allowable argument + if ($parameter->isPassByReference()) { + if ((!$argument_expression instanceof Node) || !\in_array($argument_kind, self::REFERENCE_NODE_KINDS, true)) { + $is_possible_reference = self::isExpressionReturningReference($code_base, $context, $argument_expression); + + if (!$is_possible_reference) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeNonVarPassByRef, + $argument->lineno ?? $node->lineno ?? 0, + ($i + 1), + $method->getRepresentationForIssue(true) + ); + } + } else { + $variable_name = (new ContextNode( + $code_base, + $context, + $argument_expression + ))->getVariableName(); + + if (Type::isSelfTypeString($variable_name) + && !$context->isInClassScope() + && ($argument_kind === ast\AST_STATIC_PROP || $argument_kind === ast\AST_PROP) + ) { + Issue::maybeEmit( + $code_base, + $context, + Issue::ContextNotObject, + $argument->lineno ?? $node->lineno, + "$variable_name" + ); + } + } + } + + // Get the type of the argument. We'll check it against + // the parameter in a moment + $argument_type = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $argument_expression, + true + ); + if ($argument_type->isVoidType()) { + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + self::warnVoidTypeArgument($code_base, $context, $argument_expression, $node); + } + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + self::analyzeParameter($code_base, $context, $method, $argument_type, $argument->lineno ?? $node->lineno, $i, $argument_expression, $node); + if ($parameter->isPassByReference()) { + if ($argument_expression instanceof Node) { + // @phan-suppress-next-line PhanUndeclaredProperty this is added for analyzers + $argument_expression->is_reference = true; + } + } + if ($argument_kind === ast\AST_UNPACK && $argument_expression instanceof Node) { + self::analyzeRemainingParametersForVariadic($code_base, $context, $method, $i + 1, $node, $argument_expression, $argument_type); + } + } + if (\is_array($positions_used)) { + self::checkAllNamedArgumentsPassed($code_base, $context, $node->lineno, $method, $positions_used); + } + } + + private static function emitSuspiciousNamedArgumentForVariadic( + CodeBase $code_base, + Context $context, + FunctionInterface $method, + Node $argument + ): void { + $argument_name = $argument->children['name']; + Issue::maybeEmit( + $code_base, + $context, + Issue::SuspiciousNamedArgumentForVariadic, + $argument->lineno, + $argument_name, + $method->getRepresentationForIssue(true), + $argument_name + ); + } + + private static function emitUndeclaredNamedArgument( + CodeBase $code_base, + Context $context, + FunctionInterface $method, + Node $argument + ): void { + $parameter_suggestions = []; + foreach ($method->getRealParameterList() as $parameter) { + if (!$parameter->isVariadic()) { + $name = $parameter->getName(); + $parameter_suggestions[$name] = $name; + } + } + $argument_name = $argument->children['name']; + $suggested_arguments = IssueFixSuggester::getSuggestionsForStringSet($argument_name, $parameter_suggestions); + $suggestion = $suggested_arguments ? Suggestion::fromString('Did you mean ' . \implode(' ', $suggested_arguments)) : null; + + if ($method->isPHPInternal()) { + Issue::maybeEmitWithParameters( + $code_base, + $context, + Issue::UndeclaredNamedArgumentInternal, + $argument->lineno, + [ASTReverter::toShortString($argument), $method->getRepresentationForIssue(true)], + $suggestion + ); + } else { + Issue::maybeEmitWithParameters( + $code_base, + $context, + Issue::UndeclaredNamedArgument, + $argument->lineno, + [ + ASTReverter::toShortString($argument), + $method->getRepresentationForIssue(true), + $method->getContext()->getFile(), + $method->getContext()->getLineNumberStart(), + ], + $suggestion + ); + } + } + + /** + * Warn about using named arguments with internal functions, + * ignoring known exceptions such as call_user_func, ReflectionMethod->invoke, etc. + * @param FunctionInterface $method an internal function + * @param Node $argument a node of kind ast\AST_NAMED_ARG + */ + private static function emitSuspiciousNamedArgumentVariadicInternal( + CodeBase $code_base, + Context $context, + FunctionInterface $method, + Node $argument + ): void { + $fqsen = $method instanceof Method ? $method->getRealDefiningFQSEN() : $method->getFQSEN(); + if (!\in_array($fqsen->__toString(), [ + '\call_user_func', + '\ReflectionMethod::invoke', + '\ReflectionMethod::newInstance', + '\ReflectionFunction::invoke', + '\ReflectionFunction::newInstance', + '\ReflectionFunctionAbstract::invoke', + '\ReflectionFunctionAbstract::newInstance', + '\Closure::call', + '\Closure::__invoke', + ], true)) { + Issue::maybeEmitWithParameters( + $code_base, + $context, + Issue::SuspiciousNamedArgumentVariadicInternal, + $argument->lineno, + [ + ASTReverter::toShortString($argument), + $method->getRepresentationForIssue(true), + ] + ); + } + } + + /** + * @param array $positions_used + */ + private static function checkAllNamedArgumentsPassed( + CodeBase $code_base, + Context $context, + int $lineno, + FunctionInterface $method, + array $positions_used + ): void { + foreach ($method->getRealParameterList() as $i => $parameter) { + if ($parameter->isOptional() || $parameter->isVariadic()) { + continue; + } + if (isset($positions_used[$i])) { + continue; + } + if ($method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::MissingNamedArgumentInternal, + $lineno, + $parameter, + $method->getRepresentationForIssue(true) + ); + } else { + Issue::maybeEmit( + $code_base, + $context, + Issue::MissingNamedArgument, + $lineno, + $parameter, + $method->getRepresentationForIssue(true), + $method->getContext()->getFile(), + $method->getContext()->getLineNumberStart() + ); + } + } + } + + /** + * @param Node|string|int|float|null $argument + */ + private static function warnVoidTypeArgument( + CodeBase $code_base, + Context $context, + $argument, + Node $node + ): void { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeVoidArgument, + $argument->lineno ?? $node->lineno, + ASTReverter::toShortString($argument) + ); + } + + private static function analyzeRemainingParametersForVariadic( + CodeBase $code_base, + Context $context, + FunctionInterface $method, + int $start_index, + Node $node, + Node $argument, + UnionType $argument_type + ): void { + // Check the remaining required parameters for this variadic argument. + // To avoid false positives, don't check optional parameters for now. + + // TODO: Could do better (e.g. warn about too few/many params, warn about individual types) + // if the array shape type is known or available in phpdoc. + $param_count = $method->getNumberOfRequiredParameters(); + for ($i = $start_index; $i < $param_count; $i++) { + // Get the parameter associated with this argument + $parameter = $method->getParameterForCaller($i); + + // Shouldn't be possible? + if (!$parameter) { + return; + } + + $argument_kind = $argument->kind; + + // If this is a pass-by-reference parameter, make sure + // we're passing an allowable argument + if ($parameter->isPassByReference()) { + if (!\in_array($argument_kind, self::REFERENCE_NODE_KINDS, true)) { + $is_possible_reference = self::isExpressionReturningReference($code_base, $context, $argument); + + if (!$is_possible_reference) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeNonVarPassByRef, + $argument->lineno ?? $node->lineno ?? 0, + ($i + 1), + $method->getRepresentationForIssue(true) + ); + } + } + // Omit ContextNotObject check, this was checked for the first matching parameter + } + + self::analyzeParameter($code_base, $context, $method, $argument_type, $argument->lineno, $i, $argument, $node); + if ($parameter->isPassByReference()) { + // @phan-suppress-next-line PhanUndeclaredProperty this is added for analyzers + $argument->is_reference = true; + } + } + } + + /** + * Analyze passing the an argument of type $argument_type to the ith parameter of the (possibly variadic) method $method, + * for a call made from the line $lineno. + * + * @param int $i the index of the parameter. + * @param Node|string|int|float $argument_node + * @param ?Node $node the node of the call TODO: Default + */ + public static function analyzeParameter(CodeBase $code_base, Context $context, FunctionInterface $method, UnionType $argument_type, int $lineno, int $i, $argument_node, ?Node $node): void + { + // Expand it to include all parent types up the chain + try { + $argument_type_expanded_resolved = + $argument_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base); + } catch (RecursionDepthException $_) { + return; + } + + // Check the method to see if it has the correct + // parameter types. If not, keep hunting through + // alternates of the method until we find one that + // takes the correct types + $alternate_parameter = null; + $alternate_parameter_type = null; // TODO: Properly merge "possibly undefined" union types - without this, undefined is inferred instead of possibly undefined + + foreach ($method->alternateGenerator($code_base) as $alternate_method) { + // Get the parameter associated with this argument + $candidate_alternate_parameter = $alternate_method->getParameterForCaller($i); + if (\is_null($candidate_alternate_parameter)) { + continue; + } + if ($alternate_parameter && $node) { + // If another function was already checked which had the right number of alternate parameters, don't bother allowing checks with param + $arglist = $node->kind === ast\AST_ARG_LIST ? $node : ($node->children['args'] ?? null); + if ($arglist) { + $argcount = \count($arglist->children); + + // Make sure we have enough arguments + if ($argcount < $alternate_method->getNumberOfRequiredParameters() && !self::isUnpack($arglist->children)) { + continue; + } + } + } + + $alternate_parameter = $candidate_alternate_parameter; + $alternate_parameter_type = $alternate_parameter->getNonVariadicUnionType()->withStaticResolvedInFunctionLike($alternate_method); + + // See if the argument can be cast to the + // parameter + if ($argument_type_expanded_resolved->canCastToUnionType($alternate_parameter_type)) { + if ($alternate_parameter_type->hasRealTypeSet() && $argument_type->hasRealTypeSet()) { + $real_parameter_type = $alternate_parameter_type->getRealUnionType(); + $real_argument_type = $argument_type->getRealUnionType(); + $real_argument_type_expanded_resolved = $real_argument_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base); + if (!$real_argument_type_expanded_resolved->canCastToDeclaredType($code_base, $context, $real_parameter_type)) { + $real_argument_type_expanded_resolved_nonnull = $real_argument_type_expanded_resolved->nonNullableClone(); + if ($real_argument_type_expanded_resolved_nonnull->isEmpty() || + !$real_argument_type_expanded_resolved_nonnull->canCastToDeclaredType($code_base, $context, $real_parameter_type)) { + // We know that the inferred real types don't match with the strict_types setting of the caller + // (e.g. null -> any non-null type) + // Try checking any other alternates, and emit PhanTypeMismatchArgumentReal if that fails. + // + // Don't emit PhanTypeMismatchArgumentReal if the only reason that they failed was due to nullability of individual types, + // e.g. allow ?array -> iterable + continue; + } + } + } + if (Config::get_strict_param_checking() && $argument_type->typeCount() > 1) { + self::analyzeParameterStrict($code_base, $context, $method, $argument_node, $argument_type, $alternate_parameter, $alternate_parameter_type, $lineno, $i); + } + if ($alternate_parameter->shouldWarnIfProvided()) { + self::maybeWarnProvidingUnusedParameter($code_base, $context, $lineno, $method, $alternate_parameter, $i); + } + return; + } + } + + if (!($alternate_parameter instanceof Parameter)) { + return; // skip type check - is this possible? + } + if (!isset($alternate_parameter_type)) { + throw new AssertionError('Impossible - should be set if $alternate_parameter is set'); + } + if ($alternate_parameter->shouldWarnIfProvided()) { + self::maybeWarnProvidingUnusedParameter($code_base, $context, $lineno, $method, $alternate_parameter, $i); + } + + if ($alternate_parameter->isPassByReference() && $alternate_parameter->getReferenceType() === Parameter::REFERENCE_WRITE_ONLY) { + return; + } + + if ($alternate_parameter_type->hasTemplateTypeRecursive()) { + // Don't worry about **unresolved** template types. + // We resolve them if possible in ContextNode->getMethod() + // + // TODO: Warn about the type without the templates? + return; + } + if ($alternate_parameter_type->hasTemplateParameterTypes()) { + // TODO: Make the check for templates recursive + $argument_type_expanded_templates = $argument_type->asExpandedTypesPreservingTemplate($code_base); + if ($argument_type_expanded_templates->canCastToUnionTypeHandlingTemplates($alternate_parameter_type, $code_base)) { + // - can cast MyClass<\stdClass> to MyClass + // - can cast Some<\stdClass> to Option<\stdClass> + // - cannot cast Some<\SomeOtherClass> to Option<\stdClass> + return; + } + // echo "Debug: $argument_type $argument_type_expanded_templates cannot cast to $parameter_type\n"; + } + + if ($method->isPHPInternal()) { + // If we are not in strict mode and we accept a string parameter + // and the argument we are passing has a __toString method then it is ok + if (!$context->isStrictTypes() && $alternate_parameter_type->hasNonNullStringType()) { + try { + foreach ($argument_type_expanded_resolved->asClassList($code_base, $context) as $clazz) { + if ($clazz->hasMethodWithName($code_base, "__toString", true)) { + return; + } + } + } catch (CodeBaseException $_) { + // Swallow "Cannot find class", go on to emit issue + } + } + } + // Check suppressions and emit the issue + self::warnInvalidArgumentType($code_base, $context, $method, $alternate_parameter, $alternate_parameter_type, $argument_node, $argument_type, $argument_type->asExpandedTypes($code_base), $argument_type_expanded_resolved, $lineno, $i); + } + + private static function maybeWarnProvidingUnusedParameter( + CodeBase $code_base, + Context $context, + int $lineno, + FunctionInterface $method, + Parameter $parameter, + int $i + ): void { + if ($method->getNumberOfRequiredParameters() > $i) { + // handle required parameter after optional + return; + } + if ($method->isPHPInternal()) { + // not supported for stubs + return; + } + $fqsen = $method->getFQSEN(); + if ($fqsen->getAlternateId() > 0) { + return; + } + if ($method instanceof Method) { + if ($method->isOverriddenByAnother() || $code_base->hasMethodWithFQSEN($fqsen->withAlternateId(1))) { + return; + } + } + $issue_type = $method instanceof Func && $method->isClosure() ? Issue::ProvidingUnusedParameterOfClosure : Issue::ProvidingUnusedParameter; + if ($method->hasSuppressIssue($issue_type)) { + // For convenience, allow suppressing it on the method definition as well. + return; + } + Issue::maybeEmit( + $code_base, + $context, + $issue_type, + $lineno, + $parameter->getName(), + $method->getRepresentationForIssue(true), + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } + + /** + * @param Node|string|int|float $argument_node + */ + private static function warnInvalidArgumentType( + CodeBase $code_base, + Context $context, + FunctionInterface $method, + Parameter $alternate_parameter, + UnionType $alternate_parameter_type, + $argument_node, + UnionType $argument_type, + UnionType $argument_type_expanded, + UnionType $argument_type_expanded_resolved, + int $lineno, + int $i + ): void { + /** + * @return ?string + */ + $choose_issue_type = static function (string $issue_type, string $nullable_issue_type, string $real_issue_type) use ($argument_type, $argument_type_expanded_resolved, $alternate_parameter_type, $code_base, $context, $lineno): ?string { + if ($context->hasSuppressIssue($code_base, $real_issue_type)) { + // Suppressing the most severe argument type mismatch error will suppress related issues. + // Record that the most severe issue type suppression was used and don't emit any issue. + return null; + } + // @phan-suppress-next-line PhanAccessMethodInternal + if ($argument_type_expanded_resolved->isNull() || !$argument_type_expanded_resolved->canCastToUnionTypeIfNonNull($alternate_parameter_type)) { + if ($argument_type->hasRealTypeSet() && $alternate_parameter_type->hasRealTypeSet()) { + $real_arg_type = $argument_type->getRealUnionType(); + $real_parameter_type = $alternate_parameter_type->getRealUnionType(); + if (!$real_arg_type->canCastToDeclaredType($code_base, $context, $real_parameter_type)) { + return $real_issue_type; + } + } + return $issue_type; + } + if (Issue::shouldSuppressIssue($code_base, $context, $issue_type, $lineno, [])) { + return null; + } + return $nullable_issue_type; + }; + + if ($method->isPHPInternal()) { + $issue_type = $choose_issue_type(Issue::TypeMismatchArgumentInternal, Issue::TypeMismatchArgumentNullableInternal, Issue::TypeMismatchArgumentInternalReal); + if (!is_string($issue_type)) { + return; + } + if ($issue_type === Issue::TypeMismatchArgumentInternal) { + if ($argument_type->hasRealTypeSet() && + !$argument_type->getRealUnionType()->canCastToDeclaredType($code_base, $context, $alternate_parameter_type)) { + // PHP 7.x doesn't have reflection types for many methods and global functions and won't throw, + // but will emit a warning and fail the call. + // + // XXX: There are edge cases, e.g. some php functions will allow passing in null depending on the parameter parsing API used, without warning. + $issue_type = Issue::TypeMismatchArgumentInternalProbablyReal; + } else { + if ($context->hasSuppressIssue($code_base, Issue::TypeMismatchArgumentInternalProbablyReal)) { + // Suppressing ProbablyReal also suppresses the less severe version. + return; + } + } + } + if (\in_array($issue_type, [Issue::TypeMismatchArgumentInternalReal, Issue::TypeMismatchArgumentInternalProbablyReal], true)) { + Issue::maybeEmit( + $code_base, + $context, + $issue_type, + $lineno, + ($i + 1), + $alternate_parameter->getName(), + ASTReverter::toShortString($argument_node), + $argument_type_expanded, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($argument_type), + $method->getRepresentationForIssue(), + (string)$alternate_parameter_type, + $issue_type === Issue::TypeMismatchArgumentInternalReal ? PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($alternate_parameter_type) : '' + ); + return; + } + Issue::maybeEmit( + $code_base, + $context, + $issue_type, + $lineno, + ($i + 1), + $alternate_parameter->getName(), + ASTReverter::toShortString($argument_node), + $argument_type_expanded, + $method->getRepresentationForIssue(), + (string)$alternate_parameter_type + ); + return; + } + $issue_type = $choose_issue_type(Issue::TypeMismatchArgument, Issue::TypeMismatchArgumentNullable, Issue::TypeMismatchArgumentReal); + if (!is_string($issue_type)) { + return; + } + // FIXME call memoizeFlushAll not just on types in Type::$canonical_object_map, + // but other derived types. Alternately, move away from asExpandedTypes for anything except + // classlikes, and pass in the CodeBase to canCastToUnionType and other methods. + if ($issue_type === Issue::TypeMismatchArgumentReal) { + Issue::maybeEmit( + $code_base, + $context, + $issue_type, + $lineno, + ($i + 1), + $alternate_parameter->getName(), + ASTReverter::toShortString($argument_node), + $argument_type_expanded->withUnionType($argument_type_expanded_resolved), + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($argument_type), + $method->getRepresentationForIssue(), + (string)$alternate_parameter_type, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($alternate_parameter_type), + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + return; + } + if ($context->hasSuppressIssue($code_base, Issue::TypeMismatchArgumentProbablyReal)) { + // Suppressing ProbablyReal also suppresses the less severe version. + return; + } + if ($issue_type === Issue::TypeMismatchArgument) { + if ($argument_type->hasRealTypeSet() && + !$argument_type->getRealUnionType()->canCastToDeclaredType($code_base, $context, $alternate_parameter_type)) { + // The argument's real type is completely incompatible with the documented phpdoc type. + // + // Either the phpdoc type is wrong or the argument is likely wrong. + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeMismatchArgumentProbablyReal, + $lineno, + ($i + 1), + $alternate_parameter->getName(), + ASTReverter::toShortString($argument_node), + $argument_type_expanded, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($argument_type), + $method->getRepresentationForIssue(), + $alternate_parameter_type, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($alternate_parameter_type), + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + return; + } + } + Issue::maybeEmit( + $code_base, + $context, + $issue_type, + $lineno, + ($i + 1), + $alternate_parameter->getName(), + ASTReverter::toShortString($argument_node), + $argument_type_expanded->withUnionType($argument_type_expanded_resolved), + $method->getRepresentationForIssue(), + (string)$alternate_parameter_type, + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } + + /** + * @param Node|string|int|float $argument_node + */ + private static function analyzeParameterStrict(CodeBase $code_base, Context $context, FunctionInterface $method, $argument_node, UnionType $argument_type, Variable $alternate_parameter, UnionType $parameter_type, int $lineno, int $i): void + { + if ($alternate_parameter instanceof Parameter && $alternate_parameter->isPassByReference() && $alternate_parameter->getReferenceType() === Parameter::REFERENCE_WRITE_ONLY) { + return; + } + $type_set = $argument_type->getTypeSet(); + if (\count($type_set) < 2) { + throw new AssertionError("Expected to have at least two parameter types when checking if parameter types match in strict mode"); + } + + $mismatch_type_set = UnionType::empty(); + $mismatch_expanded_types = null; + + // For the strict + foreach ($type_set as $type) { + // Expand it to include all parent types up the chain + $individual_type_expanded = $type->withStaticResolvedInContext($context)->asExpandedTypes($code_base); + + // See if the argument can be cast to the + // parameter + if (!$individual_type_expanded->canCastToUnionType( + $parameter_type + )) { + if ($method->isPHPInternal()) { + // If we are not in strict mode and we accept a string parameter + // and the argument we are passing has a __toString method then it is ok + if (!$context->isStrictTypes() && $parameter_type->hasNonNullStringType()) { + if ($individual_type_expanded->hasClassWithToStringMethod($code_base, $context)) { + continue; // don't warn about $type + } + } + } + $mismatch_type_set = $mismatch_type_set->withType($type); + if ($mismatch_expanded_types === null) { + // Warn about the first type + $mismatch_expanded_types = $individual_type_expanded; + } + } + } + + + if ($mismatch_expanded_types === null) { + // No mismatches + return; + } + + if ($method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $context, + self::getStrictArgumentIssueType($mismatch_type_set, true), + $lineno, + ($i + 1), + $alternate_parameter->getName(), + ASTReverter::toShortString($argument_node), + $argument_type, + $method->getRepresentationForIssue(), + (string)$parameter_type, + $mismatch_expanded_types + ); + return; + } + Issue::maybeEmit( + $code_base, + $context, + self::getStrictArgumentIssueType($mismatch_type_set, false), + $lineno, + ($i + 1), + $alternate_parameter->getName(), + ASTReverter::toShortString($argument_node), + $argument_type, + $method->getRepresentationForIssue(), + (string)$parameter_type, + $mismatch_expanded_types, + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } + + private static function getStrictArgumentIssueType(UnionType $union_type, bool $is_internal): string + { + if ($union_type->typeCount() === 1) { + $type = $union_type->getTypeSet()[0]; + if ($type instanceof NullType) { + return $is_internal ? Issue::PossiblyNullTypeArgumentInternal : Issue::PossiblyNullTypeArgument; + } + if ($type instanceof FalseType) { + return $is_internal ? Issue::PossiblyFalseTypeArgumentInternal : Issue::PossiblyFalseTypeArgument; + } + } + return $is_internal ? Issue::PartialTypeMismatchArgumentInternal : Issue::PartialTypeMismatchArgument; + } + + /** + * Used to check if a place expecting a reference is actually getting a reference from a node. + * Obvious types which are always references (properties, variables) must be checked for before calling this. + * + * @param Node|string|int|float|null $node + * + * @return bool - True if this node is a call to a function that may return a reference? + */ + public static function isExpressionReturningReference(CodeBase $code_base, Context $context, $node): bool + { + if (!($node instanceof Node)) { + return false; + } + $node_kind = $node->kind; + if (\in_array($node_kind, self::REFERENCE_NODE_KINDS, true)) { + return true; + } + if ($node_kind === ast\AST_UNPACK) { + return self::isExpressionReturningReference($code_base, $context, $node->children['expr']); + } + if ($node_kind === ast\AST_CALL) { + foreach ((new ContextNode( + $code_base, + $context, + $node->children['expr'] + ))->getFunctionFromNode() as $function) { + if ($function->returnsRef()) { + return true; + } + } + } elseif (\in_array($node_kind, [ast\AST_STATIC_CALL, ast\AST_METHOD_CALL, ast\AST_NULLSAFE_METHOD_CALL], true)) { + $method_name = $node->children['method'] ?? null; + if (is_string($method_name)) { + $class_node = $node->children['class'] ?? $node->children['expr']; + if (!($class_node instanceof Node)) { + return false; + } + try { + foreach (UnionTypeVisitor::classListFromNodeAndContext( + $code_base, + $context, + $class_node + ) as $class) { + if (!$class->hasMethodWithName( + $code_base, + $method_name, + true + )) { + continue; + } + + $method = $class->getMethodByName( + $code_base, + $method_name + ); + // Return true if any of the possible methods (expect that just one is found) returns a reference. + if ($method->returnsRef()) { + return true; + } + } + } catch (IssueException $_) { + // Swallow any issue exceptions here. They'll be caught elsewhere. + } + } + } + return false; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/AssignOperatorAnalysisVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/AssignOperatorAnalysisVisitor.php new file mode 100644 index 000000000..b4b72e2b2 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/AssignOperatorAnalysisVisitor.php @@ -0,0 +1,760 @@ +code_base = $code_base; + $this->context = $context; + } + + /** + * @param Node $node + * A node to visit + * @return Context + */ + public function __invoke(Node $node) + { + // NOTE: Some operations currently don't exist in any php version, such as `$x ||= 2;`, `$x xor= 2;` + return Element::acceptBinaryFlagVisitor($node, $this); + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node + * A node to check types on + */ + public function visit(Node $node): Context + { + $this->emitIssue( + Issue::Unanalyzable, + $node->lineno + ); + return $this->context; + } + + /** + * @param Node $node a node of kind AST_VAR + * @param Closure(UnionType):UnionType $get_type + */ + private function updateTargetVariableWithType(Node $node, Closure $get_type): Context + { + try { + $variable_name = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getVariableName(); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + return $this->context; + } + // Don't analyze variables when we can't determine their names. + if ($variable_name === '') { + return $this->context; + } + if ($this->context->getScope()->hasVariableWithName( + $variable_name + )) { + $variable = clone( + $this->context->getScope()->getVariableByName( + $variable_name + ) + ); + $variable->setUnionType($get_type($variable->getUnionType())); + return $this->context->withScopeVariable($variable); + } + + if (Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())) { + return $this->context; + } + // no such variable exists, warn about this + Issue::maybeEmitWithParameters( + $this->code_base, + $this->context, + Issue::UndeclaredVariableAssignOp, + $node->lineno, + [$variable_name], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name) + ); + // Then create the variable + $variable = new Variable( + $this->context, + $variable_name, + $get_type(NullType::instance(false)->asPHPDocUnionType()), + 0 + ); + return $this->context->withScopeVariable($variable); + } + + /** + * Based on AssignmentVisitor->visitDim + * @param Node $assign_op_node a node of kind ast\AST_ASSIGN_OP with ast\AST_DIM as the left hand side + * @param Closure(UnionType):UnionType $get_type + */ + private function updateTargetDimWithType(Node $assign_op_node, Closure $get_type): Context + { + $node = $assign_op_node->children['var']; + if (!$node instanceof Node) { + // Should be impossible as currently called, but warn anyway. + $this->emitIssue( + Issue::InvalidWriteToTemporaryExpression, + $assign_op_node->lineno, + ASTReverter::toShortString($node), + Type::fromObject($node) + ); + return $this->context; + } + $expr_node = $node->children['expr']; + if (!($expr_node instanceof Node)) { + $this->emitIssue( + Issue::InvalidWriteToTemporaryExpression, + $node->lineno, + ASTReverter::toShortString($node), + Type::fromObject($expr_node) + ); + return $this->context; + } + $dim_node = $node->children['dim']; + if ($expr_node->kind === \ast\AST_VAR) { + $variable_name = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getVariableName(); + if (Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())) { + if ($variable_name === 'GLOBALS') { + if (\is_string($dim_node)) { + $assign_op_node = new Node(ast\AST_ASSIGN_OP, 0, [ + 'var' => new Node(ast\AST_VAR, 0, ['name' => $dim_node], $node->lineno), + 'expr' => $assign_op_node->children['expr'], + ], $assign_op_node->lineno); + if ($this->context->isInGlobalScope()) { + return $this->updateTargetWithType($assign_op_node, $get_type); + } + // TODO: Could handle using both `global $x` and `$GLOBALS['x']` in the same function (low priority) + + // Modify the global scope + (new self( + $this->code_base, + $this->context->withScope(new GlobalScope()) + ))->updateTargetWithType($assign_op_node, $get_type); + // fall through and return the context still inside of the function + } + return $this->context; + } + if (!$this->context->getScope()->hasVariableWithName($variable_name)) { + $this->context->addScopeVariable(new Variable( + $this->context->withLineNumberStart($expr_node->lineno), + $variable_name, + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name), + 0 + )); + } + } + } + + try { + $old_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node, + false + ); + } catch (\Exception $_) { + return $this->context; + } + + $new_type = $get_type($old_type); + + // Recurse into whatever we're []'ing + return (new AssignmentVisitor( + $this->code_base, + $this->context, + $node, + $new_type + ))->visitDim($node); + } + + /** + * Based on AssignmentVisitor->visitProp + * @param Node $assign_op_node a node of kind ast\AST_ASSIGN_OP with ast\AST_PROP as the left hand side + * @param Closure(UnionType):UnionType $get_type + */ + private function updateTargetPropWithType(Node $assign_op_node, Closure $get_type): Context + { + $node = $assign_op_node->children['var']; + if (!($node instanceof Node)) { + $this->emitIssue( + Issue::InvalidWriteToTemporaryExpression, + $assign_op_node->lineno, + ASTReverter::toShortString($node), + Type::fromObject($node) + ); + return $this->context; + } + $expr_node = $node->children['expr']; + if (!($expr_node instanceof Node)) { + $this->emitIssue( + Issue::InvalidWriteToTemporaryExpression, + $node->lineno, + ASTReverter::toShortString($node), + Type::fromObject($expr_node) + ); + return $this->context; + } + + try { + $old_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node, + false + ); + } catch (\Exception $_) { + return $this->context; + } + + $new_type = $get_type($old_type); + + // Recurse into whatever we're []'ing + return (new AssignmentVisitor( + $this->code_base, + $this->context, + $node, + $new_type + ))->visitProp($node); + } + + /** + * @param Node $node + * @param Closure(UnionType):UnionType $get_type + */ + private function updateTargetWithType(Node $node, Closure $get_type): Context + { + $left = $node->children['var']; + // The left can be a non-Node for an invalid AST + $kind = $left->kind ?? null; + if ($kind === ast\AST_VAR) { + return $this->updateTargetVariableWithType($node, $get_type); + } elseif ($kind === ast\AST_DIM) { + return $this->updateTargetDimWithType($node, $get_type); + } elseif ($kind === ast\AST_PROP) { + return $this->updateTargetPropWithType($node, $get_type); + } + // TODO: Could check types of other expressions, such as properties + // TODO: Could check for `@property-read` (invalid to pass to assignment operator), etc. + return $this->context; + } + + /** + * @see BinaryOperatorFlagVisitor::visitBinaryAdd() for analysis of "+", which is similar to "+=" + */ + public function visitBinaryAdd(Node $node): Context + { + return $this->updateTargetWithType($node, function (UnionType $left) use ($node): UnionType { + $code_base = $this->code_base; + $context = $this->context; + + $right = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['expr'] + ); + + // fast-track common cases + if ($left->isNonNullIntType() && $right->isNonNullIntType()) { + if (!$context->isInLoop()) { + return BinaryOperatorFlagVisitor::computeIntOrFloatOperationResult($node, $left, $right); + } + return IntType::instance(false)->asPHPDocUnionType(); + } + + // If both left and right are arrays, then this is array + // concatenation. + if ($left->isGenericArray() && $right->isGenericArray()) { + BinaryOperatorFlagVisitor::checkInvalidArrayShapeCombination($this->code_base, $this->context, $node, $left, $right); + if ($left->isEqualTo($right)) { + return $left; + } + return ArrayType::combineArrayTypesOverriding($left, $right, false); + } + + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + // TODO: Stricten this to warn about strings based on user config. + return $type instanceof ScalarType || $type instanceof ArrayType || $type instanceof MixedType; + }, + $left, + $right, + Issue::TypeInvalidLeftOperandOfAdd, + Issue::TypeInvalidRightOperandOfAdd + ); + + static $float_type = null; + static $array_type = null; + static $int_or_float_union_type = null; + if ($int_or_float_union_type === null) { + $float_type = FloatType::instance(false); + $array_type = ArrayType::instance(false); + $int_or_float_union_type = UnionType::fromFullyQualifiedPHPDocString('int|float'); + } + + if ($left->isNonNullNumberType() && $right->isNonNullNumberType()) { + if (!$context->isInLoop()) { + return BinaryOperatorFlagVisitor::computeIntOrFloatOperationResult($node, $left, $right); + } + if (!$left->hasNonNullIntType() || !$right->hasNonNullIntType()) { + // Heuristic: If one or more of the sides is a float, the result is always a float. + return $float_type->asPHPDocUnionType(); + } + return $int_or_float_union_type; + } + + $left_is_array = ( + !$left->genericArrayElementTypes()->isEmpty() + && $left->nonArrayTypes()->isEmpty() + ) || $left->isType($array_type); + + $right_is_array = ( + !$right->genericArrayElementTypes()->isEmpty() + && $right->nonArrayTypes()->isEmpty() + ) || $right->isType($array_type); + + if ($left_is_array || $right_is_array) { + if ($left_is_array && $right_is_array) { + // TODO: Make the right types for array offsets completely override the left types? + return ArrayType::combineArrayTypesOverriding($left, $right, false); + } + + if ($left_is_array + && !$right->canCastToUnionType( + ArrayType::instance(false)->asPHPDocUnionType() + ) + ) { + $this->emitIssue( + Issue::TypeInvalidRightOperand, + $node->lineno ?? 0 + ); + return UnionType::empty(); + } elseif ($right_is_array && !$left->canCastToUnionType($array_type->asPHPDocUnionType())) { + $this->emitIssue( + Issue::TypeInvalidLeftOperand, + $node->lineno ?? 0 + ); + return UnionType::empty(); + } + // If it is a '+' and we know one side is an array + // and the other is unknown, assume array + return $array_type->asPHPDocUnionType(); + } + + return $int_or_float_union_type; + }); + } + + public function visitBinaryCoalesce(Node $node): Context + { + $var_node = $node->children['var']; + if (!$var_node instanceof Node) { + // nonsense like `2 ??= $x` + $this->emitIssue( + Issue::InvalidNode, + $node->lineno, + "Invalid left hand side for ??=" + ); + return $this->context; + } + $new_node = new ast\Node(ast\AST_BINARY_OP, $node->lineno, [ + 'left' => $var_node, + 'right' => $node->children['expr'], + ], ast\flags\BINARY_COALESCE); + + $new_type = (new BinaryOperatorFlagVisitor( + $this->code_base, + $this->context, + true + ))->visitBinaryCoalesce($new_node); + return (new AssignmentVisitor( + $this->code_base, + $this->context, + $var_node, + $new_type + ))->__invoke($var_node); + } + + private function analyzeNumericArithmeticOp(Node $node, bool $combination_is_int): Context + { + return $this->updateTargetWithType($node, function (UnionType $left) use ($node, $combination_is_int): UnionType { + $code_base = $this->code_base; + $context = $this->context; + + $right = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['expr'] + ); + if (!$right->isEmpty() && !$right->containsTruthy()) { + $this->warnRightSideZero($node, $right); + } + + static $float_type = null; + static $int_or_float_union_type = null; + if ($int_or_float_union_type === null) { + $float_type = FloatType::instance(false); + $int_or_float_union_type = UnionType::fromFullyQualifiedPHPDocString('int|float'); + } + + // fast-track common cases + if ($left->isNonNullIntType() && $right->isNonNullIntType()) { + if (!$context->isInLoop()) { + return BinaryOperatorFlagVisitor::computeIntOrFloatOperationResult($node, $left, $right); + } + if ($combination_is_int) { + // XXX can overflow to float so asRealUnionType isn't used. + return IntType::instance(false)->asPHPDocUnionType(); + } else { + return $int_or_float_union_type; + } + } + + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + // TODO: Stricten this to warn about strings based on user config. + return $type instanceof ScalarType || $type instanceof MixedType; + }, + $left, + $right, + Issue::TypeInvalidLeftOperandOfNumericOp, + Issue::TypeInvalidRightOperandOfNumericOp + ); + + if ($left->isNonNullNumberType() && $right->isNonNullNumberType()) { + if (!$context->isInLoop()) { + return BinaryOperatorFlagVisitor::computeIntOrFloatOperationResult($node, $left, $right); + } + if (!$left->hasNonNullIntType() || !$right->hasNonNullIntType()) { + // Heuristic: If one or more of the sides is a float, the result is always a float. + // TODO: Return real types if both sides are real types, e.g. `$x = 2; $x += 3;` + return $float_type->asPHPDocUnionType(); + } + return $int_or_float_union_type; + } + + // TODO: warn about subtracting to/from non-number + + return $int_or_float_union_type; + }); + } + + /** + * Warn about the right hand side always casting to zero when used in a numeric operation. + * @param UnionType $right_type a type that always casts to zero. + */ + private function warnRightSideZero(Node $node, UnionType $right_type): void + { + $issue_type = PostOrderAnalysisVisitor::ISSUE_TYPES_RIGHT_SIDE_ZERO[$node->flags] ?? null; + if (!\is_string($issue_type)) { + return; + } + $this->emitIssue( + $issue_type, + $node->children['expr']->lineno ?? $node->lineno, + ASTReverter::toShortString($node->children['expr']), + $right_type + ); + } + + /** + * @param Node $node with type AST_BINARY_OP + * @param Closure(Type):bool $is_valid_type + * @return void + * + * TODO: Deduplicate and move to a trait? + */ + private function warnAboutInvalidUnionType( + Node $node, + Closure $is_valid_type, + UnionType $left, + UnionType $right, + string $left_issue_type, + string $right_issue_type + ): void { + if (!$left->isEmpty()) { + if (!$left->hasTypeMatchingCallback($is_valid_type)) { + $this->emitIssue( + $left_issue_type, + $node->children['var']->lineno ?? $node->lineno, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags] . '=', + $left + ); + } + } + if (!$right->isEmpty()) { + if (!$right->hasTypeMatchingCallback($is_valid_type)) { + $this->emitIssue( + $right_issue_type, + $node->children['expr']->lineno ?? $node->lineno, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags] . '=', + $right + ); + } + } + } + + private function analyzeBitwiseOperation(Node $node): Context + { + return $this->updateTargetWithType($node, function (UnionType $left_type) use ($node): UnionType { + // TODO: Warn about invalid left and right-hand sides here and in BinaryOperatorFlagVisitor. + // TODO: Return real types if both sides are real types. + // Expect int|string + + $right_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']); + + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + return ($type instanceof IntType || $type instanceof StringType || $type instanceof MixedType) && !$type->isNullableLabeled(); + }, + $left_type, + $right_type, + Issue::TypeInvalidLeftOperandOfBitwiseOp, + Issue::TypeInvalidRightOperandOfBitwiseOp + ); + if (!$this->context->isInLoop()) { + if ($left_type->isNonNullNumberType() && $right_type->isNonNullNumberType()) { + return BinaryOperatorFlagVisitor::computeIntOrFloatOperationResult($node, $left_type, $right_type); + } + } + if ($right_type->hasStringType() || $left_type->hasStringType()) { + if ($right_type->isNonNullStringType() && $left_type->isNonNullStringType()) { + return StringType::instance(false)->asPHPDocUnionType(); + } + return UnionType::fromFullyQualifiedPHPDocString('int|string'); + } + return IntType::instance(false)->asPHPDocUnionType(); + }); + } + + public function visitBinaryBitwiseAnd(Node $node): Context + { + return $this->analyzeBitwiseOperation($node); + } + + public function visitBinaryBitwiseOr(Node $node): Context + { + return $this->analyzeBitwiseOperation($node); + } + + public function visitBinaryBitwiseXor(Node $node): Context + { + return $this->analyzeBitwiseOperation($node); + } + + public function visitBinaryConcat(Node $node): Context + { + return $this->updateTargetWithType($node, static function (UnionType $unused_left): UnionType { + // TODO: Check if both sides can cast to string and warn if they can't. + return StringType::instance(false)->asRealUnionType(); + }); + } + + public function visitBinaryDiv(Node $node): Context + { + return $this->analyzeNumericArithmeticOp($node, false); + } + + public function visitBinaryMod(Node $node): Context + { + $this->warnForInvalidOperandsOfModOp($node); + return $this->updateTargetWithType($node, function (UnionType $left) use ($node): UnionType { + $right = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']); + if (!$this->context->isInLoop()) { + if ($left->isNonNullNumberType() && $right->isNonNullNumberType()) { + return BinaryOperatorFlagVisitor::computeIntOrFloatOperationResult($node, $left, $right); + } + } + // TODO: Check if both sides can cast to int and warn if they can't. + return IntType::instance(false)->asRealUnionType(); + }); + } + + private function warnForInvalidOperandsOfModOp(Node $node): void + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + if (!$right->isEmpty() && !$right->containsTruthy()) { + $this->warnRightSideZero($node, $right); + } + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + return $type->isValidNumericOperand(); + }, + $left, + $right, + Issue::TypeInvalidLeftOperandOfNumericOp, + Issue::TypeInvalidRightOperandOfNumericOp + ); + } + + + public function visitBinaryMul(Node $node): Context + { + return $this->analyzeNumericArithmeticOp($node, true); + } + + public function visitBinaryPow(Node $node): Context + { + // TODO: 2 ** (-2) is a float + return $this->analyzeNumericArithmeticOp($node, true); + } + + /** + * @return Context + * NOTE: There's a draft RFC to make binary shift left/right apply to strings. (https://wiki.php.net/rfc/string-bitwise-shifts) + * For now, it always casts to int. + */ + public function visitBinaryShiftLeft(Node $node): Context + { + $this->analyzeBinaryShift($node); + return $this->updateTargetWithType($node, static function (UnionType $unused_left): UnionType { + // TODO: Check if both sides can cast to int and warn if they can't. + // TODO: Handle both sides being literals + return IntType::instance(false)->asRealUnionType(); + }); + } + + public function visitBinaryShiftRight(Node $node): Context + { + $this->analyzeBinaryShift($node); + return $this->updateTargetWithType($node, static function (UnionType $unused_left): UnionType { + // TODO: Check if both sides can cast to int and warn if they can't. + // TODO: Handle both sides being literals + return IntType::instance(false)->asRealUnionType(); + }); + } + + private function analyzeBinaryShift(Node $node): void + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + return ($type instanceof IntType || $type instanceof MixedType) && !$type->isNullableLabeled(); + }, + $left, + $right, + Issue::TypeInvalidLeftOperandOfIntegerOp, + Issue::TypeInvalidRightOperandOfIntegerOp + ); + } + + public function visitBinarySub(Node $node): Context + { + return $this->analyzeNumericArithmeticOp($node, true); + } + + /** + * @param string $issue_type + * The type of issue to emit. + * + * @param int $lineno + * The line number where the issue was found + * + * @param int|string|FQSEN|UnionType|Type ...$parameters + * Template parameters for the issue's error message + */ + protected function emitIssue( + string $issue_type, + int $lineno, + ...$parameters + ): void { + Issue::maybeEmitWithParameters( + $this->code_base, + $this->context, + $issue_type, + $lineno, + $parameters + ); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/AssignOperatorFlagVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/AssignOperatorFlagVisitor.php new file mode 100644 index 000000000..a34500630 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/AssignOperatorFlagVisitor.php @@ -0,0 +1,434 @@ +code_base = $code_base; + $this->context = $context; + } + + /** + * @param Node $node + * A node to visit + * @return UnionType + */ + public function __invoke(Node $node) + { + // NOTE: Some operations currently don't exist in any php version, such as `$x ||= 2;`, `$x xor= 2;` + return Element::acceptBinaryFlagVisitor($node, $this); + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visit(Node $node): UnionType + { + // TODO: For some types (e.g. xor, bitwise or), set the type of the variable? + // Or should that be done in PreOrderAnalysisVisitor? + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + + if ($left->isExclusivelyArray() + || $right->isExclusivelyArray() + ) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeArrayOperator, + $node->lineno ?? 0, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + $left, + $right + ); + + return UnionType::empty(); + } elseif ($left->hasNonNullIntType() + && $right->hasNonNullIntType() + ) { + return IntType::instance(false)->asPHPDocUnionType(); + } elseif ($left->hasType(FloatType::instance(false)) + && $right->hasType(FloatType::instance(false)) + ) { + return FloatType::instance(false)->asPHPDocUnionType(); + } + + static $int_or_float; + return $int_or_float ?? ($int_or_float = UnionType::fromFullyQualifiedPHPDocString('int|float')); + } + + public function visitBinaryCoalesce(Node $node): UnionType + { + $var_node = $node->children['var']; + $new_node = new ast\Node(ast\AST_BINARY_OP, $node->lineno, [ + 'left' => $var_node, + 'right' => $node->children['expr'], + ], ast\flags\BINARY_COALESCE); + + return (new BinaryOperatorFlagVisitor( + $this->code_base, + $this->context, + true + ))->visitBinaryCoalesce($new_node); + } + /** + * @return UnionType for the `&` operator + */ + public function visitBinaryBitwiseAnd(Node $node): UnionType + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + if ($left->hasNonNullIntType() + && $right->hasNonNullIntType() + ) { + return IntType::instance(false)->asPHPDocUnionType(); + } elseif ($left->hasNonNullStringType() && + $right->hasNonNullStringType()) { + // $x = 'a'; $x &= 'c'; + return StringType::instance(false)->asPHPDocUnionType(); + } + return IntType::instance(false)->asPHPDocUnionType(); + } + + /** + * @return UnionType for the `|` operator + */ + public function visitBinaryBitwiseOr(Node $node): UnionType + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + if ($left->hasNonNullIntType() + && $right->hasNonNullIntType() + ) { + return IntType::instance(false)->asPHPDocUnionType(); + } elseif ($left->hasNonNullStringType() && + $right->hasNonNullStringType()) { + // $x = 'a'; $x |= 'c'; + return StringType::instance(false)->asPHPDocUnionType(); + } + return IntType::instance(false)->asPHPDocUnionType(); + } + + /** + * Analyze the bitwise xor operator. + * + * NOTE: Code can bitwise xor strings byte by byte in PHP + * + * @return UnionType for the `^` operator + */ + public function visitBinaryBitwiseXor(Node $node): UnionType + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + + // TODO: check for other invalid types + if ($left->isExclusivelyArray() + || $right->isExclusivelyArray() + ) { + // TODO: Move these checks into AssignOperatorAnalysisVisitor + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeArrayOperator, + $node->lineno ?? 0, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + $left, + $right + ); + + return UnionType::empty(); + } elseif ($left->hasNonNullIntType() + && $right->hasNonNullIntType() + ) { + return IntType::instance(false)->asPHPDocUnionType(); + } elseif ($left->hasNonNullStringType() + && $right->hasNonNullStringType() + ) { + return StringType::instance(false)->asPHPDocUnionType(); + } + + return IntType::instance(false)->asPHPDocUnionType(); + } + + /** + * @param Node $node @phan-unused-param + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryConcat(Node $node): UnionType + { + return StringType::instance(false)->asRealUnionType(); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryAdd(Node $node): UnionType + { + static $int_or_float_or_array; + static $probably_int_type; + static $probably_array_type; + static $probably_float_type; + static $probably_int_or_float_type; + static $unknown_type; + if ($int_or_float_or_array === null) { + $int_or_float_or_array = [IntType::instance(false), FloatType::instance(false), ArrayType::instance(false)]; + $probably_float_type = UnionType::of([FloatType::instance(false)], $int_or_float_or_array); + $probably_int_or_float_type = UnionType::of([IntType::instance(false), FloatType::instance(false)], $int_or_float_or_array); + $probably_int_type = UnionType::of([IntType::instance(false)], $int_or_float_or_array); + $probably_array_type = UnionType::of([ArrayType::instance(false)], $int_or_float_or_array); + $unknown_type = UnionType::of([], $int_or_float_or_array); + } + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + + // fast-track common cases + if ($left->isNonNullIntType() + && $right->isNonNullIntType() + ) { + return $probably_int_type; + } + + // If both left and right are arrays, then this is array + // concatenation. + if ($left->isGenericArray() && $right->isGenericArray()) { + if ($left->isEqualTo($right)) { + return $left; + } + + return $probably_array_type; + } + + // TODO: isNonNullNumberType + if (($left->isNonNullIntType() + || $left->isType(FloatType::instance(false))) + && ($right->isNonNullIntType() + || $right->isType(FloatType::instance(false))) + ) { + return $probably_float_type; + } + + $left_is_array = ( + !$left->genericArrayElementTypes()->isEmpty() + && $left->nonArrayTypes()->isEmpty() + ) || $left->isType(ArrayType::instance(false)); + + $right_is_array = ( + !$right->genericArrayElementTypes()->isEmpty() + && $right->nonArrayTypes()->isEmpty() + ) || $right->isType(ArrayType::instance(false)); + + if ($left_is_array + && !$right->canCastToUnionType( + ArrayType::instance(false)->asPHPDocUnionType() + ) + ) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeInvalidRightOperand, + $node->lineno ?? 0 + ); + return $unknown_type; + } elseif ($right_is_array + && !$left->canCastToUnionType(ArrayType::instance(false)->asPHPDocUnionType()) + ) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeInvalidLeftOperand, + $node->lineno ?? 0 + ); + return $unknown_type; + } elseif ($left_is_array || $right_is_array) { + // If it is a '+' and we know one side is an array + // and the other is unknown, assume array + return $probably_array_type; + } + + return $probably_int_or_float_type; + } + + /** + * @unused-param $node + * @override + */ + public function visitBinaryDiv(Node $node): UnionType + { + // analyzed in AssignOperatorAnalysisVisitor + return FloatType::instance(false)->asRealUnionType(); + } + + /** @override */ + public function visitBinaryMul(Node $node): UnionType + { + // both sides are analyzed for issues in AssignOperatorAnalysisVisitor + return $this->optimisticAnalyzeNumericOp($node); + } + + /** @override */ + public function visitBinarySub(Node $node): UnionType + { + return $this->optimisticAnalyzeNumericOp($node); + } + + private function optimisticAnalyzeNumericOp(Node $node): UnionType + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['var'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'] + ); + if ($left->hasNonNullIntType() + && $right->hasNonNullIntType() + ) { + static $int_type = null; + return $int_type ?? ($int_type = UnionType::of([IntType::instance(false)], [FloatType::instance(false)])); + } + // analyzed in AssignOperatorAnalysisVisitor + return FloatType::instance(false)->asRealUnionType(); + } + + /** + * @unused-param $node + * @override + */ + public function visitBinaryMod(Node $node): UnionType + { + // analyzed in AssignOperatorAnalysisVisitor + return IntType::instance(false)->asRealUnionType(); + } + + /** + * @unused-param $node + * @override + */ + public function visitBinaryPow(Node $node): UnionType + { + // analyzed in AssignOperatorAnalysisVisitor + return FloatType::instance(false)->asRealUnionType(); + } + + /** + * @unused-param $node + * @override + */ + public function visitBinaryShiftLeft(Node $node): UnionType + { + return IntType::instance(false)->asRealUnionType(); + } + + /** + * @unused-param $node + * @override + */ + public function visitBinaryShiftRight(Node $node): UnionType + { + return IntType::instance(false)->asRealUnionType(); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/AssignmentVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/AssignmentVisitor.php new file mode 100644 index 000000000..998679264 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/AssignmentVisitor.php @@ -0,0 +1,1977 @@ +right_type = $right_type->withSelfResolvedInContext($context)->convertUndefinedToNullable(); + $this->dim_depth = $dim_depth; + $this->dim_type = $dim_type; // null for `$x[] =` or when dim_depth is 0. + $this->assignment_node = $assignment_node; + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node + * A node to analyze as the target of an assignment + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + * + * @throws UnanalyzableException + */ + public function visit(Node $node): Context + { + // TODO: Add more details. + // This should only happen when the polyfill parser is used on invalid ASTs + $this->emitIssue( + Issue::Unanalyzable, + $node->lineno + ); + return $this->context; + } + + // TODO visitNullsafeMethodCall should not be possible on the left hand side? + + /** + * The following is an example of how this would happen. + * (TODO: Check if the right-hand side is an object with offsetSet() or a reference? + * + * ```php + * class C { + * function f() { + * return [ 24 ]; + * } + * } + * (new C)->f()[1] = 42; + * ``` + * + * @param Node $node + * A node to analyze as the target of an assignment + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + */ + public function visitMethodCall(Node $node): Context + { + if ($this->dim_depth >= 2) { + return $this->context; + } + $method_name = $node->children['method']; + + if (!\is_string($method_name)) { + if ($method_name instanceof Node) { + $method_name = UnionTypeVisitor::anyStringLiteralForNode($this->code_base, $this->context, $method_name); + } + if (!\is_string($method_name)) { + return $this->context; + } + } + + try { + $method = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getMethod($method_name, false, true); + $this->checkAssignmentToFunctionResult($node, [$method]); + } catch (Exception $_) { + // ignore it + } + return $this->context; + } + + /** + * The following is an example of how this would happen. + * + * This checks if the left-hand side is a reference. + * + * PhanTypeArraySuspicious covers checking for offsetSet. + * + * ```php + * function &f() { + * $x = [ 24 ]; return $x; + * } + * f()[1] = 42; + * ``` + * + * @param Node $node + * A node to analyze as the target of an assignment + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + */ + public function visitCall(Node $node): Context + { + $expression = $node->children['expr']; + if ($this->dim_depth < 2) { + // Get the function. + // If the function is undefined, always try to create a placeholder from Phan's type signatures for internal functions so they can still be type checked. + $this->checkAssignmentToFunctionResult($node, (new ContextNode( + $this->code_base, + $this->context, + $expression + ))->getFunctionFromNode(true)); + } + return $this->context; + } + + /** + * @param iterable $function_list_generator + */ + private function checkAssignmentToFunctionResult(Node $node, iterable $function_list_generator): void + { + try { + foreach ($function_list_generator as $function) { + if ($function->returnsRef()) { + return; + } + if ($this->dim_depth > 0) { + $return_type = $function->getUnionType(); + if ($return_type->isEmpty()) { + return; + } + if ($return_type->hasPossiblyObjectTypes()) { + // PhanTypeArraySuspicious covers that, though + return; + } + } + } + if (isset($function)) { + $this->emitIssue( + Issue::TypeInvalidCallExpressionAssignment, + $node->lineno, + ASTReverter::toShortString($this->assignment_node->children['var'] ?? $node), + $function->getUnionType() + ); + } + } catch (CodeBaseException $_) { + // ignore it. + } + } + + /** + * The following is an example of how this would happen. + * + * ```php + * class A{ + * function &f() { + * $x = [ 24 ]; return $x; + * } + * } + * A::f()[1] = 42; + * ``` + * + * @param Node $node + * A node to analyze as the target of an assignment + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + */ + public function visitStaticCall(Node $node): Context + { + return $this->visitMethodCall($node); + } + + /** + * This happens for code like the following + * ``` + * list($a) = [1, 2, 3]; + * ``` + * + * @param Node $node + * A node to analyze as the target of an assignment + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + */ + public function visitArray(Node $node): Context + { + $this->checkValidArrayDestructuring($node); + if ($this->right_type->hasTopLevelArrayShapeTypeInstances()) { + $this->analyzeShapedArrayAssignment($node); + } else { + // common case + $this->analyzeGenericArrayAssignment($node); + } + return $this->context; + } + + private function checkValidArrayDestructuring(Node $node): void + { + if (!$node->children) { + $this->emitIssue( + Issue::SyntaxEmptyListArrayDestructuring, + $node->lineno + ); + return; + } + $bitmask = 0; + foreach ($node->children as $c) { + // When $c is null, it's the same as an array entry without a key for purposes of warning. + $bitmask |= (isset($c->children['key']) ? 1 : 2); + if ($bitmask === 3) { + $this->emitIssue( + Issue::SyntaxMixedKeyNoKeyArrayDestructuring, + $c->lineno ?? $node->lineno, + ASTReverter::toShortString($node) + ); + return; + } + } + } + + /** + * Analyzes code such as list($a) = [1, 2, 3]; + * @see self::visitArray() + */ + private function analyzeShapedArrayAssignment(Node $node): void + { + // Figure out the type of elements in the list + $fallback_element_type = null; + /** @suppress PhanAccessMethodInternal */ + $get_fallback_element_type = function () use (&$fallback_element_type): UnionType { + return $fallback_element_type ?? ($fallback_element_type = ( + $this->right_type->genericArrayElementTypes() + ->withRealTypeSet(UnionType::computeRealElementTypesForDestructuringAccess($this->right_type->getRealTypeSet())))); + }; + + $expect_string_keys_lineno = false; + $expect_int_keys_lineno = false; + + $key_set = []; + + foreach ($node->children ?? [] as $child_node) { + // Some times folks like to pass a null to + // a list to throw the element away. I'm not + // here to judge. + if (!($child_node instanceof Node)) { + // Track the element that was thrown away. + $key_set[] = true; + continue; + } + + if ($child_node->kind !== ast\AST_ARRAY_ELEM) { + $this->emitIssue( + Issue::InvalidNode, + $child_node->lineno, + "Spread operator is not supported in assignments" + ); + continue; + } + // Get the key and value nodes for each + // array element we're assigning to + // TODO: Check key types are valid? + $key_node = $child_node->children['key']; + + if ($key_node === null) { + $key_set[] = true; + \end($key_set); + $key_value = \key($key_set); + + $expect_int_keys_lineno = $child_node->lineno; // list($x, $y) = ... is equivalent to list(0 => $x, 1 => $y) = ... + } else { + if ($key_node instanceof Node) { + $key_value = (new ContextNode($this->code_base, $this->context, $key_node))->getEquivalentPHPScalarValue(); + } else { + $key_value = $key_node; + } + if (\is_scalar($key_value)) { + $key_set[$key_value] = true; + if (\is_int($key_value)) { + $expect_int_keys_lineno = $child_node->lineno; + } elseif (\is_string($key_value)) { + $expect_string_keys_lineno = $child_node->lineno; + } + } else { + $key_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $key_node); + $key_type_enum = GenericArrayType::keyTypeFromUnionTypeValues($key_type); + // TODO: Warn about types that can't cast to int|string + if ($key_type_enum === GenericArrayType::KEY_INT) { + $expect_int_keys_lineno = $child_node->lineno; + } elseif ($key_type_enum === GenericArrayType::KEY_STRING) { + $expect_string_keys_lineno = $child_node->lineno; + } + } + } + + if (\is_scalar($key_value)) { + $element_type = UnionTypeVisitor::resolveArrayShapeElementTypesForOffset($this->right_type, $key_value); + if ($element_type === null) { + $element_type = $get_fallback_element_type(); + } elseif ($element_type === false) { + $this->emitIssue( + Issue::TypeInvalidDimOffsetArrayDestructuring, + $child_node->lineno, + StringUtil::jsonEncode($key_value), + ASTReverter::toShortString($child_node), + (string)$this->right_type + ); + $element_type = $get_fallback_element_type(); + } else { + if ($element_type->hasRealTypeSet()) { + $element_type = self::withComputedRealUnionType($element_type, $this->right_type, static function (UnionType $new_right_type) use ($key_value): UnionType { + return UnionTypeVisitor::resolveArrayShapeElementTypesForOffset($new_right_type, $key_value) ?: UnionType::empty(); + }); + } + } + } else { + $element_type = $get_fallback_element_type(); + } + + $this->analyzeValueNodeOfShapedArray($element_type, $child_node->children['value']); + } + + if (!Config::getValue('scalar_array_key_cast')) { + $this->checkMismatchArrayDestructuringKey($expect_int_keys_lineno, $expect_string_keys_lineno); + } + } + + /** + * Utility function to compute accurate real union types + * + * TODO: Move this into a common class such as UnionType? + * @param Closure(UnionType):UnionType $recompute_inferred_type + */ + private static function withComputedRealUnionType(UnionType $inferred_type, UnionType $source_type, Closure $recompute_inferred_type): UnionType + { + if (!$inferred_type->hasRealTypeSet()) { + return $inferred_type; + } + if ($source_type->getRealTypeSet() === $source_type->getTypeSet()) { + return $inferred_type; + } + $real_inferred_type = $recompute_inferred_type($inferred_type->getRealUnionType()); + return $inferred_type->withRealTypeSet($real_inferred_type->getTypeSet()); + } + + /** + * @param Node|string|int|float $value_node + */ + private function analyzeValueNodeOfShapedArray( + UnionType $element_type, + $value_node + ): void { + if (!$value_node instanceof Node) { + return; + } + $kind = $value_node->kind; + if ($kind === \ast\AST_REF) { + $value_node = $value_node->children['expr']; + if (!$value_node instanceof Node) { + return; + } + // TODO: Infer that this is creating or copying a reference [&$a] = [&$b] + } + if ($kind === \ast\AST_VAR) { + $variable = Variable::fromNodeInContext( + $value_node, + $this->context, + $this->code_base, + false + ); + + // Set the element type on each element of + // the list + $this->analyzeSetUnionType($variable, $element_type, $value_node); + + // Note that we're not creating a new scope, just + // adding variables to the existing scope + $this->context->addScopeVariable($variable); + } elseif ($kind === \ast\AST_PROP) { + try { + $property = (new ContextNode( + $this->code_base, + $this->context, + $value_node + ))->getProperty(false, true); + + // Set the element type on each element of + // the list + $this->analyzeSetUnionType($property, $element_type, $value_node); + } catch (UnanalyzableException | NodeException $_) { + // Ignore it. There's nothing we can do. + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + return; + } + } else { + $this->context = (new AssignmentVisitor( + $this->code_base, + $this->context, + $value_node, + $element_type, + 0 + ))->__invoke($value_node); + } + } // TODO: Warn if $value_node is not a node. NativeSyntaxCheckPlugin already does this. + + /** + * Set the element's union type. + * This should be used for warning about assignments such as `$leftHandSide = $str`, but not `is_string($var)`, + * when typed properties could be used. + * + * @param Node|string|int|float|null $node + */ + private function analyzeSetUnionType( + TypedElementInterface $element, + UnionType $element_type, + $node + ): void { + // Let the caller warn about possibly undefined offsets, e.g. ['field' => $value] = ... + // TODO: Convert real types to nullable? + $element_type = $element_type->withIsPossiblyUndefined(false); + $element->setUnionType($element_type); + if ($element instanceof PassByReferenceVariable) { + $assign_node = new Node(ast\AST_ASSIGN, 0, ['expr' => $node], $node->lineno ?? $this->assignment_node->lineno); + self::analyzeSetUnionTypePassByRef($this->code_base, $this->context, $element, $element_type, $assign_node); + } + } + + /** + * Set the element's union type. + * This should be used for warning about assignments such as `$leftHandSide = $str`, but not `is_string($var)`, + * when typed properties could be used. + * + * Static version of analyzeSetUnionType + * + * @param Node|string|int|float $node + */ + public static function analyzeSetUnionTypeInContext( + CodeBase $code_base, + Context $context, + TypedElementInterface $element, + UnionType $element_type, + $node + ): void { + $element->setUnionType($element_type); + if ($element instanceof PassByReferenceVariable) { + self::analyzeSetUnionTypePassByRef( + $code_base, + $context, + $element, + $element_type, + new Node(ast\AST_ASSIGN, 0, ['expr' => $node], $node->lineno ?? $context->getLineNumberStart()) + ); + } + } + + /** + * Set the reference element's union type. + * This should be used for warning about assignments such as `$leftHandSideRef = $str`, but not `is_string($varRef)`, + * when typed properties could be used. + * + * @param Node|string|int|float $node the assignment expression + */ + private static function analyzeSetUnionTypePassByRef( + CodeBase $code_base, + Context $context, + PassByReferenceVariable $reference_element, + UnionType $new_type, + $node + ): void { + $element = $reference_element->getElement(); + while ($element instanceof PassByReferenceVariable) { + $reference_element = $element; + $element = $element->getElement(); + } + if ($element instanceof Property) { + $real_union_type = $element->getRealUnionType(); + if (!$real_union_type->isEmpty() && !$new_type->getRealUnionType()->canCastToDeclaredType($code_base, $context, $real_union_type)) { + $reference_context = $reference_element->getContextOfCreatedReference(); + if ($reference_context) { + // Here, we emit the issue at the place where the reference was created, + // since that's the code that can be changed or where issues should be suppressed. + Issue::maybeEmit( + $code_base, + $reference_context, + Issue::TypeMismatchPropertyRealByRef, + $reference_context->getLineNumberStart(), + isset($node->children['expr']) ? ASTReverter::toShortString($node->children['expr']) : '(unknown)', + $new_type, + $element->getRepresentationForIssue(), + $real_union_type, + $context->getFile(), + $node->lineno ?? $context->getLineNumberStart() + ); + } + return; + } + if (!$new_type->asExpandedTypes($code_base)->canCastToUnionType($element->getPHPDocUnionType())) { + $reference_context = $reference_element->getContextOfCreatedReference(); + if ($reference_context) { + Issue::maybeEmit( + $code_base, + $reference_context, + Issue::TypeMismatchPropertyByRef, + $reference_context->getLineNumberStart(), + isset($node->children['expr']) ? ASTReverter::toShortString($node->children['expr']) : '(unknown)', + $new_type, + $element->getRepresentationForIssue(), + $element->getPHPDocUnionType(), + $context->getFile(), + $node->lineno ?? $context->getLineNumberStart() + ); + } + } + } + } + + /** + * Analyzes code such as list($a) = function_returning_array(); + * @param Node $node the ast\AST_ARRAY node on the left hand side of the assignment + * @see self::visitArray() + */ + private function analyzeGenericArrayAssignment(Node $node): void + { + // Figure out the type of elements in the list + $right_type = $this->right_type; + if ($right_type->isEmpty()) { + $element_type = UnionType::empty(); + } else { + $array_access_types = $right_type->asArrayOrArrayAccessSubTypes($this->code_base); + if ($array_access_types->isEmpty()) { + $this->emitIssue( + Issue::TypeInvalidExpressionArrayDestructuring, + $node->lineno, + $this->getAssignedExpressionString(), + $right_type, + 'array|ArrayAccess' + ); + } + $element_type = + $array_access_types->genericArrayElementTypes() + ->withRealTypeSet(UnionType::computeRealElementTypesForDestructuringAccess($right_type->getRealTypeSet())); + // @phan-suppress-previous-line PhanAccessMethodInternal + } + + $expect_string_keys_lineno = false; + $expect_int_keys_lineno = false; + + $scalar_array_key_cast = Config::getValue('scalar_array_key_cast'); + + foreach ($node->children ?? [] as $child_node) { + // Some times folks like to pass a null to + // a list to throw the element away. I'm not + // here to judge. + if (!($child_node instanceof Node)) { + continue; + } + if ($child_node->kind !== ast\AST_ARRAY_ELEM) { + $this->emitIssue( + Issue::InvalidNode, + $child_node->lineno, + "Spread operator is not supported in assignments" + ); + continue; + } + + // Get the key and value nodes for each + // array element we're assigning to + // TODO: Check key types are valid? + $key_node = $child_node->children['key']; + if (!$scalar_array_key_cast) { + if ($key_node === null) { + $expect_int_keys_lineno = $child_node->lineno; // list($x, $y) = ... is equivalent to list(0 => $x, 1 => $y) = ... + } else { + $key_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $key_node); + $key_type_enum = GenericArrayType::keyTypeFromUnionTypeValues($key_type); + // TODO: Warn about types that can't cast to int|string + if ($key_type_enum === GenericArrayType::KEY_INT) { + $expect_int_keys_lineno = $child_node->lineno; + } elseif ($key_type_enum === GenericArrayType::KEY_STRING) { + $expect_string_keys_lineno = $child_node->lineno; + } + } + } + + $value_node = $child_node->children['value']; + if (!($value_node instanceof Node)) { + // Skip non-nodes to avoid crash + // TODO: Emit a new issue type for https://github.com/phan/phan/issues/1693 + } elseif ($value_node->kind === \ast\AST_VAR) { + $variable = Variable::fromNodeInContext( + $value_node, + $this->context, + $this->code_base, + false + ); + + // Set the element type on each element of + // the list + $this->analyzeSetUnionType($variable, $element_type, $value_node); + + // Note that we're not creating a new scope, just + // adding variables to the existing scope + $this->context->addScopeVariable($variable); + } elseif ($value_node->kind === \ast\AST_PROP) { + try { + $property = (new ContextNode( + $this->code_base, + $this->context, + $value_node + ))->getProperty(false, true); + + // Set the element type on each element of + // the list + $this->analyzeSetUnionType($property, $element_type, $value_node); + } catch (UnanalyzableException | NodeException $_) { + // Ignore it. There's nothing we can do. + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + continue; + } + } else { + $this->context = (new AssignmentVisitor( + $this->code_base, + $this->context, + $value_node, + $element_type, + 0 + ))->__invoke($value_node); + } + } + + $this->checkMismatchArrayDestructuringKey($expect_int_keys_lineno, $expect_string_keys_lineno); + } + + /** + * @param int|false $expect_int_keys_lineno + * @param int|false $expect_string_keys_lineno + */ + private function checkMismatchArrayDestructuringKey($expect_int_keys_lineno, $expect_string_keys_lineno): void + { + if ($expect_int_keys_lineno !== false || $expect_string_keys_lineno !== false) { + $right_hand_key_type = GenericArrayType::keyTypeFromUnionTypeKeys($this->right_type); + if ($expect_int_keys_lineno !== false && ($right_hand_key_type & GenericArrayType::KEY_INT) === 0) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeMismatchArrayDestructuringKey, + $expect_int_keys_lineno, + 'int', + 'string' + ); + } elseif ($expect_string_keys_lineno !== false && ($right_hand_key_type & GenericArrayType::KEY_STRING) === 0) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeMismatchArrayDestructuringKey, + $expect_string_keys_lineno, + 'string', + 'int' + ); + } + } + } + + /** + * @param Node $node + * A node to analyze as the target of an assignment + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + */ + public function visitDim(Node $node): Context + { + $expr_node = $node->children['expr']; + if (!($expr_node instanceof Node)) { + $this->emitIssue( + Issue::InvalidWriteToTemporaryExpression, + $node->lineno, + ASTReverter::toShortString($node), + Type::fromObject($expr_node) + ); + return $this->context; + } + if ($expr_node->kind === \ast\AST_VAR) { + $variable_name = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getVariableName(); + + if (Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())) { + if ($variable_name === 'GLOBALS') { + return $this->analyzeSuperglobalDim($node, $variable_name); + } + if (!$this->context->getScope()->hasVariableWithName($variable_name)) { + $this->context->addScopeVariable(new Variable( + $this->context->withLineNumberStart($expr_node->lineno), + $variable_name, + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name), + 0 + )); + } + } + } + + // TODO: Check if the unionType is valid for the [] + // For most types, it should be int|string, but SplObjectStorage and a few user-defined types will be exceptions. + // Infer it from offsetSet? + $dim_node = $node->children['dim']; + if ($dim_node instanceof Node) { + // TODO: Use ContextNode to infer dim_value + $dim_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $dim_node + ); + $dim_value = $dim_type->asSingleScalarValueOrNullOrSelf(); + } elseif (\is_scalar($dim_node)) { + $dim_value = $dim_node; + $dim_type = Type::fromObject($dim_node)->asRealUnionType(); + } else { + // TODO: If the array shape has only one set of keys, then appending should add to that shape? Possibly not a common use case. + $dim_type = null; + $dim_value = null; + } + + if ($dim_type !== null && !\is_object($dim_value)) { + // TODO: This is probably why Phan has bugs with multi-dimensional assignment adding new union types instead of combining with existing ones. + $right_type = ArrayShapeType::fromFieldTypes([ + $dim_value => $this->right_type, + ], false)->asRealUnionType(); + } else { + // Make the right type a generic (i.e. int -> int[]) + if ($dim_node !== null) { + if ($dim_type !== null) { + $key_type_enum = GenericArrayType::keyTypeFromUnionTypeValues($dim_type); + } else { + $key_type_enum = GenericArrayType::KEY_MIXED; + } + $right_inner_type = $this->right_type; + if ($right_inner_type->isEmpty()) { + $right_type = GenericArrayType::fromElementType(MixedType::instance(false), false, $key_type_enum)->asRealUnionType(); + } else { + $right_type = $right_inner_type->asGenericArrayTypes($key_type_enum); + } + } else { + $right_type = $this->right_type->asNonEmptyListTypes()->nonFalseyClone(); + } + if (!$right_type->hasRealTypeSet()) { + $right_type = $right_type->withRealTypeSet(UnionType::typeSetFromString('non-empty-array')); + } + } + + // Recurse into whatever we're []'ing + $context = (new AssignmentVisitor( + $this->code_base, + $this->context, + $this->assignment_node, + $right_type, + $this->dim_depth + 1, + $dim_type + ))->__invoke($expr_node); + + return $context; + } + + /** + * Analyze an assignment where $variable_name is a superglobal, and return the new context. + * May create a new variable in $this->context. + * TODO: Emit issues if the assignment is incompatible with the pre-existing type? + */ + private function analyzeSuperglobalDim(Node $node, string $variable_name): Context + { + $dim = $node->children['dim']; + if ('GLOBALS' === $variable_name) { + if (!\is_string($dim)) { + // You're not going to believe this, but I just + // found a piece of code like $GLOBALS[mt_rand()]. + // Super weird, right? + return $this->context; + } + + if (Variable::isHardcodedVariableInScopeWithName($dim, $this->context->isInGlobalScope())) { + // Don't override types of superglobals such as $_POST, $argv through $_GLOBALS['_POST'] = expr either. TODO: Warn. + return $this->context; + } + + $variable = new Variable( + $this->context, + $dim, + $this->right_type, + 0 + ); + + $this->context->addGlobalScopeVariable( + $variable + ); + } + // TODO: Assignment sanity checks. + return $this->context; + } + + // TODO: visitNullsafeProp should not be possible on the left hand side? + + /** + * @param Node $node + * A node to analyze as the target of an assignment. + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + */ + public function visitProp(Node $node): Context + { + // Get class list first, warn if the class list is invalid. + try { + $class_list = (new ContextNode( + $this->code_base, + $this->context, + $node->children['expr'] + ))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT, Issue::TypeExpectedObjectPropAccess); + } catch (\Exception $_) { + // If we can't figure out what kind of a class + // this is, don't worry about it. + // + // Note that CodeBaseException is one possible exception due to invalid code created by the fallback parser, etc. + return $this->context; + } + + $property_name = $node->children['prop']; + if ($property_name instanceof Node) { + $property_name = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $property_name)->asSingleScalarValueOrNull(); + } + + // Things like $foo->$bar + if (!\is_string($property_name)) { + return $this->context; + } + $expr_node = $node->children['expr']; + if ($expr_node instanceof Node && + $expr_node->kind === \ast\AST_VAR && + $expr_node->children['name'] === 'this') { + $this->handleThisPropertyAssignmentInLocalScopeByName($node, $property_name); + } + + foreach ($class_list as $clazz) { + // Check to see if this class has the property or + // a setter + if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) { + if (!$clazz->hasMethodWithName($this->code_base, '__set', true)) { + continue; + } + } + + try { + $property = $clazz->getPropertyByNameInContext( + $this->code_base, + $property_name, + $this->context, + false, + $node, + true + ); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + return $this->context; + } + try { + return $this->analyzePropAssignment($clazz, $property, $node); + } catch (RecursionDepthException $_) { + return $this->context; + } + } + + // Check if it is a built in class with dynamic properties but (possibly) no __set, such as SimpleXMLElement or stdClass or V8Js + $is_class_with_arbitrary_types = isset($class_list[0]) ? $class_list[0]->hasDynamicProperties($this->code_base) : false; + + if ($is_class_with_arbitrary_types || Config::getValue('allow_missing_properties')) { + try { + // Create the property + $property = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getOrCreateProperty($property_name, false); + + $this->addTypesToProperty($property, $node); + } catch (\Exception $_) { + // swallow it + } + } elseif (\count($class_list) > 0) { + foreach ($class_list as $clazz) { + if ($clazz->hasDynamicProperties($this->code_base)) { + return $this->context; + } + } + $first_class = $class_list[0]; + $this->emitIssueWithSuggestion( + Issue::UndeclaredProperty, + $node->lineno ?? 0, + ["{$first_class->getFQSEN()}->$property_name"], + IssueFixSuggester::suggestSimilarProperty( + $this->code_base, + $this->context, + $first_class, + $property_name, + false + ) + ); + } else { + // If we hit this part, we couldn't figure out + // the class, so we ignore the issue + } + + return $this->context; + } + + /** + * This analyzes an assignment to an instance or static property. + * + * @param Node $node the left hand side of the assignment + */ + private function analyzePropAssignment(Clazz $clazz, Property $property, Node $node): Context + { + if ($property->isReadOnly()) { + $this->analyzeAssignmentToReadOnlyProperty($property, $node); + } + // TODO: Iterate over individual types, don't look at the whole type at once? + + // If we're assigning to an array element then we don't + // know what the array structure of the parameter is + // outside of the scope of this assignment, so we add to + // its union type rather than replace it. + $property_union_type = $property->getPHPDocUnionType()->withStaticResolvedInContext($this->context); + $resolved_right_type = $this->right_type->withStaticResolvedInContext($this->context); + if ($this->dim_depth > 0) { + if ($resolved_right_type->canCastToExpandedUnionType( + $property_union_type, + $this->code_base + )) { + $this->addTypesToProperty($property, $node); + if (Config::get_strict_property_checking() && $resolved_right_type->typeCount() > 1) { + $this->analyzePropertyAssignmentStrict($property, $resolved_right_type, $node); + } + } elseif ($property_union_type->asExpandedTypes($this->code_base)->hasArrayAccess()) { + // Add any type if this is a subclass with array access. + $this->addTypesToProperty($property, $node); + } else { + // Convert array shape types to generic arrays to reduce false positive PhanTypeMismatchProperty instances. + + // TODO: If the codebase explicitly sets a phpdoc array shape type on a property assignment, + // then preserve the array shape type. + $new_types = $this->typeCheckDimAssignment($property_union_type, $node) + ->withFlattenedArrayShapeOrLiteralTypeInstances() + ->withStaticResolvedInContext($this->context); + + // TODO: More precise than canCastToExpandedUnionType + if (!$new_types->canCastToExpandedUnionType( + $property_union_type, + $this->code_base + )) { + // echo "Emitting warning for $new_types\n"; + // TODO: Don't emit if array shape type is compatible with the original value of $property_union_type + $this->emitTypeMismatchPropertyIssue( + $node, + $property, + $resolved_right_type, + $new_types, + $property_union_type + ); + } else { + if (Config::get_strict_property_checking() && $resolved_right_type->typeCount() > 1) { + $this->analyzePropertyAssignmentStrict($property, $resolved_right_type, $node); + } + $this->right_type = $new_types; + $this->addTypesToProperty($property, $node); + } + } + return $this->context; + } elseif ($clazz->isPHPInternal() && $clazz->getFQSEN() !== FullyQualifiedClassName::getStdClassFQSEN()) { + // We don't want to modify the types of internal classes such as \ast\Node even if they are compatible + // This would result in unpredictable results, and types which are more specific than they really are. + // stdClass is an exception to this, for issues such as https://github.com/phan/phan/pull/700 + return $this->context; + } else { + // This is a regular assignment, not an assignment to an offset + if (!$resolved_right_type->canCastToExpandedUnionType( + $property_union_type, + $this->code_base + ) + && !($resolved_right_type->hasTypeInBoolFamily() && $property_union_type->hasTypeInBoolFamily()) + && !$clazz->hasDynamicProperties($this->code_base) + && !$property->isDynamicProperty() + ) { + if ($resolved_right_type->nonNullableClone()->canCastToExpandedUnionType($property_union_type, $this->code_base) && + !$resolved_right_type->isType(NullType::instance(false))) { + if ($this->shouldSuppressIssue(Issue::TypeMismatchProperty, $node->lineno)) { + return $this->context; + } + $this->emitIssue( + Issue::PossiblyNullTypeMismatchProperty, + $node->lineno, + ASTReverter::toShortString($node), + (string)$this->right_type->withUnionType($resolved_right_type), + $property->getRepresentationForIssue(), + (string)$property_union_type, + 'null' + ); + } else { + // echo "Emitting warning for {$resolved_right_type->asExpandedTypes($this->code_base)} to {$property_union_type->asExpandedTypes($this->code_base)}\n"; + $this->emitTypeMismatchPropertyIssue($node, $property, $resolved_right_type, $this->right_type->withUnionType($resolved_right_type), $property_union_type); + } + return $this->context; + } + + if (Config::get_strict_property_checking() && $this->right_type->typeCount() > 1) { + $this->analyzePropertyAssignmentStrict($property, $this->right_type, $node); + } + } + + // After having checked it, add this type to it + $this->addTypesToProperty($property, $node); + + return $this->context; + } + + /** + * @param UnionType $resolved_right_type the type of the expression to use when checking for real type mismatches + * @param UnionType $warn_type the type to use in issue messages + */ + private function emitTypeMismatchPropertyIssue( + Node $node, + Property $property, + UnionType $resolved_right_type, + UnionType $warn_type, + UnionType $property_union_type + ): void { + if ($this->context->hasSuppressIssue($this->code_base, Issue::TypeMismatchPropertyReal)) { + return; + } + if (self::isRealMismatch($this->code_base, $property->getRealUnionType(), $resolved_right_type)) { + $this->emitIssue( + Issue::TypeMismatchPropertyReal, + $node->lineno, + $this->getAssignedExpressionString(), + $warn_type, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($warn_type), + $property->getRepresentationForIssue(), + $property_union_type, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($property_union_type) + ); + return; + } + if ($this->context->hasSuppressIssue($this->code_base, Issue::TypeMismatchPropertyProbablyReal)) { + return; + } + if ($resolved_right_type->hasRealTypeSet() && + !$resolved_right_type->getRealUnionType()->canCastToDeclaredType($this->code_base, $this->context, $property_union_type)) { + $this->emitIssue( + Issue::TypeMismatchPropertyProbablyReal, + $node->lineno, + $this->getAssignedExpressionString(), + $warn_type, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($warn_type), + $property->getRepresentationForIssue(), + $property_union_type, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($property_union_type) + ); + return; + } + $this->emitIssue( + Issue::TypeMismatchProperty, + $node->lineno, + $this->getAssignedExpressionString(), + $warn_type, + $property->getRepresentationForIssue(), + $property_union_type + ); + } + + private function getAssignedExpressionString(): string + { + $expr = $this->assignment_node->children['expr'] ?? null; + if ($expr === null) { + return '(unknown)'; + } + $str = ASTReverter::toShortString($expr); + if ($this->dim_depth > 0) { + return "($str as a field)"; + } + return $str; + } + + /** + * Returns true if Phan should emit a more severe issue type for real type mismatch + */ + private static function isRealMismatch(CodeBase $code_base, UnionType $real_property_type, UnionType $real_actual_type): bool + { + if ($real_property_type->isEmpty()) { + return false; + } + return !$real_actual_type->asExpandedTypes($code_base)->isStrictSubtypeOf($code_base, $real_property_type); + } + + /** + * Modifies $this->context (if needed) to track the assignment to a property of $this within a function-like. + * This handles conditional branches. + * @param string $prop_name + * TODO: If $this->right_type is the empty union type and the property is declared, assume the phpdoc/real types instead of the empty union type. + */ + private function handleThisPropertyAssignmentInLocalScopeByName(Node $node, string $prop_name): void + { + if ($this->dim_depth === 0) { + $new_type = $this->right_type; + } else { + // Copied from visitVar + $old_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node); + $right_type = $this->typeCheckDimAssignment($old_type, $node); + $old_type = $old_type->nonNullableClone(); + if ($old_type->isEmpty()) { + $old_type = ArrayType::instance(false)->asPHPDocUnionType(); + } + + if ($this->dim_depth > 1) { + $new_type = $this->computeTypeOfMultiDimensionalAssignment($old_type, $right_type); + } elseif ($old_type->hasTopLevelNonArrayShapeTypeInstances() || $right_type->hasTopLevelNonArrayShapeTypeInstances() || $right_type->isEmpty()) { + $new_type = $old_type->withUnionType($right_type); + } else { + $new_type = ArrayType::combineArrayTypesOverriding($right_type, $old_type, true); + } + } + $this->context = $this->context->withThisPropertySetToTypeByName($prop_name, $new_type); + } + + private function analyzeAssignmentToReadOnlyProperty(Property $property, Node $node): void + { + $is_from_phpdoc = $property->isFromPHPDoc(); + $context = $property->getContext(); + if (!$is_from_phpdoc && $this->context->isInFunctionLikeScope()) { + $method = $this->context->getFunctionLikeInScope($this->code_base); + if ($method instanceof Method && strcasecmp($method->getName(), '__construct') === 0) { + $class_type = $method->getClassFQSEN()->asType(); + if ($class_type->asExpandedTypes($this->code_base)->hasType($property->getClassFQSEN()->asType())) { + // This is a constructor setting its own properties or a base class's properties. + // TODO: Could support private methods + return; + } + } + } + $this->emitIssue( + $is_from_phpdoc ? Issue::AccessReadOnlyMagicProperty : Issue::AccessReadOnlyProperty, + $node->lineno ?? 0, + $property->asPropertyFQSENString(), + $context->getFile(), + $context->getLineNumberStart() + ); + } + + private function analyzePropertyAssignmentStrict(Property $property, UnionType $assignment_type, Node $node): void + { + $type_set = $assignment_type->getTypeSet(); + if (\count($type_set) < 2) { + throw new AssertionError('Expected to have at least two types when checking if types match in strict mode'); + } + + $property_union_type = $property->getUnionType(); + if ($property_union_type->hasTemplateTypeRecursive()) { + $property_union_type = $property_union_type->asExpandedTypes($this->code_base); + } + + $mismatch_type_set = UnionType::empty(); + $mismatch_expanded_types = null; + + // For the strict + foreach ($type_set as $type) { + // Expand it to include all parent types up the chain + $individual_type_expanded = $type->asExpandedTypes($this->code_base); + + // See if the argument can be cast to the + // parameter + if (!$individual_type_expanded->canCastToUnionType( + $property_union_type + )) { + $mismatch_type_set = $mismatch_type_set->withType($type); + if ($mismatch_expanded_types === null) { + // Warn about the first type + $mismatch_expanded_types = $individual_type_expanded; + } + } + } + + + if ($mismatch_expanded_types === null) { + // No mismatches + return; + } + if ($this->shouldSuppressIssue(Issue::TypeMismatchPropertyReal, $node->lineno) || + $this->shouldSuppressIssue(Issue::TypeMismatchPropertyProbablyReal, $node->lineno) || + $this->shouldSuppressIssue(Issue::TypeMismatchProperty, $node->lineno) + ) { + // TypeMismatchProperty also suppresses PhanPossiblyNullTypeMismatchProperty, etc. + return; + } + + $this->emitIssue( + self::getStrictPropertyMismatchIssueType($mismatch_type_set), + $node->lineno, + ASTReverter::toShortString($node), + (string)$this->right_type, + $property->getRepresentationForIssue(), + (string)$property_union_type, + (string)$mismatch_expanded_types + ); + } + + private static function getStrictPropertyMismatchIssueType(UnionType $union_type): string + { + if ($union_type->typeCount() === 1) { + $type = $union_type->getTypeSet()[0]; + if ($type instanceof NullType) { + return Issue::PossiblyNullTypeMismatchProperty; + } + if ($type instanceof FalseType) { + return Issue::PossiblyFalseTypeMismatchProperty; + } + } + return Issue::PartialTypeMismatchProperty; + } + + /** + * Based on AssignmentVisitor->addTypesToProperty + * Used for analyzing reference parameters' possible effects on properties. + * @internal the API will likely change + */ + public static function addTypesToPropertyStandalone( + CodeBase $code_base, + Context $context, + Property $property, + UnionType $new_types + ): void { + $original_property_types = $property->getUnionType(); + if ($property->getRealUnionType()->isEmpty() && $property->getPHPDocUnionType()->isEmpty()) { + $property->setUnionType( + $new_types + ->eraseRealTypeSetRecursively() + ->withUnionType($property->getUnionType()->eraseRealTypeSetRecursively()) + ->withStaticResolvedInContext($context) + ->withFlattenedArrayShapeOrLiteralTypeInstances() + ); + return; + } + if ($original_property_types->isEmpty()) { + // TODO: Be more precise? + $property->setUnionType( + $new_types + ->withStaticResolvedInContext($context) + ->withFlattenedArrayShapeOrLiteralTypeInstances() + ->withRealTypeSet($property->getRealUnionType()->getTypeSet()) + ); + return; + } + + $has_literals = $original_property_types->hasLiterals(); + $new_types = $new_types->withStaticResolvedInContext($context)->withFlattenedArrayShapeTypeInstances(); + + $updated_property_types = $original_property_types; + foreach ($new_types->getTypeSet() as $new_type) { + if ($new_type instanceof MixedType) { + // Don't add MixedType to a non-empty property - It makes inferences on that property useless. + continue; + } + + // Only allow compatible types to be added to declared properties. + // Allow anything to be added to dynamic properties. + // TODO: Be more permissive about declared properties without phpdoc types. + if (!$new_type->asExpandedTypes($code_base)->canCastToUnionType($original_property_types) && !$property->isDynamicProperty()) { + continue; + } + + // Check for adding a specific array to as generic array as a workaround for #1783 + if (\get_class($new_type) === ArrayType::class && $original_property_types->hasGenericArray()) { + continue; + } + if (!$has_literals) { + $new_type = $new_type->asNonLiteralType(); + } + $updated_property_types = $updated_property_types->withType($new_type); + } + + // TODO: Add an option to check individual types, not just the whole union type? + // If that is implemented, verify that generic arrays will properly cast to regular arrays (public $x = [];) + $property->setUnionType($updated_property_types->withRealTypeSet($property->getRealUnionType()->getTypeSet())); + } + + + + /** + * @param Property $property - The property which should have types added to it + */ + private function addTypesToProperty(Property $property, Node $node): void + { + if ($property->getRealUnionType()->isEmpty() && $property->getPHPDocUnionType()->isEmpty()) { + $property->setUnionType( + $this->right_type + ->withUnionType($property->getUnionType()) + ->withStaticResolvedInContext($this->context) + ->withFlattenedArrayShapeOrLiteralTypeInstances() + ->eraseRealTypeSetRecursively() + ); + return; + } + $original_property_types = $property->getUnionType(); + if ($original_property_types->isEmpty()) { + // TODO: Be more precise? + $property->setUnionType( + $this->right_type + ->withStaticResolvedInContext($this->context) + ->withFlattenedArrayShapeOrLiteralTypeInstances() + ->eraseRealTypeSetRecursively() + ->withRealTypeSet($property->getRealUnionType()->getTypeSet()) + ); + return; + } + + if ($this->dim_depth > 0) { + $new_types = $this->typeCheckDimAssignment($original_property_types, $node); + } else { + $new_types = $this->right_type; + } + $has_literals = $original_property_types->hasLiterals(); + $new_types = $new_types->withStaticResolvedInContext($this->context)->withFlattenedArrayShapeTypeInstances(); + + $updated_property_types = $original_property_types; + foreach ($new_types->getTypeSet() as $new_type) { + if ($new_type instanceof MixedType) { + // Don't add MixedType to a non-empty property - It makes inferences on that property useless. + continue; + } + + // Only allow compatible types to be added to declared properties. + // Allow anything to be added to dynamic properties. + // TODO: Be more permissive about declared properties without phpdoc types. + if (!$new_type->asExpandedTypes($this->code_base)->canCastToUnionType($original_property_types) && !$property->isDynamicProperty()) { + continue; + } + + // Check for adding a specific array to as generic array as a workaround for #1783 + if (\get_class($new_type) === ArrayType::class && $original_property_types->hasGenericArray()) { + continue; + } + if (!$has_literals) { + $new_type = $new_type->asNonLiteralType(); + } + $updated_property_types = $updated_property_types->withType($new_type); + } + + // TODO: Add an option to check individual types, not just the whole union type? + // If that is implemented, verify that generic arrays will properly cast to regular arrays (public $x = [];) + $property->setUnionType($updated_property_types->withRealTypeSet($property->getRealUnionType()->getTypeSet())); + } + + /** + * @param Node $node + * A node to analyze as the target of an assignment. + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + * + * @see self::visitProp() + */ + public function visitStaticProp(Node $node): Context + { + $property_name = $node->children['prop']; + + // Things like self::${$x} + if (!\is_string($property_name)) { + return $this->context; + } + + try { + $class_list = (new ContextNode( + $this->code_base, + $this->context, + $node->children['class'] + ))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME, Issue::TypeExpectedObjectStaticPropAccess); + } catch (\Exception $_) { + // If we can't figure out what kind of a class + // this is, don't worry about it + // + // Note that CodeBaseException is one possible exception due to invalid code created by the fallback parser, etc. + return $this->context; + } + + foreach ($class_list as $clazz) { + // Check to see if this class has the property + if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) { + continue; + } + + try { + // Look for static properties with that $property_name + $property = $clazz->getPropertyByNameInContext( + $this->code_base, + $property_name, + $this->context, + true, + null, + true + ); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + return $this->context; + } + + try { + return $this->analyzePropAssignment($clazz, $property, $node); + } catch (RecursionDepthException $_) { + return $this->context; + } + } + + if (\count($class_list) > 0) { + $this->emitIssue( + Issue::UndeclaredStaticProperty, + $node->lineno ?? 0, + $property_name, + (string)$class_list[0]->getFQSEN() + ); + } else { + // If we hit this part, we couldn't figure out + // the class, so we ignore the issue + } + + return $this->context; + } + + /** + * @param Node $node + * A node of type ast\AST_VAR to analyze as the target of an assignment + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + */ + public function visitVar(Node $node): Context + { + try { + $variable_name = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getVariableName(); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + return $this->context; + } + // Don't analyze variables when we can't determine their names. + if ($variable_name === '') { + return $this->context; + } + + if ($this->context->getScope()->hasVariableWithName($variable_name)) { + $variable = $this->context->getScope()->getVariableByName($variable_name); + } else { + $variable_type = Variable::getUnionTypeOfHardcodedVariableInScopeWithName( + $variable_name, + $this->context->isInGlobalScope() + ); + if ($variable_type) { + $variable = new Variable( + $this->context, + $variable_name, + $variable_type, + 0 + ); + } else { + $variable = null; + } + } + // Check to see if the variable already exists + if ($variable) { + // We clone the variable so as to not disturb its previous types + // as we replace it. + $variable = clone($variable); + + // If we're assigning to an array element then we don't + // know what the array structure of the parameter is + // outside of the scope of this assignment, so we add to + // its union type rather than replace it. + if ($this->dim_depth > 0) { + $old_variable_union_type = $variable->getUnionType(); + if ($this->dim_depth === 1 && $old_variable_union_type->getRealUnionType()->isExclusivelyArray()) { + // We're certain of the types of $values, but not of $values[0], so check that the depth is exactly 1. + // @phan-suppress-next-line PhanUndeclaredProperty used in unused variable detection - array access to an object might have a side effect + $node->phan_is_assignment_to_real_array = true; + } + $right_type = $this->typeCheckDimAssignment($old_variable_union_type, $node); + if ($old_variable_union_type->isEmpty()) { + $old_variable_union_type = ArrayType::instance(false)->asPHPDocUnionType(); + } + // Note: Trying to assign dim offsets to a scalar such as `$x = 2` does not modify the variable. + $old_variable_union_type = $old_variable_union_type->nonNullableClone(); + // TODO: Make the behavior more precise for $x['a']['b'] = ...; when $x is an array shape. + if ($this->dim_depth > 1) { + $new_union_type = $this->computeTypeOfMultiDimensionalAssignment($old_variable_union_type, $right_type); + } elseif ($old_variable_union_type->isEmpty() || $old_variable_union_type->hasPossiblyObjectTypes() || $right_type->hasTopLevelNonArrayShapeTypeInstances() || $right_type->isEmpty()) { + $new_union_type = $old_variable_union_type->withUnionType( + $right_type + ); + // echo "Combining array shape types $right_type $old_variable_union_type $new_union_type\n"; + } else { + $new_union_type = ArrayType::combineArrayTypesOverriding( + $right_type, + $old_variable_union_type, + true + ); + } + // Note that after $x[anything] = anything, $x is guaranteed not to be the empty array. + // TODO: Handle `$x = 'x'; $s[0] = '0';` + $this->analyzeSetUnionType($variable, $new_union_type->nonFalseyClone(), $this->assignment_node->children['expr'] ?? null); + } else { + $this->analyzeSetUnionType($variable, $this->right_type, $this->assignment_node->children['expr'] ?? null); + } + + $this->context->addScopeVariable( + $variable + ); + + return $this->context; + } + + // no such variable exists, check for invalid array Dim access + if ($this->dim_depth > 0) { + $this->emitIssue( + Issue::UndeclaredVariableDim, + $node->lineno ?? 0, + $variable_name + ); + } + + $variable = new Variable( + $this->context, + $variable_name, + UnionType::empty(), + 0 + ); + if ($this->dim_depth > 0) { + // Reduce false positives: If $variable did not already exist, assume it may already have other array fields + // (e.g. in a loop, or in the global scope) + // TODO: Don't if this isn't in a loop or the global scope. + $variable->setUnionType($this->right_type->withType(ArrayType::instance(false))); + } else { + // Set that type on the variable + $variable->setUnionType( + $this->right_type + ); + if ($this->assignment_node->kind === ast\AST_ASSIGN_REF) { + $expr = $this->assignment_node->children['expr']; + if ($expr instanceof Node && \in_array($expr->kind, [ast\AST_STATIC_PROP, ast\AST_PROP], true)) { + try { + $property = (new ContextNode( + $this->code_base, + $this->context, + $expr + ))->getProperty($expr->kind === ast\AST_STATIC_PROP); + $variable = new PassByReferenceVariable( + $variable, + $property, + $this->code_base, + $this->context + ); + } catch (IssueException | NodeException $_) { + // Hopefully caught elsewhere + } + } + } + } + + // Note that we're not creating a new scope, just + // adding variables to the existing scope + $this->context->addScopeVariable($variable); + + return $this->context; + } + + private function computeTypeOfMultiDimensionalAssignment(UnionType $old_union_type, UnionType $right_type): UnionType + { + if ($this->dim_depth <= 1) { + throw new AssertionError("Expected dim_depth > 1, got $this->dim_depth"); + } + if (!$right_type->hasTopLevelArrayShapeTypeInstances() || !$old_union_type->hasTopLevelArrayShapeTypeInstances()) { + return $old_union_type->withUnionType($right_type); + } + + return UnionType::of( + self::computeTypeSetOfMergedArrayShapeTypes($old_union_type->getTypeSet(), $right_type->getTypeSet(), $this->dim_depth, false), + self::computeTypeSetOfMergedArrayShapeTypes($old_union_type->getRealTypeSet(), $right_type->getRealTypeSet(), $this->dim_depth, true) + ); + } + + /** + * @param list $old_type_set may contain ArrayShapeType instances + * @param list $new_type_set may contain ArrayShapeType instances + * @return list possibly containing duplicates. + * TODO: Handle $this->dim_depth of more than 2 + */ + private static function computeTypeSetOfMergedArrayShapeTypes(array $old_type_set, array $new_type_set, int $dim_depth, bool $is_real): array + { + if ($is_real) { + if (!$old_type_set || !$new_type_set) { + return []; + } + } + $result = []; + $new_array_shape_types = []; + foreach ($new_type_set as $type) { + if ($type instanceof ArrayShapeType) { + $new_array_shape_types[] = $type; + } else { + $result[] = $type; + } + } + if (!$new_array_shape_types) { + return \array_merge($old_type_set, $new_type_set); + } + $old_array_shape_types = []; + foreach ($old_type_set as $type) { + if ($type instanceof ArrayShapeType) { + $old_array_shape_types[] = $type; + } else { + $result[] = $type; + } + } + if (!$old_array_shape_types) { + return \array_merge($old_type_set, $new_type_set); + } + // Postcondition: $old_array_shape_types and $new_array_shape_types are non-empty lists of ArrayShapeTypes + $old_array_shape_type = ArrayShapeType::union($old_array_shape_types); + $new_array_shape_type = ArrayShapeType::union($new_array_shape_types); + $combined_fields = $old_array_shape_type->getFieldTypes(); + foreach ($new_array_shape_type->getFieldTypes() as $field => $field_type) { + $old_field_type = $combined_fields[$field] ?? null; + if ($old_field_type) { + if ($dim_depth >= 3) { + $combined_fields[$field] = UnionType::of(self::computeTypeSetOfMergedArrayShapeTypes( + $old_field_type->getTypeSet(), + $field_type->getTypeSet(), + $dim_depth - 1, + true + )); + } else { + $combined_fields[$field] = ArrayType::combineArrayTypesOverriding($field_type, $old_field_type, true); + } + } else { + $combined_fields[$field] = $field_type; + } + } + $result[] = ArrayShapeType::fromFieldTypes($combined_fields, false); + return $result; + } + + /** + * @param UnionType $assign_type - The type which is being added to + * @return UnionType - Usually the unmodified UnionType. Sometimes, the adjusted type, e.g. for string modification. + */ + public function typeCheckDimAssignment(UnionType $assign_type, Node $node): UnionType + { + static $int_or_string_type = null; + static $string_array_type = null; + static $simple_xml_element_type = null; + + if ($int_or_string_type === null) { + $int_or_string_type = UnionType::fromFullyQualifiedPHPDocString('int|string'); + $string_array_type = UnionType::fromFullyQualifiedPHPDocString('string[]'); + $simple_xml_element_type = + Type::fromNamespaceAndName('\\', 'SimpleXMLElement', false); + } + $dim_type = $this->dim_type; + $right_type = $this->right_type; + + // Sanity check: Don't add list to a property that isn't list + // unless it has 1 or more array types and all are list + $right_type = self::normalizeListTypesInDimAssignment($assign_type, $right_type); + + if ($assign_type->isEmpty() || ($assign_type->hasGenericArray() && !$assign_type->asExpandedTypes($this->code_base)->hasArrayAccess())) { + // For empty union types or 'array', expect the provided dimension to be able to cast to int|string + if ($dim_type && !$dim_type->isEmpty() && !$dim_type->canCastToUnionType($int_or_string_type)) { + $this->emitIssue( + Issue::TypeMismatchDimAssignment, + $node->lineno, + (string)$assign_type, + (string)$dim_type, + (string)$int_or_string_type + ); + } + return $right_type; + } + $assign_type_expanded = $assign_type->withStaticResolvedInContext($this->context)->asExpandedTypes($this->code_base); + //echo "$assign_type_expanded : " . json_encode($assign_type_expanded->hasArrayLike()) . "\n"; + + // TODO: Better heuristic to deal with false positives on ArrayAccess subclasses + if ($assign_type_expanded->hasArrayAccess() && !$assign_type_expanded->hasGenericArray()) { + return UnionType::empty(); + } + + if (!$assign_type_expanded->hasArrayLike()) { + if ($assign_type->hasNonNullStringType()) { + // Are we assigning to a variable/property of type 'string' (with no ArrayAccess or array types)? + if (\is_null($dim_type)) { + $this->emitIssue( + Issue::TypeMismatchDimEmpty, + $node->lineno ?? 0, + (string)$assign_type, + 'int' + ); + } elseif (!$dim_type->isEmpty() && !$dim_type->hasNonNullIntType()) { + $this->emitIssue( + Issue::TypeMismatchDimAssignment, + $node->lineno, + (string)$assign_type, + (string)$dim_type, + 'int' + ); + } else { + if ($right_type->canCastToUnionType($string_array_type)) { + // e.g. $a = 'aaa'; $a[0] = 'x'; + // (Currently special casing this, not handling deeper dimensions) + return StringType::instance(false)->asPHPDocUnionType(); + } + } + } elseif (!$assign_type->hasTypeMatchingCallback(static function (Type $type) use ($simple_xml_element_type): bool { + return !$type->isNullableLabeled() && ($type instanceof MixedType || $type === $simple_xml_element_type); + })) { + // Imitate the check in UnionTypeVisitor, don't warn for mixed (but warn for `?mixed`), etc. + $this->emitIssue( + Issue::TypeArraySuspicious, + $node->lineno, + ASTReverter::toShortString($node), + (string)$assign_type + ); + } + } + return $right_type; + } + + private static function normalizeListTypesInDimAssignment(UnionType $assign_type, UnionType $right_type): UnionType + { + // Offsets of $can_cast: + // 0. lazily computed: True if list types should be kept as-is. + // 1. lazily computed: Should this cast from a regular array to an associative array? + $can_cast = []; + /** + * @param list $type_set + * @return list with top level list converted to non-empty-array. May contain duplicates. + */ + $map_type_set = static function (array $type_set) use ($assign_type, &$can_cast): array { + foreach ($type_set as $i => $type) { + if ($type instanceof ListType) { + $result = ($can_cast[0] = ($can_cast[0] ?? $assign_type->hasTypeMatchingCallback(static function (Type $other_type): bool { + if (!$other_type instanceof ArrayType) { + return false; + } + if ($other_type instanceof ListType) { + return true; + } + // @phan-suppress-next-line PhanAccessMethodInternal + if ($other_type instanceof ArrayShapeType && $other_type->canCastToList()) { + return true; + } + return false; + }))); + if ($result) { + continue; + } + $type_set[$i] = NonEmptyGenericArrayType::fromElementType($type->genericArrayElementType(), $type->isNullable(), $type->getKeyType()); + } elseif ($type instanceof GenericArrayType) { + $result = ($can_cast[1] = ($can_cast[1] ?? $assign_type->hasTypeMatchingCallback(static function (Type $other_type): bool { + if (!$other_type instanceof ArrayType) { + return false; + } + if ($other_type instanceof AssociativeArrayType) { + return true; + } + // @phan-suppress-next-line PhanAccessMethodInternal + if ($other_type instanceof ArrayShapeType && $other_type->canCastToList()) { + return true; + } + return false; + }))); + if (!$result) { + continue; + } + $type_set[$i] = NonEmptyAssociativeArrayType::fromElementType($type->genericArrayElementType(), $type->isNullable(), $type->getKeyType()); + } + } + return $type_set; + }; + $new_type_set = $map_type_set($right_type->getTypeSet()); + $new_real_type_set = $map_type_set($right_type->getRealTypeSet()); + if (\count($can_cast) === 0) { + return $right_type; + } + return UnionType::of($new_type_set, $new_real_type_set); + // echo "Converting $right_type to $assign_type: $result\n"; + } + + /** + * @param Node $node + * A node to analyze as the target of an assignment of type AST_REF (found only in foreach) + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + */ + public function visitRef(Node $node): Context + { + // Note: AST_REF is only ever generated in AST_FOREACH, so this should be fine. + $var = $node->children['var']; + if ($var instanceof Node) { + return $this->__invoke($var); + } + $this->emitIssue( + Issue::Unanalyzable, + $node->lineno + ); + return $this->context; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/BinaryOperatorFlagVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/BinaryOperatorFlagVisitor.php new file mode 100644 index 000000000..af3904c7a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/BinaryOperatorFlagVisitor.php @@ -0,0 +1,1067 @@ +kind=ast\AST_BINARY_OP). + * The visit* method invoked is based on Node->flags. + * + * This returns the union type of the binary operator. + * It emits issues as a side effect, usually based on $should_catch_issue_exception + * + * TODO: Improve analysis of bitwise operations, warn if non-int is provided and consistently return int if it's guaranteed + */ +final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation +{ + + /** + * @var CodeBase The code base within which we're operating + */ + private $code_base; + + /** + * @var Context The context in which we are determining the union type of the result of a binary operator + */ + private $context; + + /** + * @var bool should we catch issue exceptions while analyzing and proceed with the best guess at the resulting union type? + * If false, exceptions will be propagated to the caller. + */ + private $should_catch_issue_exception; + + /** + * Create a new BinaryOperatorFlagVisitor + */ + public function __construct( + CodeBase $code_base, + Context $context, + bool $should_catch_issue_exception = false + ) { + $this->code_base = $code_base; + $this->context = $context; + $this->should_catch_issue_exception = $should_catch_issue_exception; + } + + /** + * @param Node $node + * A node to visit + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function __invoke(Node $node) + { + return $this->{Element::VISIT_BINARY_LOOKUP_TABLE[$node->flags] ?? 'handleMissing'}($node); + } + + /** + * @throws AssertionError + * @suppress PhanUnreferencedPrivateMethod this is referenced by __invoke + */ + private function handleMissing(Node $node): void + { + throw new AssertionError("All flags must match. Found kind=" . Debug::nodeName($node) . ', flags=' . Element::flagDescription($node) . ' raw flags=' . $node->flags . ' at ' . $this->context->withLineNumberStart((int)$node->lineno)); + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visit(Node $node): UnionType + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['left'], + $this->should_catch_issue_exception + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['right'], + $this->should_catch_issue_exception + ); + static $int_or_float = null; + + if ($left->isExclusivelyArray() + || $right->isExclusivelyArray() + ) { + return UnionType::empty(); + } elseif ($left->hasType(FloatType::instance(false)) + || $right->hasType(FloatType::instance(false)) + ) { + if ($left->hasTypeMatchingCallback( + static function (Type $type): bool { + return !($type instanceof FloatType); + } + ) && $right->hasTypeMatchingCallback( + static function (Type $type): bool { + return !($type instanceof FloatType); + } + ) + ) { + return $int_or_float ?? ($int_or_float = UnionType::fromFullyQualifiedPHPDocString('int|float')); + } + + return FloatType::instance(false)->asPHPDocUnionType(); + } elseif ($left->hasNonNullIntType() + && $right->hasNonNullIntType() + ) { + return IntType::instance(false)->asPHPDocUnionType(); + } + + return $int_or_float ?? ($int_or_float = UnionType::fromFullyQualifiedPHPDocString('int|float')); + } + + /** + * Analyzes the `<=>` operator. + * + * @param Node $node @phan-unused-param + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinarySpaceship(Node $node): UnionType + { + // TODO: Any sanity checks should go here. + + // <=> returns -1, 0, or 1 + return UnionType::fromFullyQualifiedRealString('-1|0|1'); + } + + /** + * Analyzes the `<<` operator. + * + * @param Node $node @phan-unused-param + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryShiftLeft(Node $node): UnionType + { + // TODO: Any sanity checks should go here. + return IntType::instance(false)->asRealUnionType(); + } + + /** + * Analyzes the `>>` operator. + * + * @param Node $node @phan-unused-param + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryShiftRight(Node $node): UnionType + { + // TODO: Any sanity checks should go here. + return IntType::instance(false)->asRealUnionType(); + } + + /** + * Code can bitwise xor strings byte by byte (or integers by value) in PHP + * @override + */ + public function visitBinaryBitwiseXor(Node $node): UnionType + { + return $this->analyzeBinaryBitwiseCommon($node); + } + + /** + * @override + */ + public function visitBinaryBitwiseOr(Node $node): UnionType + { + return $this->analyzeBinaryBitwiseCommon($node); + } + + /** + * @override + */ + public function visitBinaryBitwiseAnd(Node $node): UnionType + { + return $this->analyzeBinaryBitwiseCommon($node); + } + + private function analyzeBinaryBitwiseCommon(Node $node): UnionType + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['left'], + $this->should_catch_issue_exception + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['right'], + $this->should_catch_issue_exception + ); + + if ($left->hasNonNullIntType()) { + if ($right->hasNonNullIntType()) { + return self::computeIntOrFloatOperationResult($node, $left, $right); + } + if ($right->hasNonNullStringType()) { + $this->emitIssue( + Issue::TypeMismatchBitwiseBinaryOperands, + $node->lineno ?? 0, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + $left, + $right + ); + } + } elseif ($left->hasNonNullStringType()) { + if ($right->hasNonNullStringType()) { + return UnionType::fromFullyQualifiedPHPDocAndRealString('string', 'int|string'); + } + if ($right->hasNonNullIntType()) { + $this->emitIssue( + Issue::TypeMismatchBitwiseBinaryOperands, + $node->lineno ?? 0, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + $left, + $right + ); + } + } + if (!$left->hasValidBitwiseOperand() || !$right->hasValidBitwiseOperand()) { + $this->emitIssue( + Issue::TypeInvalidBitwiseBinaryOperator, + $node->lineno ?? 0, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + $left, + $right + ); + } + + return UnionType::fromFullyQualifiedPHPDocAndRealString('int', 'int|string'); + } + + /** + * TODO: Switch to asRealUnionType when both operands are real + * @internal + */ + public static function computeIntOrFloatOperationResult( + Node $node, + UnionType $left, + UnionType $right + ): UnionType { + static $real_int_or_string; + static $real_int; + static $real_float; + static $real_int_or_float; + if ($real_int_or_string === null) { + $real_int_or_string = [IntType::instance(false), StringType::instance(false)]; + $real_int = [IntType::instance(false)]; + $real_float = [FloatType::instance(false)]; + $real_int_or_float = [IntType::instance(false), FloatType::instance(false)]; + } + $left_value = $left->asSingleScalarValueOrNull(); + if ($left_value !== null) { + $right_value = $right->asSingleScalarValueOrNull(); + if ($right_value !== null) { + /** + * This will aggressively infer the real type for expressions where both values have known real literal types (e.g. 2+2*3), + * but fall back if the real type was less specific. + * + * @param list $default_types + */ + $make_literal_union_type = static function (Type $result, array $default_types) use ($left, $right): UnionType { + if ($left->isExclusivelyRealTypes() && $right->isExclusivelyRealTypes()) { + return $result->asRealUnionType(); + } + return UnionType::of([$result], $default_types); + }; + switch ($node->flags) { + case ast\flags\BINARY_BITWISE_OR: + return $make_literal_union_type( + LiteralIntType::instanceForValue($left_value | $right_value, false), + $real_int + ); + case ast\flags\BINARY_BITWISE_AND: + return $make_literal_union_type( + LiteralIntType::instanceForValue($left_value & $right_value, false), + $real_int + ); + case ast\flags\BINARY_BITWISE_XOR: + return $make_literal_union_type( + LiteralIntType::instanceForValue($left_value ^ $right_value, false), + $real_int + ); + case ast\flags\BINARY_MUL: + $value = $left_value * $right_value; + return $make_literal_union_type( + is_int($value) ? LiteralIntType::instanceForValue($value, false) + : LiteralFloatType::instanceForValue($value, false), + $real_int_or_float + ); + case ast\flags\BINARY_DIV: + // @phan-suppress-next-line PhanSuspiciousTruthyString deliberate check - this possible string is implicitly cast to a number. + if (!$right_value) { + // TODO: Emit warning about division by zero. + return FloatType::instance(false)->asRealUnionType(); + } + $value = $left_value / $right_value; + return $make_literal_union_type( + is_int($value) ? LiteralIntType::instanceForValue($value, false) + : LiteralFloatType::instanceForValue($value, false), + $real_int_or_float + ); + case ast\flags\BINARY_MOD: + // @phan-suppress-next-line PhanSuspiciousTruthyString deliberate check - this possible string is implicitly cast to a number. + if (!$right_value) { + // TODO: Emit warning about division by zero. + return IntType::instance(false)->asRealUnionType(); + } + $value = $left_value % $right_value; + return $make_literal_union_type( + LiteralIntType::instanceForValue($value, false), + $real_int + ); + case ast\flags\BINARY_SUB: + $value = $left_value - $right_value; + return $make_literal_union_type( + is_int($value) ? LiteralIntType::instanceForValue($value, false) + : LiteralFloatType::instanceForValue($value, false), + $real_int_or_float + ); + case ast\flags\BINARY_ADD: + $value = $left_value + $right_value; + return $make_literal_union_type( + is_int($value) ? LiteralIntType::instanceForValue($value, false) + : LiteralFloatType::instanceForValue($value, false), + $real_int_or_float + ); + case ast\flags\BINARY_POW: + $value = $left_value ** $right_value; + return $make_literal_union_type( + is_int($value) ? LiteralIntType::instanceForValue($value, false) + : LiteralFloatType::instanceForValue($value, false), + $real_int_or_float + ); + } + } + } + + $is_binary_op = \in_array($node->flags, [ast\flags\BINARY_BITWISE_XOR, ast\flags\BINARY_BITWISE_AND, ast\flags\BINARY_BITWISE_OR], true); + + if ($is_binary_op) { + return UnionType::fromFullyQualifiedPHPDocAndRealString('int', 'int|string'); + } + if ($left->isExclusivelyRealFloatTypes() || $right->isExclusivelyRealFloatTypes()) { + return FloatType::instance(false)->asRealUnionType(); + } + if ($node->flags === ast\flags\BINARY_DIV) { + return UnionType::fromFullyQualifiedRealString('int|float'); + } + // A heuristic to reduce false positives. + // e.g. an operation on float and float returns float. + // e.g. an operation on int|float and int|float returns int|float. + // e.g. an operation on int and int returns int. + if ($left->hasTypesCoercingToNonInt() || $right->hasTypesCoercingToNonInt()) { + $main_type = ($left->hasIntType() && $right->hasIntType()) ? 'int|float' : 'float'; + } else { + $main_type = 'int'; + } + return UnionType::fromFullyQualifiedPHPDocAndRealString( + $main_type, + 'int|float' + ); + } + + /** + * @param string $issue_type + * The type of issue to emit such as Issue::ParentlessClass + * + * @param int $lineno + * The line number where the issue was found + * + * @param int|string|FQSEN|UnionType|Type ...$parameters + * Template parameters for the issue's error message + */ + protected function emitIssue( + string $issue_type, + int $lineno, + ...$parameters + ): void { + Issue::maybeEmitWithParameters( + $this->code_base, + $this->context, + $issue_type, + $lineno, + $parameters + ); + } + + /** + * @param Node $node @unused-param + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryBoolAnd(Node $node): UnionType + { + return BoolType::instance(false)->asRealUnionType(); + } + + /** + * @param Node $node @unused-param + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryBoolXor(Node $node): UnionType + { + return BoolType::instance(false)->asRealUnionType(); + } + + /** + * @param Node $node @unused-param + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryBoolOr(Node $node): UnionType + { + return BoolType::instance(false)->asRealUnionType(); + } + + /** + * @param Node $node A node to check types on (@phan-unused-param) + * + * TODO: Check that both types can cast to string or scalars? + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryConcat(Node $node): UnionType + { + $left_node = $node->children['left']; + $left_value = $left_node instanceof Node ? UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $left_node, + $this->should_catch_issue_exception + )->asSingleScalarValueOrNullOrSelf() : $left_node; + if (\is_object($left_value)) { + return StringType::instance(false)->asRealUnionType(); + } + $right_node = $node->children['right']; + $right_value = $right_node instanceof Node ? UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $right_node, + $this->should_catch_issue_exception + )->asSingleScalarValueOrNullOrSelf() : $right_node; + if (\is_object($right_value)) { + return StringType::instance(false)->asRealUnionType(); + } + return LiteralStringType::instanceForValue($left_value . $right_value, false)->asRealUnionType(); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + private function visitBinaryOpCommon(Node $node): UnionType + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['left'], + $this->should_catch_issue_exception + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['right'], + $this->should_catch_issue_exception + ); + + $left_is_array_like = $left->isExclusivelyArrayLike(); + $right_is_array_like = $right->isExclusivelyArrayLike(); + + $left_can_cast_to_array = $left->canCastToUnionType( + ArrayType::instance(false)->asPHPDocUnionType() + ); + + $right_can_cast_to_array = $right->canCastToUnionType( + ArrayType::instance(false)->asPHPDocUnionType() + ); + + if ($left_is_array_like + && !$right->hasArrayLike() + && !$right_can_cast_to_array + && !$right->isEmpty() + && !$right->containsNullable() + && !$left->hasAnyType($right->getTypeSet()) // TODO: Strict canCastToUnionType() variant? + ) { + $this->emitIssue( + Issue::TypeComparisonFromArray, + $node->lineno ?? 0, + (string)$right->asNonLiteralType() + ); + } elseif ($right_is_array_like + && !$left->hasArrayLike() + && !$left_can_cast_to_array + && !$left->isEmpty() + && !$left->containsNullable() + && !$right->hasAnyType($left->getTypeSet()) // TODO: Strict canCastToUnionType() variant? + ) { + $this->emitIssue( + Issue::TypeComparisonToArray, + $node->lineno ?? 0, + (string)$left->asNonLiteralType() + ); + } + + return BoolType::instance(false)->asRealUnionType(); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryIsIdentical(Node $node): UnionType + { + return $this->visitBinaryOpCommon($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryIsNotIdentical(Node $node): UnionType + { + return $this->visitBinaryOpCommon($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryIsEqual(Node $node): UnionType + { + return $this->visitBinaryOpCommon($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryIsNotEqual(Node $node): UnionType + { + return $this->visitBinaryOpCommon($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryIsSmaller(Node $node): UnionType + { + return $this->visitBinaryOpCommon($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryIsSmallerOrEqual(Node $node): UnionType + { + return $this->visitBinaryOpCommon($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryIsGreater(Node $node): UnionType + { + return $this->visitBinaryOpCommon($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryIsGreaterOrEqual(Node $node): UnionType + { + return $this->visitBinaryOpCommon($node); + } + + /** + * @param Node $node with type AST_BINARY_OP + * @param Closure(Type):bool $is_valid_type + */ + private function warnAboutInvalidUnionType( + Node $node, + Closure $is_valid_type, + UnionType $left, + UnionType $right, + string $left_issue_type, + string $right_issue_type + ): void { + if (!$left->isEmpty()) { + if (!$left->hasTypeMatchingCallback($is_valid_type)) { + $this->emitIssue( + $left_issue_type, + $node->children['left']->lineno ?? $node->lineno, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + $left + ); + } + } + if (!$right->isEmpty()) { + if (!$right->hasTypeMatchingCallback($is_valid_type)) { + $this->emitIssue( + $right_issue_type, + $node->children['right']->lineno ?? $node->lineno, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + $right + ); + } + } + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryAdd(Node $node): UnionType + { + $code_base = $this->code_base; + $context = $this->context; + $left = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['left'], + $this->should_catch_issue_exception + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['right'], + $this->should_catch_issue_exception + ); + + static $probably_float_type = null; + static $probably_int_or_float_type = null; + static $probably_array_type = null; + static $probably_unknown_type = null; + static $array_type = null; + if ($probably_float_type === null) { + $probably_float_type = UnionType::fromFullyQualifiedPHPDocAndRealString('float', 'int|float|array'); + $probably_int_or_float_type = UnionType::fromFullyQualifiedPHPDocAndRealString('int|float', 'int|float|array'); + $probably_array_type = UnionType::fromFullyQualifiedPHPDocAndRealString('array', 'int|float|array'); + $probably_unknown_type = UnionType::fromFullyQualifiedPHPDocAndRealString('', 'int|float|array'); + // TODO: More precise check for array + $array_type = ArrayType::instance(false); + } + + + // fast-track common cases + if ($left->isNonNullIntOrFloatType() && $right->isNonNullIntOrFloatType()) { + return self::computeIntOrFloatOperationResult($node, $left, $right); + } + + // If both left and right union types are arrays, then this is array + // concatenation. (`$left + $right`) + if ($left->isGenericArray() && $right->isGenericArray()) { + self::checkInvalidArrayShapeCombination($this->code_base, $this->context, $node, $left, $right); + if ($left->isEqualTo($right)) { + return $left; + } + return ArrayType::combineArrayTypesOverriding($left, $right, false); + } + + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + return $type->isValidNumericOperand() || $type instanceof ArrayType; + }, + $left, + $right, + Issue::TypeInvalidLeftOperandOfAdd, + Issue::TypeInvalidRightOperandOfAdd + ); + + if ($left->isNonNullNumberType() && $right->isNonNullNumberType()) { + if (!$left->hasNonNullIntType() || !$right->hasNonNullIntType()) { + // Heuristic: If one or more of the sides is a float, the result is always a float. + return $probably_float_type; + } + return $probably_int_or_float_type; + } + + $left_is_array = ( + !$left->genericArrayElementTypes()->isEmpty() + && $left->nonArrayTypes()->isEmpty() + ) || $left->isType($array_type); + + $right_is_array = ( + !$right->genericArrayElementTypes()->isEmpty() + && $right->nonArrayTypes()->isEmpty() + ) || $right->isType($array_type); + + if ($left_is_array || $right_is_array) { + if ($left_is_array && $right_is_array) { + return ArrayType::combineArrayTypesOverriding($left, $right, false); + } + + if ($left_is_array + && !$right->canCastToUnionType( + ArrayType::instance(false)->asPHPDocUnionType() + ) + ) { + $this->emitIssue( + Issue::TypeInvalidRightOperand, + $node->lineno ?? 0 + ); + return $probably_unknown_type; + } elseif ($right_is_array && !$left->canCastToUnionType($array_type->asPHPDocUnionType())) { + $this->emitIssue( + Issue::TypeInvalidLeftOperand, + $node->lineno ?? 0 + ); + return $probably_unknown_type; + } + // If it is a '+' and we know one side is an array + // and the other is unknown, assume array + return $probably_array_type; + } + + return $probably_int_or_float_type; + } + + /** + * Check for suspicious combination of two arrays with + * `+` or `+=` operators. + */ + public static function checkInvalidArrayShapeCombination( + CodeBase $code_base, + Context $context, + Node $node, + UnionType $left, + UnionType $right + ): void { + if (!$left->hasRealTypeSet() || !$right->hasRealTypeSet()) { + return; + } + $possible_right_fields = []; + foreach ($right->getRealTypeSet() as $type) { + if (!$type instanceof ArrayShapeType) { + if ($type instanceof ListType) { + continue; + } + return; + } + $possible_right_fields += $type->getFieldTypes(); + } + $common_left_fields = null; + foreach ($left->getRealTypeSet() as $type) { + if (!$type instanceof ArrayShapeType) { + if ($type instanceof ListType) { + continue; + } + return; + } + $left_fields = []; + foreach ($type->getFieldTypes() as $key => $inner_type) { + if (!$inner_type->isPossiblyUndefined()) { + $left_fields[$key] = true; + } + } + if (\is_array($common_left_fields)) { + $common_left_fields = \array_intersect($common_left_fields, $left_fields); + } else { + $common_left_fields = $left_fields; + } + } + if ($common_left_fields && !\array_diff_key($possible_right_fields, $common_left_fields)) { + Issue::maybeEmit( + $code_base, + $context, + Issue::UselessBinaryAddRight, + $node->lineno, + $left, + $right, + ASTReverter::toShortString($node) + ); + return; + } + $common_left_fields = $common_left_fields ?? []; + if ($common_left_fields === \array_values($common_left_fields) && $possible_right_fields === \array_values($possible_right_fields)) { + foreach (\array_merge($left->getRealTypeSet(), $right->getRealTypeSet()) as $type) { + if ($type instanceof ArrayShapeType) { + // @phan-suppress-next-line PhanAccessMethodInternal + if (!$type->canCastToList()) { + return; + } + } + } + Issue::maybeEmit( + $code_base, + $context, + Issue::SuspiciousBinaryAddLists, + $node->lineno, + $left, + $right, + ASTReverter::toShortString($node) + ); + } + } + + + /** + * Analyzes the result of a floating-point or integer arithmetic operation. + * The result will be a combination of 'int' or 'float' + * + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + private function getTypeOfNumericArithmeticOp(Node $node): UnionType + { + $code_base = $this->code_base; + $context = $this->context; + $left = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['left'], + $this->should_catch_issue_exception + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['right'], + $this->should_catch_issue_exception + ); + + // fast-track common cases + if ($left->isNonNullIntOrFloatType() && $right->isNonNullIntOrFloatType()) { + return self::computeIntOrFloatOperationResult($node, $left, $right); + } + + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + // TODO: Stricten this to warn about strings based on user config. + return $type->isValidNumericOperand(); + }, + $left, + $right, + Issue::TypeInvalidLeftOperandOfNumericOp, + Issue::TypeInvalidRightOperandOfNumericOp + ); + + static $float_type = null; + static $int_or_float_union_type = null; + if ($int_or_float_union_type === null) { + $float_type = FloatType::instance(false)->asRealUnionType(); + $int_or_float_union_type = UnionType::fromFullyQualifiedRealString('int|float'); + } + if ($left->isExclusivelyRealFloatTypes() || $right->isExclusivelyRealFloatTypes()) { + return $float_type; + } + return $int_or_float_union_type; + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinarySub(Node $node): UnionType + { + return $this->getTypeOfNumericArithmeticOp($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryMul(Node $node): UnionType + { + return $this->getTypeOfNumericArithmeticOp($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryDiv(Node $node): UnionType + { + return $this->getTypeOfNumericArithmeticOp($node); + } + + /** + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryPow(Node $node): UnionType + { + return $this->getTypeOfNumericArithmeticOp($node); + } + + /** + * @unused-param $node + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryMod(Node $node): UnionType + { + // TODO: Warn about invalid left or right side + return IntType::instance(false)->asRealUnionType(); + } + + /** + * Common visitor for binary boolean operations + * + * @param Node $node + * A node to check types on + * + * @return UnionType + * The resulting type(s) of the binary operation + */ + public function visitBinaryCoalesce(Node $node): UnionType + { + $left_node = $node->children['left']; + $left_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $left_node, + $this->should_catch_issue_exception + ); + if (!($left_node instanceof Node)) { + // TODO: Be more aggressive for constants, etc, when we are very sure the type is accurate. + return $left_type; + } + + $right_node = $node->children['right']; + if ($right_node instanceof Node && $right_node->kind === ast\AST_THROW) { + return $left_type->nonNullableClone(); + } + $right_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $right_node, + $this->should_catch_issue_exception + ); + if ($left_type->isEmpty()) { + if ($right_type->isEmpty()) { + return MixedType::instance(false)->asPHPDocUnionType(); + } elseif ($right_type->isNull()) { + // When the right type is null, and the left type is unknown, + // infer nullable mixed. + // + // To infer something useful when strict type checking is disabled, + // don't add mixed when the right type is something other than null. + return MixedType::instance(true)->asPHPDocUnionType(); + } + } + + // On the left side, remove null and replace '?T' with 'T' + // Don't bother if the right side contains null. + if (!$right_type->isEmpty() && $left_type->containsNullable() && !$right_type->containsNullable()) { + if ($left_type->getRealUnionType()->isRealTypeNullOrUndefined()) { + return $right_type; + } + $left_type = $left_type->nonNullableClone(); + } + + return $left_type->withUnionType($right_type)->asNormalizedTypes(); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/BlockExitStatusChecker.php b/bundled-libs/phan/phan/src/Phan/Analysis/BlockExitStatusChecker.php new file mode 100644 index 000000000..1070a21e5 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/BlockExitStatusChecker.php @@ -0,0 +1,772 @@ +flags whenever possible in order to avoid keeping around \ast\Node + * instances for longer than those would be used. + * This assumes that Nodes aren't manipulated, or manipulations to Nodes will preserve the semantics (including computed exit status) or clear $node->flags. + * + * - Creating an additional object property would increase overall memory usage, which is why properties are used. + * - AST_IF, AST_IF_ELEM, AST_DO_WHILE, AST_FOR, AST_WHILE, AST_STMT_LIST, + * etc (e.g. switch and switch case, try/finally). + * are node types which are known to not have flags in AST version 40. + * - In the future, add a new property such as $node->children['__exitStatus'] if used for a node type with flags, or use the higher bits. + * + * TODO: Change to AnalysisVisitor if this ever emits issues. + * TODO: Analyze switch (if there is a default) in another PR (And handle fallthrough) + * + * @phan-file-suppress PhanUnusedPublicFinalMethodParameter + * @phan-file-suppress PhanPartialTypeMismatchArgument + */ +final class BlockExitStatusChecker extends KindVisitorImplementation +{ + // These should be at most 1 << 31, in order to work in 32-bit php. + // NOTE: Any exit status must be a combination of at least one of these bits + // E.g. if STATUS_PROCEED is mixed with STATUS_RETURN, it would mean it is possible both to go to completion or return. + public const STATUS_PROCEED = (1 << 20); // At least one branch continues to completion. + public const STATUS_GOTO = (1 << 21); // At least one branch leads to a goto statement + public const STATUS_CONTINUE = (1 << 22); // At least one branch leads to a continue statement + public const STATUS_BREAK = (1 << 23); // At least one branch leads to a break statement + public const STATUS_THROW = (1 << 24); // At least one branch leads to a throw statement + public const STATUS_RETURN = (1 << 25); // At least one branch leads to a return/exit() statement (or an infinite loop) + + public const STATUS_THROW_OR_RETURN_BITMASK = + self::STATUS_THROW | + self::STATUS_RETURN; + + // Any status which doesn't lead to proceeding. + public const STATUS_NOT_PROCEED_BITMASK = + self::STATUS_GOTO | + self::STATUS_CONTINUE | + self::STATUS_BREAK | + self::STATUS_THROW | + self::STATUS_RETURN; + + public const STATUS_BITMASK = + self::STATUS_PROCEED | + self::STATUS_NOT_PROCEED_BITMASK; + + public const STATUS_MAYBE_PROCEED = + self::STATUS_PROCEED | + self::STATUS_GOTO; + + public function __construct() + { + } + + /** + * Computes the bitmask representing the possible ways this block of code might exit. + * + * This currently does not handle goto or `break N` comprehensively. + */ + public function check(Node $node = null): int + { + if (!$node) { + return self::STATUS_PROCEED; + } + $result = $this->__invoke($node); + if (!\is_int($result) || $result <= 0) { + throw new AssertionError('Expected positive int'); + } + return $result; + } + + /** + * If we don't know how to analyze a node type (or left it out), assume it always proceeds + * @return int - The status bitmask corresponding to always proceeding + */ + public function visit(Node $node): int + { + return self::STATUS_PROCEED; + } + + /** + * @param Node|string|int|float $cond + */ + private static function isTruthyLiteral($cond): bool + { + if ($cond instanceof Node) { + // TODO: Could look up values for remaining constants and inline expressions, but doing that has low value. + if ($cond->kind === \ast\AST_CONST) { + $cond_name_string = $cond->children['name']->children['name'] ?? null; + return \is_string($cond_name_string) && \strcasecmp($cond_name_string, 'true') === 0; + } + return false; + } + // Cast string, int, etc. literal to a bool + return (bool)$cond; + } + + /** + * A break statement unconditionally breaks out of a loop/switch + * @return int the corresponding status code + */ + public function visitBreak(Node $node): int + { + return self::STATUS_BREAK; + } + + /** + * A continue statement unconditionally continues out of a loop/switch. + * TODO: Make this account for levels + * @return int the corresponding status code + */ + public function visitContinue(Node $node): int + { + return self::STATUS_CONTINUE; + } + + /** + * A throw statement unconditionally throws + * @return int the corresponding status code + */ + public function visitThrow(Node $node): int + { + return self::STATUS_THROW; + } + + /** + * @return int the corresponding status code for the try/catch/finally block + */ + public function visitTry(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = $this->computeStatusOfTry($node); + $node->flags = $status; + return $status; + } + + private function computeStatusOfTry(Node $node): int + { + $main_status = $this->check($node->children['try']); + // Finding good heuristics is difficult. + // e.g. "return someFunctionThatMayThrow()" in try{} block would be inferred as STATUS_RETURN, but may actually be STATUS_THROW + + $finally_node = $node->children['finally']; + if ($finally_node) { + $finally_status = $this->check($finally_node); + // TODO: Could emit an issue as a side effect + // Having any sort of status in a finally statement is + // likely to have unintuitive behavior. + if (($finally_status & (~self::STATUS_THROW_OR_RETURN_BITMASK)) === 0) { + return $finally_status; + } + } else { + $finally_status = self::STATUS_PROCEED; + } + $catches_node = $node->children['catches']; + if (\count($catches_node->children) === 0) { + return self::mergeFinallyStatus($main_status, $finally_status); + } + // TODO: Could enhance slightly by checking for catch nodes with the exact same types (or subclasses) as names of exception thrown. + $combined_status = self::mergeFinallyStatus($main_status, $finally_status) | $this->visitCatchList($catches_node); + if (($finally_status & self::STATUS_PROCEED) === 0) { + $combined_status &= ~self::STATUS_PROCEED; + } + // No idea. + return $combined_status; + } + + /** + * @return int the corresponding status code + */ + public function visitCatchList(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = $this->computeStatusOfCatchList($node); + $node->flags = $status; + return $status; + } + + private function computeStatusOfCatchList(Node $node): int + { + $catch_list = $node->children; + if (count($catch_list) === 0) { + return self::STATUS_PROCEED; // status probably won't matter + } + // TODO: Could enhance slightly by checking for catch nodes with the exact same types (or subclasses) as names of exception thrown. + $combined_status = 0; + // Try to cover all possible cases, such as try { return throwsException(); } catch(Exception $e) { break; } + foreach ($node->children as $catch_node) { + if (!$catch_node instanceof Node) { + throw new AssertionError('Expected catch statement to be a Node'); + } + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null for catch nodes + $catch_node_status = $this->visitStmtList($catch_node->children['stmts']); + $combined_status |= $catch_node_status; + } + // No idea. + return $combined_status; + } + + private static function mergeFinallyStatus(int $try_status, int $finally_status): int + { + // If at least one of try or finally are guaranteed not to proceed to completion, + // then combine those possibilities. + if (($try_status & $finally_status & self::STATUS_PROCEED) === 0) { + return ($try_status | $finally_status) & ~self::STATUS_PROCEED; + } + return $try_status | $finally_status; + } + + /** + * @return int the corresponding status code + * @suppress PhanTypeMismatchArgumentNullable + */ + public function visitSwitch(Node $node): int + { + $cond = $node->children['cond']; + if ($cond instanceof Node) { + $cond_status = $this->check($cond); + if (($cond_status & self::STATUS_PROCEED) === 0) { + // handle throw expressions, switch(exit()), etc. + return $cond_status; + } + } else { + $cond_status = self::STATUS_PROCEED; + } + return $this->visitSwitchList($node->children['stmts']) | ($cond_status & ~self::STATUS_PROCEED); + } + + /** + * @return int the corresponding status code + */ + public function visitSwitchList(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = $this->computeStatusOfSwitchList($node); + $node->flags = $status; + return $status; + } + + private function computeStatusOfSwitchList(Node $node): int + { + $switch_stmt_case_nodes = $node->children ?? []; + if (\count($switch_stmt_case_nodes) === 0) { + return self::STATUS_PROCEED; + } + $has_default = false; + $combined_statuses = 0; + foreach ($switch_stmt_case_nodes as $index => $case_node) { + if (!$case_node instanceof Node) { + throw new AssertionError('Expected switch case to be a Node'); + } + if ($case_node->children['cond'] === null) { + $has_default = true; + } + $case_status = self::getStatusOfSwitchCase($case_node, $index, $switch_stmt_case_nodes); + if (($case_status & self::STATUS_CONTINUE_OR_BREAK) !== 0) { + // Ignore statuses such as break/continue. They take effect inside, but are a proceed status outside + $case_status = ($case_status & ~self::STATUS_CONTINUE_OR_BREAK) | self::STATUS_PROCEED; + } + $combined_statuses |= $case_status; + } + if (!$has_default) { + $combined_statuses |= self::STATUS_PROCEED; + } + return $combined_statuses; + } + + /** + * @param list $siblings + */ + private function getStatusOfSwitchCase(Node $case_node, int $index, array $siblings): int + { + $status = $case_node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = $this->computeStatusOfSwitchCase($case_node, $index, $siblings); + $case_node->flags = $status; + return $status; + } + + /** + * @param array $siblings + */ + private function computeStatusOfSwitchCase(Node $case_node, int $index, array $siblings): int + { + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null + $status = $this->visitStmtList($case_node->children['stmts']); + if (($status & self::STATUS_PROCEED) === 0) { + // Check if the current switch case will not fall through. + return $status; + } + $next_sibling = $siblings[$index + 1] ?? null; + if (!$next_sibling instanceof Node) { + return $status; + } + $next_status = self::getStatusOfSwitchCase($next_sibling, $index + 1, $siblings); + // Combine the possibilities. + // e.g. `case 1: if (cond()) { return; } case 2: throw;`, case 1 will either break or throw, + // but won't proceed normally to the outside of the switch statement. + return ($status & ~self::STATUS_PROCEED) | $next_status; + } + + /** + * @return int the corresponding status code + * @suppress PhanTypeMismatchArgumentNullable + */ + public function visitMatch(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $cond_status = $this->check($node->children['cond']); + if (($cond_status & self::STATUS_PROCEED) === 0) { + return $cond_status; + } + return $this->visitMatchArmList($node->children['stmts']) | ($cond_status & ~self::STATUS_PROCEED); + } + + /** + * @return int the corresponding status code for the match arm list + */ + public function visitMatchArmList(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = $this->computeStatusOfMatchArmList($node); + $node->flags = $status; + return $status; + } + + /** + * @return int the corresponding status code + * @suppress PhanTypeMismatchArgumentNullable + */ + public function visitMatchArm(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = $this->computeMatchArmStatus($node); + $node->flags |= $status; + return $status; + } + + private function computeMatchArmStatus(Node $node): int + { + ['cond' => $cond, 'expr' => $expr] = $node->children; + $cond_status = 0; + foreach ($cond->children ?? [] as $cond_expr) { + if (!$cond_expr instanceof Node) { + $cond_status |= self::STATUS_PROCEED; + continue; + } + $cond_status |= $this->check($cond_expr); + if (($cond_status & self::STATUS_PROCEED) === 0) { + return $cond_status; + } + } + return ($cond_status & ~self::STATUS_PROCEED) | ($expr instanceof Node ? $this->check($expr) : self::STATUS_PROCEED); + } + + private function computeStatusOfMatchArmList(Node $node): int + { + $default_status = self::STATUS_THROW; // UnhandledMatchError if no default node exists + $combined_status = 0; + foreach ($node->children as $arm_node) { + // @phan-suppress-next-line PhanPossiblyUndeclaredProperty + $arm_cond = $arm_node->children['cond']; + if ($arm_cond === null) { + $default_status = $this->visitMatchArm($arm_node); + continue; + } + $combined_status |= $this->visitMatchArm($arm_node); + } + return $default_status | $combined_status; + } + + public const UNEXITABLE_LOOP_INNER_STATUS = self::STATUS_PROCEED | self::STATUS_CONTINUE; + + public const STATUS_CONTINUE_OR_BREAK = self::STATUS_CONTINUE | self::STATUS_BREAK; + + public function visitForeach(Node $node): int + { + // We assume foreach loops are over a finite sequence, and that it's possible for that sequence to have at least one element. + $inner_status = $this->check($node->children['stmts']); + + // 1. break/continue apply to the inside of a loop, not outside. Not going to analyze "break 2;", may emit an info level issue in the future. + // 2. We assume that it's possible that any given loop can have 0 iterations. + // A TODO exists above to check for special cases. + return ($inner_status & ~self::STATUS_CONTINUE_OR_BREAK) | self::STATUS_PROCEED; + } + + public function visitWhile(Node $node): int + { + $inner_status = $this->check($node->children['stmts']); + // TODO: Check for unconditionally false conditions. + if (self::isTruthyLiteral($node->children['cond'])) { + // Use a special case to analyze "while (1) {exprs}" or "for (; true; ) {exprs}" + // TODO: identify infinite loops, mark those as STATUS_NO_PROCEED or STATUS_RETURN. + return self::computeDerivedStatusOfInfiniteLoop($inner_status); + } + // This is (to our awareness) **not** an infinite loop + + + // 1. break/continue apply to the inside of a loop, not outside. Not going to analyze "break 2;", may emit an info level issue in the future. + // 2. We assume that it's possible that any given loop can have 0 iterations. + // A TODO exists above to check for special cases. + return ($inner_status & ~self::STATUS_CONTINUE_OR_BREAK) | self::STATUS_PROCEED; + } + + /** + * @return int the corresponding status code + */ + public function visitFor(Node $node): int + { + $inner_status = $this->check($node->children['stmts']); + // for loops have an expression list as a condition. + $cond_nodes = $node->children['cond']->children ?? []; // NOTE: $node->children['cond'] is null for the expression `for (;;)` + // TODO: Check for unconditionally false conditions. + if (count($cond_nodes) === 0 || self::isTruthyLiteral(\end($cond_nodes))) { + // Use a special case to analyze "while (1) {exprs}" or "for (; true; ) {exprs}" + // TODO: identify infinite loops, mark those as STATUS_NO_PROCEED or STATUS_RETURN. + return self::computeDerivedStatusOfInfiniteLoop($inner_status); + } + // This is (to our awareness) **not** an infinite loop + + + // 1. break/continue apply to the inside of a loop, not outside. Not going to analyze "break 2;", may emit an info level issue in the future. + // 2. We assume that it's possible that any given loop can have 0 iterations. + // A TODO exists above to check for special cases. + return ($inner_status & ~self::STATUS_CONTINUE_OR_BREAK) | self::STATUS_PROCEED; + // TODO: Improve this by checking for loops which almost definitely have at least one iteration, + // such as "foreach ([$val] as $v)" or "for ($i = 0; $i < 10; $i++)" + + // if (($inner_status & ~self::STATUS_THROW_OR_RETURN_BITMASK) === 0) { + // // The inside of the loop will unconditionally throw or return. + // return $inner_status + // } + } + + // Logic to determine status of "while (1) {exprs}" or "for (; true; ) {exprs}" + // TODO: identify infinite loops, mark those as STATUS_NO_PROCEED or STATUS_RETURN. + private static function computeDerivedStatusOfInfiniteLoop(int $inner_status): int + { + $status = $inner_status & ~self::UNEXITABLE_LOOP_INNER_STATUS; + if ($status === 0) { + return self::STATUS_RETURN; // this is an infinite loop, it didn't contain break/throw/return statements? + } + if (($status & self::STATUS_BREAK) !== 0) { + // if the inside of "while (true) {} contains a break statement, + // then execution can proceed past the end of the loop. + return ($status & ~self::STATUS_BREAK) | self::STATUS_PROCEED; + } + return $status; + } + + /** + * A return statement unconditionally returns (Assume expression passed in doesn't throw) + * @return int the corresponding status code + */ + public function visitReturn(Node $node): int + { + return self::STATUS_RETURN; + } + + /** + * An exit statement unconditionally exits (Assume expression passed in doesn't throw) + * @return int the corresponding status code + */ + public function visitExit(Node $node): int + { + return self::STATUS_RETURN; + } + + /** + * @return int the corresponding status code + */ + public function visitUnaryOp(Node $node): int + { + // Don't modify $node->flags, use unmodified flags here + if ($node->flags !== \ast\flags\UNARY_SILENCE) { + return self::STATUS_PROCEED; + } + // Analyze exit status of `@expr` like `expr` (e.g. @trigger_error()) + $expr = $node->children['expr']; + if (!($expr instanceof Node)) { + return self::STATUS_PROCEED; + } + return $this->__invoke($expr); + } + + /** + * Determines the exit status of a function call, such as trigger_error() + * + * NOTE: A trigger_error() statement may or may not exit, depending on the constant and user configuration. + * @return int the corresponding status code + */ + public function visitCall(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = self::computeStatusOfCall($node); + $node->flags = $status; + return $status; + } + + private static function computeStatusOfCall(Node $node): int + { + $expression = $node->children['expr']; + if ($expression instanceof Node) { + if ($expression->kind !== \ast\AST_NAME) { + return self::STATUS_PROCEED; // best guess + } + $function_name = $expression->children['name']; + if (!\is_string($function_name)) { + return self::STATUS_PROCEED; + } + } else { + if (!\is_string($expression)) { + return self::STATUS_THROW; // Probably impossible. + } + $function_name = $expression; + } + if ($function_name === '') { + return self::STATUS_THROW; // nonsense such as ''(); + } + if ($function_name[0] === '\\') { + $function_name = \substr($function_name, 1); + } + // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal + if (\strcasecmp($function_name, 'trigger_error') === 0) { + return self::computeTriggerErrorStatusCodeForConstant($node->children['args']->children[1] ?? null); + } + // TODO: Could allow .phan/config.php or plugins to define additional behaviors, e.g. for methods. + // E.g. if (!$var) {HttpFramework::generate_302_and_die(); } + return self::STATUS_PROCEED; + } + + /** + * @param ?(Node|string|int|float) $constant_ast + */ + private static function computeTriggerErrorStatusCodeForConstant($constant_ast): int + { + // return PROCEED if this can't be determined. + // TODO: Could check for integer literals + if (!($constant_ast instanceof Node)) { + return self::STATUS_PROCEED; + } + if ($constant_ast->kind !== \ast\AST_CONST) { + return self::STATUS_PROCEED; + } + $name = $constant_ast->children['name']->children['name'] ?? null; + if (!\is_string($name)) { + return self::STATUS_PROCEED; + } + if (\in_array($name, ['E_ERROR', 'E_PARSE', 'E_CORE_ERROR', 'E_COMPILE_ERROR', 'E_USER_ERROR'], true)) { + return self::STATUS_RETURN; + } + if ($name === 'E_RECOVERABLE_ERROR') { + return self::STATUS_THROW; + } + + return self::STATUS_PROCEED; // Assume this is a warning or notice? + } + + /** + * A statement list has the weakest return status out of all of the (non-PROCEEDing) statements. + * FIXME: This is buggy, doesn't account for one statement having STATUS_CONTINUE some of the time but not all of it. + * (We don't check for STATUS_CONTINUE yet, so this doesn't matter yet.) + * @return int the corresponding status code + */ + public function visitStmtList(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = $this->computeStatusOfBlock($node->children); + $node->flags = $status; + return $status; + } + + /** + * An expression list has the weakest return status out of all of the (non-PROCEEDing) statements. + * @return int the corresponding status code + * @override + */ + public function visitExprList(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = $this->computeStatusOfBlock($node->children); + $node->flags = $status; + return $status; + } + + /** + * Analyzes a node with kind \ast\AST_IF + * @return int the exit status of a block (whether or not it would unconditionally exit, return, throw, etc. + * @override + */ + public function visitIf(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + $status = $this->computeStatusOfIf($node); + $node->flags = $status; + return $status; + } + + private function computeStatusOfIf(Node $node): int + { + $has_if_elems_for_all_cases = false; + $combined_statuses = 0; + foreach ($node->children as $child_node) { + '@phan-var Node $child_node'; + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null + $status = $this->visitStmtList($child_node->children['stmts']); + $combined_statuses |= $status; + + $cond_node = $child_node->children['cond']; + // check for "else" or "elseif (true)" + if ($cond_node === null || self::isTruthyLiteral($cond_node)) { + $has_if_elems_for_all_cases = true; + break; + } + } + if (!$has_if_elems_for_all_cases) { + $combined_statuses |= self::STATUS_PROCEED; + } + return $combined_statuses; + } + + + /** + * Analyzes a node with kind \ast\AST_DO_WHILE + * @return int the exit status of a block (whether or not it would unconditionally exit, return, throw, etc. + */ + public function visitDoWhile(Node $node): int + { + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null + $inner_status = $this->visitStmtList($node->children['stmts']); + if (($inner_status & ~self::STATUS_THROW_OR_RETURN_BITMASK) === 0) { + // The inner block throws or returns before the end can be reached. + return $inner_status; + } + // TODO: Check for unconditionally false conditions. + if (self::isTruthyLiteral($node->children['cond'])) { + // Use a special case to analyze "while (1) {exprs}" or "for (; true; ) {exprs}" + // TODO: identify infinite loops, mark those as STATUS_NO_PROCEED or STATUS_RETURN. + return $this->computeDerivedStatusOfInfiniteLoop($inner_status); + } + // This is (to our awareness) **not** an infinite loop + + + // 1. break/continue apply to the inside of a loop, not outside. Not going to analyze "break 2;", may emit an info level issue in the future. + // 2. We assume that it's possible that any given loop can have 0 iterations. + // A TODO exists above to check for special cases. + return ($inner_status & ~self::STATUS_CONTINUE_OR_BREAK) | self::STATUS_PROCEED; + } + + /** + * Analyzes a node with kind \ast\AST_IF_ELEM + * @return int the exit status of a block (whether or not it would unconditionally exit, return, throw, etc. + */ + public function visitIfElem(Node $node): int + { + $status = $node->flags & self::STATUS_BITMASK; + if ($status) { + return $status; + } + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null + $status = $this->visitStmtList($node->children['stmts']); + $node->flags = $status; + return $status; + } + + /** + * @return int the corresponding status code + */ + public function visitGoto(Node $node): int + { + return self::STATUS_GOTO; + } + + /** + * @param list $block + * @return int the exit status of a block (whether or not it would unconditionally exit, return, throw, etc. + */ + private function computeStatusOfBlock(array $block): int + { + $maybe_status = 0; + foreach ($block as $child) { + if ($child === null) { + continue; + } + // e.g. can be non-Node for statement lists such as `if ($a) { return; }echo "X";2;` (under unknown conditions) + if (!($child instanceof Node)) { + continue; + } + $status = $this->check($child); + if (($status & self::STATUS_PROCEED) === 0) { + // If it's guaranteed we won't stop after this statement, + // then skip the subsequent statements. + return $status | ($maybe_status & ~self::STATUS_PROCEED); + } + $maybe_status |= $status; + } + return self::STATUS_PROCEED | $maybe_status; + } + + /** + * Will the node $node unconditionally never fall through to the following statement? + */ + public static function willUnconditionallySkipRemainingStatements(Node $node): bool + { + return ((new self())->__invoke($node) & self::STATUS_MAYBE_PROCEED) === 0; + } + + /** + * Will the node $node unconditionally throw or return (or exit), + */ + public static function willUnconditionallyThrowOrReturn(Node $node): bool + { + return ((new self())->__invoke($node) & ~self::STATUS_THROW_OR_RETURN_BITMASK) === 0; + } + + /** + * Will the node $node unconditionally proceed (no break/continue, throw, or goto) + */ + public static function willUnconditionallyProceed(Node $node): bool + { + return (new self())->__invoke($node) === self::STATUS_PROCEED; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ClassConstantTypesAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/ClassConstantTypesAnalyzer.php new file mode 100644 index 000000000..7b06ad96c --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ClassConstantTypesAnalyzer.php @@ -0,0 +1,108 @@ +getConstantMap($code_base) as $constant) { + // This phase is done before the analysis phase, so there aren't any dynamic properties to filter out. + + // Get the union type of this constant. This may throw (e.g. it can refers to missing elements). + $comment = $constant->getComment(); + if (!$comment) { + continue; + } + foreach ($comment->getVariableList() as $variable_comment) { + try { + $union_type = $variable_comment->getUnionType(); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $code_base, + $constant->getContext(), + $exception->getIssueInstance() + ); + continue; + } + + if ($union_type->hasTemplateTypeRecursive()) { + Issue::maybeEmit( + $code_base, + $constant->getContext(), + Issue::TemplateTypeConstant, + $constant->getFileRef()->getLineNumberStart(), + $constant->getFQSEN() + ); + } + // Look at each type in the parameter's Union Type + foreach ($union_type->withFlattenedArrayShapeOrLiteralTypeInstances()->getTypeSet() as $outer_type) { + $has_object = $outer_type->isObject(); + foreach ($outer_type->getReferencedClasses() as $type) { + $has_object = true; + // If it's a reference to self, its OK + if ($type->isSelfType()) { + continue; + } + + if (!($constant->hasDefiningFQSEN() && $constant->getDefiningFQSEN() === $constant->getFQSEN())) { + continue; + } + if ($type instanceof TemplateType) { + continue; + } + + // Make sure the class exists + $type_fqsen = FullyQualifiedClassName::fromType($type); + + if ($code_base->hasClassWithFQSEN($type_fqsen)) { + if ($code_base->hasClassWithFQSEN($type_fqsen->withAlternateId(1))) { + UnionType::emitRedefinedClassReferenceWarning( + $code_base, + $constant->getContext(), + $type_fqsen + ); + } + } else { + Issue::maybeEmitWithParameters( + $code_base, + $constant->getContext(), + Issue::UndeclaredTypeClassConstant, + $constant->getFileRef()->getLineNumberStart(), + [$constant->getFQSEN(), (string)$outer_type], + IssueFixSuggester::suggestSimilarClass($code_base, $constant->getContext(), $type_fqsen, null, 'Did you mean', IssueFixSuggester::CLASS_SUGGEST_CLASSES_AND_TYPES) + ); + } + } + if ($has_object) { + Issue::maybeEmitWithParameters( + $code_base, + $constant->getContext(), + Issue::CommentObjectInClassConstantType, + $constant->getFileRef()->getLineNumberStart(), + [$constant->getFQSEN(), (string)$outer_type] + ); + } + } + } + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ClassInheritanceAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/ClassInheritanceAnalyzer.php new file mode 100644 index 000000000..7dfb73880 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ClassInheritanceAnalyzer.php @@ -0,0 +1,175 @@ +isPHPInternal()) { + return; + } + + if ($clazz->hasParentType()) { + $class_exists = self::fqsenExistsForClass( + $clazz->getParentClassFQSEN(), + $code_base, + $clazz, + Issue::UndeclaredExtendedClass + ); + + if ($class_exists) { + self::testClassAccess( + $clazz, + $clazz->getParentClass($code_base), + $code_base + ); + } + } + + foreach ($clazz->getInterfaceFQSENList() as $fqsen) { + $class_exists = self::fqsenExistsForClass( + $fqsen, + $code_base, + $clazz, + Issue::UndeclaredInterface + ); + + if ($class_exists) { + self::testClassAccess( + $clazz, + $code_base->getClassByFQSEN($fqsen), + $code_base + ); + } + } + + foreach ($clazz->getTraitFQSENList() as $fqsen) { + $class_exists = self::fqsenExistsForClass( + $fqsen, + $code_base, + $clazz, + Issue::UndeclaredTrait + ); + if ($class_exists) { + self::testClassAccess( + $clazz, + $code_base->getClassByFQSEN($fqsen), + $code_base + ); + } + } + } + + /** + * @return bool + * True if the FQSEN exists. If not, a log line is emitted + */ + private static function fqsenExistsForClass( + FullyQualifiedClassName $fqsen, + CodeBase $code_base, + Clazz $clazz, + string $issue_type + ): bool { + if (!$code_base->hasClassWithFQSEN($fqsen)) { + $filter = null; + switch ($issue_type) { + case Issue::UndeclaredExtendedClass: + $filter = IssueFixSuggester::createFQSENFilterForClasslikeCategories($code_base, true, false, false); + break; + case Issue::UndeclaredTrait: + $filter = IssueFixSuggester::createFQSENFilterForClasslikeCategories($code_base, false, true, false); + break; + case Issue::UndeclaredInterface: + $filter = IssueFixSuggester::createFQSENFilterForClasslikeCategories($code_base, false, false, true); + break; + } + $suggestion = IssueFixSuggester::suggestSimilarClass($code_base, $clazz->getContext(), $fqsen, $filter); + + Issue::maybeEmitWithParameters( + $code_base, + $clazz->getContext(), + $issue_type, + $clazz->getLinenoOfAncestorReference($fqsen), + [(string)$fqsen], + $suggestion + ); + + return false; + } + + return true; + } + + /** + * @param Clazz $source_class + * The class accessing the $target_class + * + * @param Clazz $target_class + * The class being accessed from the $source_class + * + * @param CodeBase $code_base + * The code base in which both classes exist + */ + private static function testClassAccess( + Clazz $source_class, + Clazz $target_class, + CodeBase $code_base + ): void { + if ($target_class->isNSInternal($code_base) + && !$target_class->isNSInternalAccessFromContext( + $code_base, + $source_class->getContext() + ) + ) { + Issue::maybeEmit( + $code_base, + $source_class->getInternalContext(), + Issue::AccessClassInternal, + $source_class->getFileRef()->getLineNumberStart(), + (string)$target_class, + $target_class->getFileRef()->getFile(), + (string)$target_class->getFileRef()->getLineNumberStart() + ); + } + if ($target_class->isDeprecated()) { + if ($target_class->isTrait()) { + $issue_type = Issue::DeprecatedTrait; + } elseif ($target_class->isInterface()) { + $issue_type = Issue::DeprecatedInterface; + } else { + $issue_type = Issue::DeprecatedClass; + } + Issue::maybeEmit( + $code_base, + $source_class->getInternalContext(), + $issue_type, + $source_class->getFileRef()->getLineNumberStart(), + $target_class->getFQSEN(), + $target_class->getContext()->getFile(), + $target_class->getContext()->getLineNumberStart(), + $target_class->getDeprecationReason() + ); + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/CompositionAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/CompositionAnalyzer.php new file mode 100644 index 000000000..b457f9d8d --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/CompositionAnalyzer.php @@ -0,0 +1,146 @@ +getAncestorClassList($code_base); + + // No chance of failed composition if we don't inherit from anything. + if (!$inherited_class_list) { + return; + } + + // Since we're not necessarily getting this list of classes + // via getClass, we need to ensure that hydration has occurred. + $class->hydrate($code_base); + + // For each property, find out every inherited class that defines it + // and check to see if the types line up. + // (This must be done after hydration, because some properties are loaded from traits) + foreach ($class->getPropertyMap($code_base) as $property) { + try { + $property_union_type = $property->getDefaultType() ?? UnionType::empty(); + } catch (IssueException $_) { + $property_union_type = UnionType::empty(); + } + + // Check for that property on each inherited + // class/trait/interface + foreach ($inherited_class_list as $inherited_class) { + $inherited_class->hydrate($code_base); + + // Skip any classes/traits/interfaces not defining that + // property + if (!$inherited_class->hasPropertyWithName($code_base, $property->getName())) { + continue; + } + + // We don't call `getProperty` because that will create + // them in some circumstances. + $inherited_property_map = + $inherited_class->getPropertyMap($code_base); + + if (!isset($inherited_property_map[$property->getName()])) { + continue; + } + + // Get the inherited property + $inherited_property = + $inherited_property_map[$property->getName()]; + + if ($inherited_property->isDynamicOrFromPHPDoc()) { + continue; + } + if ($inherited_property->getRealDefiningFQSEN() === $property->getRealDefiningFQSEN()) { + continue; + } + + // Figure out if this property type can cast to the + // inherited definition's type. + // Use the phpdoc comment or real type declaration instead of the inferred + // types from the default to perform this check. + try { + $inherited_property_union_type = $inherited_property->getDefaultType() ?? UnionType::empty(); + } catch (IssueException $_) { + $inherited_property_union_type = UnionType::empty(); + } + if (!$property->isDynamicOrFromPHPDoc()) { + $real_property_type = $property->getRealUnionType()->asNormalizedTypes(); + $real_inherited_property_type = $inherited_property->getRealUnionType()->asNormalizedTypes(); + if (!$real_property_type->isEqualTo($real_inherited_property_type)) { + Issue::maybeEmit( + $code_base, + $property->getContext(), + Issue::IncompatibleRealPropertyType, + $property->getFileRef()->getLineNumberStart(), + $property->getFQSEN(), + $real_property_type, + $inherited_property->getFQSEN(), + $real_inherited_property_type, + $inherited_property->getFileRef()->getFile(), + $inherited_property->getFileRef()->getLineNumberStart() + ); + } + } + + if ($property->getFQSEN() === $property->getRealDefiningFQSEN()) { + // No need to warn about incompatible composition of trait with another ancestor if the property's default was overridden + continue; + } + $can_cast = + $property_union_type->canCastToExpandedUnionType( + $inherited_property_union_type, + $code_base + ); + + if ($can_cast) { + continue; + } + + // Don't emit an issue if the property suppresses the issue + // NOTE: The current context is the class, not either of the properties. + if ($property->checkHasSuppressIssueAndIncrementCount(Issue::IncompatibleCompositionProp)) { + continue; + } + + Issue::maybeEmit( + $code_base, + $property->getContext(), + Issue::IncompatibleCompositionProp, + $property->getFileRef()->getLineNumberStart(), + (string)$class->getFQSEN(), + (string)$inherited_class->getFQSEN(), + $property->getName(), + (string)$class->getFQSEN(), + $property_union_type, + $inherited_property_union_type, + $class->getFileRef()->getFile(), + $class->getFileRef()->getLineNumberStart() + ); + } + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor.php new file mode 100644 index 000000000..560057912 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor.php @@ -0,0 +1,1233 @@ +code_base = $code_base; + $this->context = $context; + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visit(Node $node): Context + { + $this->checkVariablesDefined($node); + if (Config::getValue('redundant_condition_detection')) { + $this->checkRedundantOrImpossibleTruthyCondition($node, $this->context, null, false); + } + return $this->context; + } + + /** + * Check if variables from within a generic condition are defined. + * @param Node $node + * A node to parse + */ + protected function checkVariablesDefined(Node $node): void + { + while ($node->kind === ast\AST_UNARY_OP) { + $node = $node->children['expr']; + if (!($node instanceof Node)) { + return; + } + } + // Get the type just to make sure everything + // is defined. + UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node, + true + ); + } + + /** + * Check if variables from within isset are defined. + * @param Node $node + * A node to parse + */ + private function checkVariablesDefinedInIsset(Node $node): void + { + while ($node->kind === ast\AST_UNARY_OP) { + $node = $node->children['expr']; + if (!($node instanceof Node)) { + return; + } + } + if ($node->kind === ast\AST_DIM) { + $this->checkArrayAccessDefined($node); + return; + } + // Get the type just to make sure everything + // is defined. + UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node, + true + ); + } + + /** + * Analyzes (isset($x['field'])) + * @return void + * + * TODO: Add to NegatedConditionVisitor + */ + private function checkArrayAccessDefined(Node $node): void + { + $code_base = $this->code_base; + $context = $this->context; + + // TODO: Infer that the offset exists after this check + UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['dim'], + true + ); + // Check the array type to trigger TypeArraySuspicious + /* $array_type = */ + UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['expr'], + true + ); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitBinaryOp(Node $node): Context + { + $flags = $node->flags; + switch ($flags) { + case flags\BINARY_BOOL_AND: + return $this->analyzeShortCircuitingAnd($node->children['left'], $node->children['right']); + case flags\BINARY_BOOL_OR: + return $this->analyzeShortCircuitingOr($node->children['left'], $node->children['right']); + case flags\BINARY_IS_IDENTICAL: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeIdentical($node->children['left'], $node->children['right']); + case flags\BINARY_IS_EQUAL: + // TODO: Could be more precise, and preserve 0, [], etc. for `$x == null` + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeEqual($node->children['left'], $node->children['right']); + case flags\BINARY_IS_NOT_IDENTICAL: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeNotIdentical($node->children['left'], $node->children['right']); + case flags\BINARY_IS_NOT_EQUAL: + return $this->analyzeAndUpdateToBeNotEqual($node->children['left'], $node->children['right']); + case flags\BINARY_IS_GREATER: + case flags\BINARY_IS_GREATER_OR_EQUAL: + case flags\BINARY_IS_SMALLER: + case flags\BINARY_IS_SMALLER_OR_EQUAL: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeCompared($node->children['left'], $node->children['right'], $flags); + default: + $this->checkVariablesDefined($node); + return $this->context; + } + } + + /** + * @param Node|string|int|float $left + * a Node or non-node to parse (possibly an AST literal) + * + * @param Node|string|int|float $right + * a Node or non-node to parse (possibly an AST literal) + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the short-circuiting logical and. + */ + private function analyzeShortCircuitingAnd($left, $right): Context + { + // Aside: If left/right is not a node, left/right is a literal such as a number/string, and is either always truthy or always falsey. + // Inside of this conditional may be dead or redundant code. + if ($left instanceof Node) { + $this->context = $this->__invoke($left); + } + // TODO: Warn if !$left + if ($right instanceof Node) { + return $this->__invoke($right); + } + return $this->context; + } + + /** + * @param Node|string|int|float $left + * a Node or non-node to parse (possibly an AST literal) + * + * @param Node|string|int|float $right + * a Node or non-node to parse (possibly an AST literal) + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the short-circuiting logical or. + * @suppress PhanSuspiciousTruthyString deliberate check + */ + private function analyzeShortCircuitingOr($left, $right): Context + { + // Aside: If left/right is not a node, left/right is a literal such as a number/string, and is either always truthy or always falsey. + // Inside of this conditional may be dead or redundant code. + if (!($left instanceof Node)) { + if ($left) { + return $this->context; + } + if (!($right instanceof Node)) { + return $this->context; + } + return $this->__invoke($right); + } + if (!($right instanceof Node)) { + if ($right) { + return $this->context; + } + return $this->__invoke($left); + } + $code_base = $this->code_base; + $context = $this->context; + $left_false_context = (new NegatedConditionVisitor($code_base, $context))->__invoke($left); + $left_true_context = (new ConditionVisitor($code_base, $context))->__invoke($left); + // We analyze the right-hand side of `cond($x) || cond2($x)` as if `cond($x)` was false. + $right_true_context = (new ConditionVisitor($code_base, $left_false_context))->__invoke($right); + // When the ConditionVisitor is true, at least one of the left or right contexts must be true. + return (new ContextMergeVisitor($context, [$left_true_context, $right_true_context]))->combineChildContextList(); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitUnaryOp(Node $node): Context + { + $expr_node = $node->children['expr']; + $flags = $node->flags; + if ($flags !== flags\UNARY_BOOL_NOT) { + if (Config::getValue('redundant_condition_detection')) { + $this->checkRedundantOrImpossibleTruthyCondition($node, $this->context, null, false); + } + // TODO: Emit dead code issue for non-nodes + if ($expr_node instanceof Node) { + if ($flags === flags\UNARY_SILENCE) { + return $this->__invoke($expr_node); + } + $this->checkVariablesDefined($expr_node); + } + return $this->context; + } + // TODO: Emit dead code issue for non-nodes + if ($expr_node instanceof Node) { + return (new NegatedConditionVisitor($this->code_base, $this->context))->__invoke($expr_node); + } elseif (Config::getValue('redundant_condition_detection')) { + // Check `scalar` of `if (!scalar)` + $this->checkRedundantOrImpossibleTruthyCondition($expr_node, $this->context, null, true); + } + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitIsset(Node $node): Context + { + $var_node = $node->children['var']; + if (!($var_node instanceof Node)) { + return $this->context; + } + if ($var_node->kind !== ast\AST_VAR) { + return $this->checkComplexIsset($var_node); + } + + $var_name = $var_node->children['name']; + if (!\is_string($var_name)) { + $this->checkVariablesDefinedInIsset($var_node); + return $this->context; + } + return $this->withSetVariable($var_name, $var_node, $var_node); + } + + public const ACCESS_IS_OBJECT = 1; + public const ACCESS_ARRAY_KEY_EXISTS = 2; + public const ACCESS_IS_SET = 3; + public const ACCESS_DIM_SET = 4; + public const ACCESS_STRING_DIM_SET = 5; + + /** @internal */ + public const DEFAULTS_FOR_ACCESS_TYPE = [ + self::ACCESS_IS_OBJECT => 'object', + self::ACCESS_ARRAY_KEY_EXISTS => 'non-empty-array|object', + self::ACCESS_IS_SET => 'int|string|float|bool|non-empty-array|object|resource', + self::ACCESS_DIM_SET => 'string|non-empty-array|object', + self::ACCESS_STRING_DIM_SET => 'non-empty-array|object', + ]; + + /** + * From isset($var), infer that $var is non-null + * From isset($obj->prop['field']), infer that $obj is non-null + * Also infer that $obj is an object (don't do that for $obj['field']->prop) + */ + private function withSetVariable(string $var_name, Node $var_node, Node $ancestor_node): Context + { + $context = $this->context; + $is_object = $var_node->kind === ast\AST_PROP; + + $scope = $context->getScope(); + if (!$scope->hasVariableWithName($var_name)) { + $new_type = Variable::getUnionTypeOfHardcodedVariableInScopeWithName($var_name, $context->isInGlobalScope()); + if (!$new_type || ($is_object && !$new_type->hasObjectTypes())) { + $new_type = $is_object ? ObjectType::instance(false)->asRealUnionType() : UnionType::empty(); + } + // Support analyzing cases such as `if (isset($x)) { use($x); }`, or `assert(isset($x))` + return $context->withScopeVariable(new Variable( + $context->withLineNumberStart($var_node->lineno ?? 0), + $var_name, + $new_type, + 0 + )); + } + if ($is_object) { + $variable = clone($context->getScope()->getVariableByName($var_name)); + $this->analyzeIsObjectAssertion($variable); + $context = $this->modifyPropertySimple($var_node, static function (UnionType $type): UnionType { + return $type->nonNullableClone(); + }, $context); + if ($ancestor_node !== $var_node && self::isThisVarNode($var_node->children['expr'])) { + $old_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $ancestor_node); + $context = (new AssignmentVisitor( + $this->code_base, + // We clone the original context to avoid affecting the original context for the elseif. + // AssignmentVisitor modifies the provided context in place. + // + // There is a difference between `if (is_string($x['field']))` and `$x['field'] = (some string)` for the way the `elseif` should be analyzed. + $context->withClonedScope(), + $ancestor_node, + $old_type->nonNullableClone() + ))->__invoke($ancestor_node); + } + return $context->withScopeVariable($variable); + } + if ($var_node !== $ancestor_node) { + return $this->removeTypesNotSupportingAccessFromVariable( + $var_node, + $context, + $ancestor_node->kind === ast\AST_PROP ? self::ACCESS_IS_OBJECT : self::ACCESS_DIM_SET + ); + } + return $this->removeNullFromVariable($var_node, $context, true); + } + + /** + * @param Node $node a node that is NOT of type ast\AST_VAR + */ + private function checkComplexIsset(Node $node): Context + { + // Loop to support getting the var name in is_array($x['field'][0]) + $has_prop_access = false; + $context = $this->context; + $var_node = $node; + $parent_node = $node; + while (true) { + $kind = $var_node->kind; + if ($kind === ast\AST_VAR) { + break; + } + $parent_node = $var_node; + if ($kind === ast\AST_DIM) { + $var_node = $var_node->children['expr']; + if (!$var_node instanceof Node) { + return $context; + } + continue; + } elseif ($kind === ast\AST_PROP) { + // TODO modify this pseudo-variable for $this->prop + $has_prop_access = true; + $var_node = $var_node->children['expr']; + if (!$var_node instanceof Node) { + return $context; + } + continue; + } + + // TODO: Handle more than one level of nesting + return $context; + } + $var_name = $var_node->children['name']; + if (!\is_string($var_name)) { + return $context; + } + if ($has_prop_access) { + // For `$x->prop['field'][0]`, $parent_node would be `$x->prop`. + // And for that expression, phan would infer that $var_name was non-null AND an object. + return $this->withSetVariable($var_name, $parent_node, $node); + } + + // This is $x['field'] or $x[$i][something] + + if (!$context->getScope()->hasVariableWithName($var_name)) { + $new_type = Variable::getUnionTypeOfHardcodedVariableInScopeWithName($var_name, $context->isInGlobalScope()); + if (!$new_type || !$new_type->hasArrayLike()) { + $new_type = ArrayType::instance(false)->asPHPDocUnionType(); + } + // Support analyzing cases such as `if (isset($x['key'])) { use($x); }`, or `assert(isset($x['key']))` + return $context->withScopeVariable(new Variable( + $context->withLineNumberStart($node->lineno ?? 0), + $var_name, + $new_type, // can be array or (unlikely) ArrayAccess + 0 + )); + } + if ($var_node === $node) { + $context = $this->removeNullFromVariable($var_node, $context, true); + } else { + if ($parent_node->kind === ast\AST_PROP) { + // `isset($x->prop)` implies $x is an object + $access_kind = self::ACCESS_IS_OBJECT; + } else { + // Allow `isset($x[0])` to imply $x can be a string, but not `isset($x['field'])` + $dim = $node->children['dim'] ?? null; + if (\is_string($dim) && \filter_var($dim, \FILTER_VALIDATE_INT) === false) { + $access_kind = self::ACCESS_STRING_DIM_SET; + } else { + $access_kind = self::ACCESS_DIM_SET; + } + } + $context = $this->removeTypesNotSupportingAccessFromVariable( + $var_node, + $context, + $access_kind + ); + } + + $variable = $context->getScope()->getVariableByName($var_name); + $var_node_union_type = $variable->getUnionType(); + + if ($var_node_union_type->hasTopLevelArrayShapeTypeInstances()) { + $new_union_type = $this->withSetArrayShapeTypes($var_node_union_type, $parent_node->children['dim'], $context, true); + if ($new_union_type !== $var_node_union_type) { + $variable = clone($variable); + $variable->setUnionType($new_union_type); + $context = $context->withScopeVariable($variable); + } + } + $this->context = $context; + return $context; + } + + /** + * @param UnionType $union_type the type being modified by inferences from isset or array_key_exists + * @param Node|string|float|int|bool $dim_node represents the dimension being accessed. (E.g. can be a literal or an AST_CONST, etc. + * @param Context $context the context with inferences made prior to this condition + * + * @param bool $non_nullable if an offset is created, will it be non-nullable? + */ + private function withSetArrayShapeTypes(UnionType $union_type, $dim_node, Context $context, bool $non_nullable): UnionType + { + $dim_value = $dim_node instanceof Node ? (new ContextNode($this->code_base, $context, $dim_node))->getEquivalentPHPScalarValue() : $dim_node; + // TODO: detect and warn about null + if (!\is_scalar($dim_value)) { + return $union_type; + } + + $dim_union_type = UnionTypeVisitor::resolveArrayShapeElementTypesForOffset($union_type, $dim_value); + if (!$dim_union_type) { + // There are other types, this dimension does not exist yet + if (!$union_type->hasTopLevelArrayShapeTypeInstances()) { + return $union_type; + } + return ArrayType::combineArrayShapeTypesWithField($union_type, $dim_value, MixedType::instance(false)->asPHPDocUnionType()); + } elseif ($dim_union_type->containsNullableOrUndefined()) { + if (!$non_nullable && !$dim_union_type->isPossiblyUndefined()) { + // The offset in question already exists in the array shape type, and we won't be changing it. + // (E.g. array_key_exists('key', $x) where $x is array{key:?int,other:string}) + return $union_type; + } + + return ArrayType::combineArrayShapeTypesWithField( + $union_type, + $dim_value, + $dim_union_type->nonNullableClone()->withIsPossiblyUndefined(false) + ); + } + return $union_type; + } + + /** + * @param Node $node + * A node to parse, with kind ast\AST_VAR + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitVar(Node $node): Context + { + $this->checkVariablesDefined($node); + return $this->removeFalseyFromVariable($node, $this->context, false); + } + + public function visitNullsafeProp(Node $node): Context + { + return $this->visitProp($node); + } + + /** + * @param Node $node + * A node to parse, with kind ast\AST_PROP (e.g. `if ($this->prop_name)`) + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitProp(Node $node): Context + { + // TODO: Make this imply $expr_node is an object? + $expr_node = $node->children['expr']; + if (!($expr_node instanceof Node)) { + return $this->context; + } + if ($expr_node->kind !== ast\AST_VAR || $expr_node->children['name'] !== 'this') { + return $this->context; + } + if (!\is_string($node->children['prop'])) { + return $this->context; + } + return $this->modifyPropertyOfThisSimple( + $node, + function (UnionType $type) use ($node): UnionType { + if (Config::getValue('error_prone_truthy_condition_detection')) { + $this->checkErrorProneTruthyCast($node, $this->context, $type); + } + return $type->nonFalseyClone(); + }, + $this->context + ); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitInstanceof(Node $node): Context + { + //$this->checkVariablesDefined($node); + // Only look at things of the form + // `$variable instanceof ClassName` + $context = $this->context; + $class_node = $node->children['class']; + if (!($class_node instanceof Node)) { + return $context; + } + $expr_node = $node->children['expr']; + if (!($expr_node instanceof Node)) { + return $context; + } + if ($expr_node->kind !== ast\AST_VAR) { + return $this->modifyComplexExpression( + $expr_node, + /** + * @param list $args + */ + function (CodeBase $code_base, Context $context, Variable $variable, array $args) use ($class_node): void { + $this->setInstanceofVariableType($variable, $class_node); + }, + $context, + [] + ); + } + + try { + // Get the variable we're operating on + $variable = $this->getVariableFromScope($expr_node, $context); + if (\is_null($variable)) { + return $context; + } + // Make a copy of the variable + $variable = clone($variable); + $this->setInstanceofVariableType($variable, $class_node); + // Overwrite the variable with its new type + $context = $context->withScopeVariable( + $variable + ); + } catch (IssueException $exception) { + Issue::maybeEmitInstance($this->code_base, $context, $exception->getIssueInstance()); + } catch (\Exception $_) { + // Swallow it + } + + return $context; + } + + /** + * Modifies the union type of $variable in place + */ + private function setInstanceofVariableType(Variable $variable, Node $class_node): void + { + // Get the type that we're checking it against + $type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $class_node + ); + $object_types = $type->objectTypesStrict(); + if (!$object_types->isEmpty()) { + // We know that the variable is the provided object type (or a subclass) + // See https://secure.php.net/instanceof - + + // Add the type to the variable + $variable->setUnionType(self::calculateNarrowedUnionType($this->code_base, $this->context, $variable->getUnionType(), $object_types)); + } else { + // We know that variable is some sort of object if this condition is true. + if ($class_node->kind !== ast\AST_NAME && + !$type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeInvalidInstanceof, + $this->context->getLineNumberStart(), + ASTReverter::toShortString($class_node), + (string)$type->asNonLiteralType() + ); + } + self::analyzeIsObjectAssertion($variable); + } + } + + /** + * E.g. Given subclass1|subclass2|false and base_class/base_interface, returns subclass1|subclass2 + * E.g. Given subclass1|mixed|false and base_class/base_interface, returns base_class/base_interface + */ + private static function calculateNarrowedUnionType(CodeBase $code_base, Context $context, UnionType $old_type, UnionType $asserted_object_type): UnionType + { + $new_type_set = []; + foreach ($old_type->getTypeSet() as $type) { + if ($type instanceof MixedType) { + // MixedType can cast to other types + return $asserted_object_type; + } + if (!$type->isObject()) { + // ignore non-object types + continue; + } + if (!$type->isObjectWithKnownFQSEN()) { + // Anything that can cast to $asserted_object_type should become $asserted_object_type + // TODO: Handle isPossiblyObject/iterable + return $asserted_object_type; + } + $type = $type->withIsNullable(false); + if (!$type->asPHPDocUnionType()->canCastToDeclaredType($code_base, $context, $asserted_object_type)) { + // This isn't on a common type hierarchy + continue; + } + if (!$type->asExpandedTypes($code_base)->canCastToUnionType($asserted_object_type)) { + // The variable includes a base class of the asserted type. + return $asserted_object_type; + } + $new_type_set[] = $type; + } + if (!$new_type_set) { + return $asserted_object_type; + } + if (!$asserted_object_type->hasRealTypeSet()) { + return UnionType::of($new_type_set, $old_type->getRealTypeSet()); + } + $new_real_type_set = []; + foreach ($old_type->getRealTypeSet() as $type) { + if ($type instanceof MixedType) { + // MixedType can cast to other types + return UnionType::of($new_type_set, $old_type->getRealTypeSet()); + } + if (!$type->isObject()) { + // ignore non-object types + continue; + } + if (!$type->isObjectWithKnownFQSEN()) { + // Anything that can cast to $asserted_object_type should become $asserted_object_type + // TODO: Handle isPossiblyObject/iterable + return UnionType::of($new_type_set, $old_type->getRealTypeSet()); + } + $type = $type->withIsNullable(false); + if (!$type->asExpandedTypes($code_base)->canCastToUnionType($asserted_object_type)) { + continue; + } + $new_real_type_set[] = $type; + } + return UnionType::of($new_type_set, $new_real_type_set ?: $asserted_object_type->getRealTypeSet()); + } + + /** + * @param Variable $variable (Node argument in a call to is_object) + */ + private static function analyzeIsObjectAssertion(Variable $variable): void + { + // Change the type to match is_object relationship + // If we already have the `object` type or generic object types, then keep those + // (E.g. T|false becomes T, T[]|iterable|null becomes Traversable, object|bool becomes object) + $variable->setUnionType($variable->getUnionType()->objectTypesStrict()); + } + + /** + * This function is called once, and returns closures to modify the types of variables. + * + * This contains Phan's logic for inferring the resulting union types of variables, e.g. in \is_array($x). + * + * @return array - The closures to call for a given global function + * @phan-return array + */ + private static function initTypeModifyingClosuresForVisitCall(): array + { + $make_direct_assertion_callback = static function (string $union_type_string): Closure { + $asserted_union_type = UnionType::fromFullyQualifiedRealString( + $union_type_string + ); + /** + * @param list $args + */ + return static function (CodeBase $unused_code_base, Context $unused_context, Variable $variable, array $args) use ($asserted_union_type): void { + // Otherwise, overwrite the type for any simple + // primitive types. + $variable->setUnionType($asserted_union_type); + }; + }; + + /** + * @param list $args + */ + $array_callback = static function (CodeBase $code_base, Context $context, Variable $variable, array $args): void { + // Change the type to match the is_array relationship + // If we already have generic array types, then keep those + // (E.g. T[]|false becomes T[], ?array|null becomes array, callable becomes callable-array) + $variable->setUnionType($variable->getUnionType()->arrayTypesStrictCast()); + }; + + /** + * @param list $args + */ + $object_callback = static function (CodeBase $unused_code_base, Context $unused_context, Variable $variable, array $args): void { + self::analyzeIsObjectAssertion($variable); + }; + /** + * @param list $args + */ + $is_a_callback = static function (CodeBase $code_base, Context $context, Variable $variable, array $args) use ($object_callback): void { + $real_class_name = $args[1] ?? null; + if ($real_class_name instanceof Node) { + $class_name = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $real_class_name)->asSingleScalarValueOrNull(); + } else { + $class_name = $real_class_name; + } + if (!\is_string($class_name)) { + // Limit the types of $variable to an object if we can't infer the class name. + $object_callback($code_base, $context, $variable, $args); + return; + } + try { + $fqsen = FullyQualifiedClassName::fromFullyQualifiedString($class_name); + } catch (FQSENException $_) { + throw new IssueException(Issue::fromType(Issue::TypeComparisonToInvalidClass)( + $context->getFile(), + $context->getLineNumberStart(), + [StringUtil::encodeValue($class_name)] + )); + } + // TODO: validate argument + $class_type = \is_string($real_class_name) ? $fqsen->asType()->asRealUnionType() : $fqsen->asType()->asPHPDocUnionType(); + $variable->setUnionType(self::calculateNarrowedUnionType($code_base, $context, $variable->getUnionType(), $class_type)); + }; + + /** + * @param string $extract_types + * @param UnionType $default_if_empty + * @return Closure(CodeBase,Context,Variable,array):void + */ + $make_callback = static function (string $extract_types, UnionType $default_if_empty, bool $allow_undefined = false): Closure { + $method = new ReflectionMethod(UnionType::class, $extract_types); + /** + * @param list $args + * @suppress PhanPluginUnknownObjectMethodCall can't analye ReflectionMethod + */ + return static function (CodeBase $code_base, Context $context, Variable $variable, array $args) use ($method, $default_if_empty, $allow_undefined): void { + // Change the type to match the is_a relationship + // If we already have possible callable types, then keep those + // (E.g. Closure|false becomes Closure) + $union_type = $variable->getUnionType(); + $new_type = $method->invoke($union_type); + if ($new_type->isEmpty()) { + // If there are no inferred types, or the only type we saw was 'null', + // assume there this can be any possible scalar. + // (Excludes `resource`, which is technically a scalar) + // + // FIXME move this to PostOrderAnalysisVisitor so that all expressions can be analyzed, not just variables? + $new_type = $default_if_empty; + } else { + // Add the missing type set before making the non-nullable clone. + // Otherwise, it'd have the real type set non-null-mixed. + if (!$new_type->hasRealTypeSet()) { + $new_type = $new_type->withRealTypeSet($default_if_empty->getRealTypeSet()); + } + $new_type = $new_type->nonNullableClone(); + if (!$allow_undefined) { + $new_type = $new_type->withIsPossiblyUndefined(false); + } + } + $variable->setUnionType($new_type); + }; + }; + + /** + * @param list $args + */ + $iterable_callback = static function (CodeBase $code_base, Context $context, Variable $variable, array $args): void { + // Change the type to match the is_iterable relationship + // If we already have generic array types or Traversable, then keep those + // (E.g. T[]|false becomes T[], ?array|null becomes array, callable becomes iterable, object becomes \Traversable) + $variable->setUnionType($variable->getUnionType()->withStaticResolvedInContext($context)->iterableTypesStrictCast($code_base)); + }; + /** + * @param list $args + */ + $countable_callback = static function (CodeBase $code_base, Context $context, Variable $variable, array $args): void { + // Change the type to match the is_countable relationship + // If we already have possible countable types, then keep those + // (E.g. ?ArrayObject|false becomes ArrayObject) + $variable->setUnionType($variable->getUnionType()->withStaticResolvedInContext($context)->countableTypesStrictCast($code_base)); + }; + /** + * @param list $args + */ + $has_count_callback = static function (CodeBase $code_base, Context $context, Variable $variable, array $args): void { + // Change the type to match the is_countable relationship + // If we already have possible countable types, then keep those + // (E.g. ?ArrayObject|false becomes ArrayObject) + $variable->setUnionType( + $variable->getUnionType() + ->withStaticResolvedInContext($context) + ->countableTypesStrictCast($code_base) + ->nonFalseyClone() + ); + }; + $class_exists_callback = $make_callback('classStringTypes', ClassStringType::instance(false)->asRealUnionType()); + $method_exists_callback = $make_callback('classStringOrObjectTypes', UnionType::fromFullyQualifiedRealString('class-string|object')); + /** @return void */ + $callable_callback = $make_callback('callableTypes', CallableType::instance(false)->asRealUnionType()); + $bool_callback = $make_callback('boolTypes', BoolType::instance(false)->asRealUnionType()); + $int_callback = $make_callback('intTypes', IntType::instance(false)->asRealUnionType()); + $string_callback = $make_callback('stringTypes', StringType::instance(false)->asRealUnionType()); + $numeric_callback = $make_callback('numericTypes', UnionType::fromFullyQualifiedRealString('string|int|float')); + $scalar_callback = $make_callback('scalarTypesStrict', UnionType::fromFullyQualifiedRealString('string|int|float|bool')); + + // Note: LiteralIntType exists, but LiteralFloatType doesn't, which is why these are different. + $float_callback = $make_direct_assertion_callback('float'); + $null_callback = $make_direct_assertion_callback('null'); + // Note: isset() is handled in visitIsset() + + return [ + 'class_exists' => $class_exists_callback, + 'count' => $has_count_callback, // handle `if (count($x))` but not yet `if (count($x) > 0)` + 'interface_exists' => $class_exists_callback, // Currently, there's just class-string, not trait-string or interface-string. + 'trait_exists' => $class_exists_callback, + 'method_exists' => $method_exists_callback, + 'is_a' => $is_a_callback, + 'is_array' => $array_callback, + 'is_bool' => $bool_callback, + 'is_callable' => $callable_callback, + 'is_countable' => $countable_callback, + 'is_double' => $float_callback, + 'is_float' => $float_callback, + 'is_int' => $int_callback, + 'is_integer' => $int_callback, + 'is_iterable' => $iterable_callback, // TODO: Could keep basic array types and classes extending iterable + 'is_long' => $int_callback, + 'is_null' => $null_callback, + 'is_numeric' => $numeric_callback, + 'is_object' => $object_callback, + 'is_real' => $float_callback, + 'is_resource' => $make_direct_assertion_callback('resource'), + 'is_scalar' => $scalar_callback, + 'is_string' => $string_callback, + ]; + } + + /** + * Look at elements of the form `is_array($v)` and modify + * the type of the variable. + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitCall(Node $node): Context + { + // Analyze the call to the node, in case it modifies any variables (e.g. count($x = new_value()), if (preg_match(..., $matches), etc. + // TODO: Limit this to nodes which actually contain variables or properties? + // TODO: Only call this if the caller is also a ConditionVisitor, since BlockAnalysisVisitor would call this for ternaries and if statements already. + // TODO: Also implement this for visitStaticCall, visitMethodCall, etc? + $this->context = (new BlockAnalysisVisitor($this->code_base, $this->context))->__invoke($node); + + $raw_function_name = self::getFunctionName($node); + if (!\is_string($raw_function_name)) { + return $this->context; + } + $args = $node->children['args']->children; + $first_arg = $args[0] ?? null; + + // Translate the function name into the UnionType it asserts + static $map = null; + + if ($map === null) { + $map = self::initTypeModifyingClosuresForVisitCall(); + } + // TODO: Check if the return value of the function is void/always truthy (e.g. object) + + switch (\strtolower($raw_function_name)) { + case 'array_key_exists': + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + return $this->analyzeArrayKeyExists($args); + case 'defined': + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + return $this->analyzeDefined($args); + } + + // Only look at things of the form + // `\is_string($variable)` + if (!($first_arg instanceof Node && $first_arg->kind === ast\AST_VAR)) { + $type_modification_callback = $map[\strtolower($raw_function_name)] ?? null; + if (!$type_modification_callback) { + if (Config::getValue('redundant_condition_detection')) { + $this->checkRedundantOrImpossibleTruthyCondition($node, $this->context, null, false); + } + return $this->context; + } + // @phan-suppress-next-line PhanPartialTypeMismatchArgument, PhanTypeMismatchArgumentNullable + return $this->modifyComplexExpression($first_arg, $type_modification_callback, $this->context, $args); + } + + $function_name = \strtolower($raw_function_name); + if (\count($args) !== 1) { + if (!(\count($args) === 2 && \in_array($function_name, ['is_a', 'class_exists', 'method_exists'], true))) { + if (Config::getValue('redundant_condition_detection')) { + $this->checkRedundantOrImpossibleTruthyCondition($node, $this->context, null, false); + } + return $this->context; + } + } + + $type_modification_callback = $map[$function_name] ?? null; + if ($type_modification_callback === null) { + if (Config::getValue('redundant_condition_detection')) { + $this->checkRedundantOrImpossibleTruthyCondition($node, $this->context, null, false); + } + return $this->context; + } + + $context = $this->context; + + try { + // Get the variable we're operating on + $variable = $this->getVariableFromScope($first_arg, $context); + + if (\is_null($variable)) { + return $context; + } + + // Make a copy of the variable + $variable = clone($variable); + + // Modify the types of that variable. + $type_modification_callback($this->code_base, $context, $variable, $args); + + // Overwrite the variable with its new type in this + // scope without overwriting other scopes + $context = $context->withScopeVariable( + $variable + ); + } catch (IssueException $exception) { + Issue::maybeEmitInstance($this->code_base, $context, $exception->getIssueInstance()); + } catch (\Exception $_) { + // Swallow it (E.g. IssueException for undefined variable) + } + + return $context; + } + + /** + * @param list $args + */ + private function analyzeArrayKeyExists(array $args): Context + { + if (\count($args) !== 2) { + return $this->context; + } + $var_node = $args[1]; + if (!($var_node instanceof Node)) { + return $this->context; + } + return $this->updateVariableWithConditionalFilter( + $var_node, + $this->context, + static function (UnionType $_): bool { + return true; + }, + function (UnionType $type) use ($args): UnionType { + if ($type->hasTopLevelArrayShapeTypeInstances()) { + $type = $this->withSetArrayShapeTypes($type, $args[0], $this->context, false); + } + return $this->asTypeSupportingAccess($type, self::ACCESS_ARRAY_KEY_EXISTS); + }, + true, + false + ); + } + + /** + * @param list $args + */ + private function analyzeDefined(array $args): Context + { + if (\count($args) !== 1) { + return $this->context; + } + $constant_name = $args[0]; + if ($constant_name instanceof Node) { + $constant_name = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $constant_name)->asSingleScalarValueOrNullOrSelf(); + } + if (!\is_string($constant_name)) { + return $this->context; + } + $context = $this->context->withClonedScope(); + $context->addScopeVariable(new Variable( + $context, + self::CONSTANT_EXISTS_PREFIX . \ltrim($constant_name, '\\'), + UnionType::empty(), + 0 + )); + return $context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitEmpty(Node $node): Context + { + $var_node = $node->children['expr']; + if (!($var_node instanceof Node)) { + return $this->context; + } + // Should always be a node for valid ASTs, tolerant-php-parser may produce invalid nodes + if (\in_array($var_node->kind, [ast\AST_VAR, ast\AST_PROP, ast\AST_DIM], true)) { + // Don't emit notices for if (empty($x)) {}, etc. We already do that in RedundantConditionPlugin. + return $this->removeTruthyFromVariable($var_node, $this->context, true, true); + } + $this->checkVariablesDefinedInIsset($var_node); + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitExprList(Node $node): Context + { + $children = $node->children; + $count = \count($children); + if ($count > 1) { + foreach ($children as $sub_node) { + --$count; + if ($count > 0 && $sub_node instanceof Node) { + $this->checkVariablesDefined($sub_node); + } + } + } + // Only analyze the last expression in the expression list for conditions. + $last_expression = \end($node->children); + if ($last_expression instanceof Node) { + return $this->__invoke($last_expression); + } else { + // Other code should warn about this invalid AST + return $this->context; + } + } + + /** + * Useful for analyzing `if ($x = foo() && $x->method())` + * TODO: Remove empty/false/null types from $x + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitAssign(Node $node): Context + { + $context = (new BlockAnalysisVisitor($this->code_base, $this->context))->visitAssign($node); + $left = $node->children['var']; + if (!($left instanceof Node)) { + // Other code should warn about this invalid AST + return $context; + } + if ($left->kind === ast\AST_ARRAY) { + $expr_node = $node->children['expr']; + if ($expr_node instanceof Node) { + return (new self($this->code_base, $context))->__invoke($expr_node); + } + return $context; + } + return (new self($this->code_base, $context))->__invoke($left); + } + + /** + * Useful for analyzing `if ($x = foo() && $x->method())` + * TODO: Remove empty/false/null types from $x + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitAssignRef(Node $node): Context + { + $context = (new BlockAnalysisVisitor($this->code_base, $this->context))->visitAssignRef($node); + $left = $node->children['var']; + if (!($left instanceof Node)) { + // TODO: Ensure this always warns + return $context; + } + return (new self($this->code_base, $context))->__invoke($left); + } + + /** + * Update the variable represented by $expression to have the type $type. + */ + public static function updateToHaveType(CodeBase $code_base, Context $context, Node $expression, UnionType $type): Context + { + $cv = new ConditionVisitor($code_base, $context); + return $cv->analyzeBinaryConditionPattern( + $expression, + 0, + new HasTypeCondition($type) + ); + } + + /** + * Update the variable represented by $expression to not have the type $type. + */ + public static function updateToNotHaveType(CodeBase $code_base, Context $context, Node $expression, UnionType $type): Context + { + $cv = new ConditionVisitor($code_base, $context); + return $cv->analyzeBinaryConditionPattern( + $expression, + 0, + new NotHasTypeCondition($type) + ); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/BinaryCondition.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/BinaryCondition.php new file mode 100644 index 000000000..cf3acd6e9 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/BinaryCondition.php @@ -0,0 +1,47 @@ +, >=) + */ +class ComparisonCondition implements BinaryCondition +{ + /** @var int the value of ast\Node->flags */ + private $flags; + + public function __construct(int $flags) + { + $this->flags = $flags; + } + + /** + * Assert that this condition applies to the variable $var (i.e. $var < $expr) + * + * @param Node $var + * @param Node|int|string|float $expr + * @override + */ + public function analyzeVar(ConditionVisitorInterface $visitor, Node $var, $expr): Context + { + return $visitor->updateVariableToBeCompared($var, $expr, $this->flags); + } + + /** + * Assert that this condition applies to the variable $object (i.e. get_class($object) === $expr) + * + * @param Node|int|string|float $object + * @param Node|int|string|float $expr + * @suppress PhanUnusedPublicMethodParameter + */ + public function analyzeClassCheck(ConditionVisitorInterface $visitor, $object, $expr): Context + { + return $visitor->getContext(); + } + + /** + * @suppress PhanUnusedPublicMethodParameter + */ + public function analyzeCall(ConditionVisitorInterface $visitor, Node $call_node, $expr): ?Context + { + return null; + } + + /** + * @suppress PhanUnusedPublicMethodParameter + */ + public function analyzeComplexCondition(ConditionVisitorInterface $visitor, Node $complex_node, $expr): ?Context + { + return null; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/EqualsCondition.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/EqualsCondition.php new file mode 100644 index 000000000..ef82e62b1 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/EqualsCondition.php @@ -0,0 +1,81 @@ +updateVariableToBeEqual($var, $expr); + } + + /** + * Assert that this condition applies to the variable $object (i.e. get_class($object) === $expr) + * + * @param Node|int|string|float $object + * @param Node|int|string|float $expr + */ + public function analyzeClassCheck(ConditionVisitorInterface $visitor, $object, $expr): Context + { + return $visitor->analyzeClassAssertion($object, $expr) ?? $visitor->getContext(); + } + + public function analyzeCall(ConditionVisitorInterface $visitor, Node $call_node, $expr): ?Context + { + $code_base = $visitor->getCodeBase(); + $context = $visitor->getContext(); + $expr_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr); + if ($expr_type->isEmpty()) { + return null; + } + // Skip check for `if is_bool`, allow weaker comparisons such as `is_string($x) == 1` + if (!$expr_type->isExclusivelyBoolTypes() && !UnionTypeVisitor::unionTypeFromNode($code_base, $context, $call_node)->isExclusivelyBoolTypes()) { + return null; + } + if (!$expr_type->containsFalsey()) { + // e.g. `if (is_string($x) === true)` + return (new ConditionVisitor($code_base, $context))->visitCall($call_node); + } elseif (!$expr_type->containsTruthy()) { + // e.g. `if (is_string($x) === false)` + return (new NegatedConditionVisitor($code_base, $context))->visitCall($call_node); + } + return null; + } + + public function analyzeComplexCondition(ConditionVisitorInterface $visitor, Node $complex_node, $expr): ?Context + { + $code_base = $visitor->getCodeBase(); + $context = $visitor->getContext(); + $expr_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr); + if (!$expr_type->isExclusivelyBoolTypes() && !UnionTypeVisitor::unionTypeFromNode($code_base, $context, $complex_node)->isExclusivelyBoolTypes()) { + return null; + } + if (!$expr_type->containsFalsey()) { + // e.g. `if (($x instanceof Xyz) == true)` + return (new ConditionVisitor($code_base, $context))->__invoke($complex_node); + } elseif (!$expr_type->containsTruthy()) { + // e.g. `if (($x instanceof Xyz) == false)` + return (new NegatedConditionVisitor($code_base, $context))->__invoke($complex_node); + } + return null; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/HasTypeCondition.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/HasTypeCondition.php new file mode 100644 index 000000000..a558316f0 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/HasTypeCondition.php @@ -0,0 +1,90 @@ +type = $type; + } + /** + * Assert that this condition applies to the variable $var (i.e. $var has type $union_type) + * + * @param Node $var + * @param Node|int|string|float $expr @unused-param + * @override + */ + public function analyzeVar(ConditionVisitorInterface $visitor, Node $var, $expr): Context + { + // Get the variable we're operating on + $context = $visitor->getContext(); + try { + $variable = $visitor->getVariableFromScope($var, $context); + } catch (\Exception $_) { + return $context; + } + if (\is_null($variable)) { + return $context; + } + + // Make a copy of the variable + $variable = clone($variable); + + $variable->setUnionType($this->type); + + // Overwrite the variable with its new type in this + // scope without overwriting other scopes + return $context->withScopeVariable( + $variable + ); + } + + /** + * Assert that this condition applies to the variable $object (i.e. get_class($object) === $expr) + * + * @param Node|int|string|float $object + * @param Node|int|string|float $expr @unused-param + */ + public function analyzeClassCheck(ConditionVisitorInterface $visitor, $object, $expr): Context + { + $class_string = $this->type->asSingleScalarValueOrNull(); + if ($class_string === null) { + return $visitor->getContext(); + } + return $visitor->analyzeClassAssertion($object, $class_string) ?? $visitor->getContext(); + } + + /** + * @suppress PhanUnusedPublicMethodParameter + */ + public function analyzeCall(ConditionVisitorInterface $visitor, Node $call_node, $expr): ?Context + { + return null; + } + + /** + * @unused-param $visitor + * @unused-param $node + * @unused-param $expr + * @override + */ + public function analyzeComplexCondition(ConditionVisitorInterface $visitor, Node $node, $expr): ?Context + { + // TODO: Could analyze get_class($array['field']) === stdClass::class (e.g. with AssignmentVisitor) + return null; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/IdenticalCondition.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/IdenticalCondition.php new file mode 100644 index 000000000..9be1fbd5f --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/IdenticalCondition.php @@ -0,0 +1,82 @@ +updateVariableToBeIdentical($var, $expr); + } + + /** + * Assert that this condition applies to the variable $object (i.e. get_class($object) === $expr) + * + * @param Node|int|string|float $object + * @param Node|int|string|float $expr + */ + public function analyzeClassCheck(ConditionVisitorInterface $visitor, $object, $expr): Context + { + return $visitor->analyzeClassAssertion($object, $expr) ?? $visitor->getContext(); + } + + public function analyzeCall(ConditionVisitorInterface $visitor, Node $call_node, $expr): ?Context + { + if (!$expr instanceof Node) { + // Cannot be false/true. + return null; + } + $code_base = $visitor->getCodeBase(); + $context = $visitor->getContext(); + $value = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr)->asSingleScalarValueOrNullOrSelf(); + if (!\is_bool($value)) { + return null; + } + if ($value) { + // e.g. `if (is_string($x) === true)` + return (new ConditionVisitor($code_base, $context))->visitCall($call_node); + } else { + // e.g. `if (is_string($x) === false)` + return (new NegatedConditionVisitor($code_base, $context))->visitCall($call_node); + } + } + + public function analyzeComplexCondition(ConditionVisitorInterface $visitor, Node $complex_node, $expr): ?Context + { + if (!$expr instanceof Node) { + return null; + } + $code_base = $visitor->getCodeBase(); + $context = $visitor->getContext(); + $value = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr)->asSingleScalarValueOrNullOrSelf(); + if (!\is_bool($value)) { + return null; + } + if ($value) { + // e.g. `if (is_string($x) === true)` + return (new ConditionVisitor($code_base, $context))->__invoke($complex_node); + } else { + // e.g. `if (is_string($x) === false)` + return (new NegatedConditionVisitor($code_base, $context))->__invoke($complex_node); + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/NotEqualsCondition.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/NotEqualsCondition.php new file mode 100644 index 000000000..54d7b086f --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/NotEqualsCondition.php @@ -0,0 +1,79 @@ +updateVariableToBeNotEqual($var, $expr); + } + + /** + * Assert that this condition applies to the variable $object (i.e. get_class($object) != $expr) + * + * @param Node|int|string|float $object + * @param Node|int|string|float $expr + * @override + * @suppress PhanUnusedPublicMethodParameter + */ + public function analyzeClassCheck(ConditionVisitorInterface $visitor, $object, $expr): Context + { + return $visitor->getContext(); + } + + public function analyzeCall(ConditionVisitorInterface $visitor, Node $call_node, $expr): ?Context + { + $code_base = $visitor->getCodeBase(); + $context = $visitor->getContext(); + $expr_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr); + if (!$expr_type->isExclusivelyBoolTypes() && !UnionTypeVisitor::unionTypeFromNode($code_base, $context, $call_node)->isExclusivelyBoolTypes()) { + return null; + } + if (!$expr_type->containsFalsey()) { + // e.g. `if (is_string($x) != true)` + return (new NegatedConditionVisitor($code_base, $context))->visitCall($call_node); + } elseif (!$expr_type->containsTruthy()) { + // e.g. `if (is_string($x) != false)` + return (new ConditionVisitor($code_base, $context))->visitCall($call_node); + } + return null; + } + + public function analyzeComplexCondition(ConditionVisitorInterface $visitor, Node $complex_node, $expr): ?Context + { + $code_base = $visitor->getCodeBase(); + $context = $visitor->getContext(); + $expr_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr); + if (!$expr_type->isExclusivelyBoolTypes() && !UnionTypeVisitor::unionTypeFromNode($code_base, $context, $complex_node)->isExclusivelyBoolTypes()) { + return null; + } + if (!$expr_type->containsFalsey()) { + // e.g. `if (($x instanceof Xyz) != true)` + return (new NegatedConditionVisitor($code_base, $context))->__invoke($complex_node); + } elseif (!$expr_type->containsTruthy()) { + // e.g. `if (($x instanceof Xyz) != false)` + return (new ConditionVisitor($code_base, $context))->__invoke($complex_node); + } + return null; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/NotHasTypeCondition.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/NotHasTypeCondition.php new file mode 100644 index 000000000..9d844dae4 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/NotHasTypeCondition.php @@ -0,0 +1,92 @@ +type = $type; + } + + /** + * Assert that this condition applies to the variable $var (i.e. $var does not have type $union_type) + * + * @param Node $var + * @param Node|int|string|float $expr @unused-param + * @override + */ + public function analyzeVar(ConditionVisitorInterface $visitor, Node $var, $expr): Context + { + // Get the variable we're operating on + $context = $visitor->getContext(); + try { + $variable = $visitor->getVariableFromScope($var, $context); + } catch (\Exception $_) { + return $context; + } + if (\is_null($variable)) { + return $context; + } + + // Make a copy of the variable + $variable = clone($variable); + $code_base = $visitor->getCodeBase(); + $result_type = ConditionVisitorUtil::excludeMatchingTypes($code_base, $variable->getUnionType(), $this->type); + + $variable->setUnionType($result_type); + + // Overwrite the variable with its new type in this + // scope without overwriting other scopes + return $context->withScopeVariable( + $variable + ); + } + + /** + * Assert that this condition applies to the variable $object. Unimplemented. + * + * @param Node|int|string|float $object @unused-param + * @param Node|int|string|float $expr @unused-param + */ + public function analyzeClassCheck(ConditionVisitorInterface $visitor, $object, $expr): Context + { + // Unimplemented, Not likely to be commonly used. + return $visitor->getContext(); + } + + /** + * @unused-param $visitor + * @unused-param $call_node + * @unused-param $expr + */ + public function analyzeCall(ConditionVisitorInterface $visitor, Node $call_node, $expr): ?Context + { + return null; + } + + /** + * @unused-param $visitor + * @unused-param $complex_node + * @unused-param $expr + */ + public function analyzeComplexCondition(ConditionVisitorInterface $visitor, Node $complex_node, $expr): ?Context + { + // TODO: Could analyze get_class($array['field']) === stdClass::class (e.g. with AssignmentVisitor) + return null; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/NotIdenticalCondition.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/NotIdenticalCondition.php new file mode 100644 index 000000000..5af6bd7b3 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitor/NotIdenticalCondition.php @@ -0,0 +1,82 @@ +updateVariableToBeNotIdentical($var, $expr); + } + + /** + * Assert that this condition applies to the variable $object (i.e. get_class($object) !== $expr) + * + * @param Node|int|string|float $object + * @param Node|int|string|float $expr + * @suppress PhanUnusedPublicMethodParameter + */ + public function analyzeClassCheck(ConditionVisitorInterface $visitor, $object, $expr): Context + { + return $visitor->getContext(); + } + + public function analyzeCall(ConditionVisitorInterface $visitor, Node $call_node, $expr): ?Context + { + if (!$expr instanceof Node) { + return null; + } + $code_base = $visitor->getCodeBase(); + $context = $visitor->getContext(); + $value = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr)->asSingleScalarValueOrNullOrSelf(); + if (!\is_bool($value)) { + return null; + } + if ($value) { + // e.g. `if (!(is_string($x) === true))` + return (new NegatedConditionVisitor($code_base, $context))->visitCall($call_node); + } else { + // e.g. `if (!(is_string($x) === false))` + return (new ConditionVisitor($code_base, $context))->visitCall($call_node); + } + } + + public function analyzeComplexCondition(ConditionVisitorInterface $visitor, Node $complex_node, $expr): ?Context + { + if (!$expr instanceof Node) { + return null; + } + $code_base = $visitor->getCodeBase(); + $context = $visitor->getContext(); + $value = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr)->asSingleScalarValueOrNullOrSelf(); + if (!\is_bool($value)) { + return null; + } + if ($value) { + // e.g. `if (($x instanceof Xyz) !== true)` + return (new NegatedConditionVisitor($code_base, $context))->__invoke($complex_node); + } else { + // e.g. `if (($x instanceof Xyz) !== false)` + return (new ConditionVisitor($code_base, $context))->__invoke($complex_node); + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitorInterface.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitorInterface.php new file mode 100644 index 000000000..01afe600d --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitorInterface.php @@ -0,0 +1,106 @@ + 0)` + */ + public function updateVariableToBeCompared( + Node $var_node, + $expr, + int $flags + ): Context; + + /** + * @param Node $var_node + * @param Node|int|float|string $expr + * @return Context - Context after inferring type from an expression such as `if ($x != 'literal')` + */ + public function updateVariableToBeNotEqual( + Node $var_node, + $expr, + Context $context = null + ): Context; + + /** + * Returns a context where the variable for $object_node has the class found in $expr_node + * + * @param Node|string|int|float $object_node + * @param Node|string|int|float|bool $expr_node + */ + public function analyzeClassAssertion($object_node, $expr_node): ?Context; + + /** + * @return ?Variable - Returns null if the variable is undeclared and ignore_undeclared_variables_in_global_scope applies. + * or if assertions won't be applied? + * @throws IssueException if variable is undeclared and not ignored. + * @see UnionTypeVisitor::visitVar() + * + * TODO: support assertions on superglobals, within the current file scope? + */ + public function getVariableFromScope(Node $var_node, Context $context): ?Variable; +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitorUtil.php b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitorUtil.php new file mode 100644 index 000000000..be2068229 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ConditionVisitorUtil.php @@ -0,0 +1,1668 @@ +updateVariableWithConditionalFilter( + $var_node, + $context, + /** + * @suppress PhanUndeclaredProperty did_check_redundant_condition + */ + function (UnionType $type) use ($var_node, $context, $suppress_issues): bool { + $contains_truthy = $type->containsTruthy(); + if (!$suppress_issues) { + if (Config::getValue('redundant_condition_detection') && $type->hasRealTypeSet()) { + // Here, we only perform the redundant condition checks on whichever ran first, to avoid warning about both impossible and redundant conditions + if (isset($var_node->did_check_redundant_condition)) { + return $contains_truthy; + } + $var_node->did_check_redundant_condition = true; + // Here, we only perform the redundant condition checks on the ConditionVisitor to avoid warning about both impossible and redundant conditions + // for the same expression + if ($contains_truthy) { + if (!$type->getRealUnionType()->containsFalsey()) { + RedundantCondition::emitInstance( + $var_node, + $this->code_base, + $context, + Issue::ImpossibleCondition, + [ + ASTReverter::toShortString($var_node), + $type->getRealUnionType(), + 'falsey', + ], + static function (UnionType $type): bool { + return !$type->containsFalsey(); + } + ); + } + } else { + if (!$type->getRealUnionType()->containsTruthy()) { + RedundantCondition::emitInstance( + $var_node, + $this->code_base, + $context, + Issue::RedundantCondition, + [ + ASTReverter::toShortString($var_node), + $type->getRealUnionType(), + 'falsey', + ], + static function (UnionType $type): bool { + return !$type->containsTruthy(); + } + ); + } + } + } + if (Config::getValue('error_prone_truthy_condition_detection')) { + $this->checkErrorProneTruthyCast($var_node, $context, $type); + } + } + return $contains_truthy; + }, + function (UnionType $union_type) use ($var_node, $context): UnionType { + $result = $union_type->nonTruthyClone(); + if ($result->isEmpty()) { + return $this->getFalseyTypesFallback($var_node, $context); + } + if (!$result->hasRealTypeSet()) { + return $result->withRealTypeSet($this->getFalseyTypesFallback($var_node, $context)->getRealTypeSet()); + } + return $result; + }, + $suppress_issues, + $check_empty + ); + } + + final protected function getFalseyTypesFallback(Node $var_node, Context $context): UnionType + { + static $default_empty; + if (\is_null($default_empty)) { + $default_empty = UnionType::fromFullyQualifiedRealString("?0|?''|?'0'|?0.0|?array{}|?false"); + } + $fallback_type = $this->getTypesFallback($var_node, $context); + if (!\is_object($fallback_type)) { + return $default_empty; + } + $new_fallback = $fallback_type->nonTruthyClone(); + if ($new_fallback->isEmpty()) { + return $default_empty; + } + return $new_fallback; + } + + final protected function getTypesFallback(Node $var_node, Context $context): ?UnionType + { + if ($var_node->kind !== ast\AST_VAR) { + return null; + } + $var_name = $var_node->children['name']; + if (!is_string($var_name)) { + return null; + } + if (!$context->getScope()->isInFunctionLikeScope()) { + return null; + } + if (!$context->isInLoop()) { + return null; + } + $function = $context->getFunctionLikeInScope($this->code_base); + $result = $function->getVariableTypeFallbackMap($this->code_base)[$var_name] ?? null; + if ($result && !$result->isEmpty()) { + return $result; + } + return null; + } + + // Remove any types which are definitely falsey from that variable (NullType, FalseType) + final protected function removeFalseyFromVariable(Node $var_node, Context $context, bool $suppress_issues): Context + { + return $this->updateVariableWithConditionalFilter( + $var_node, + $context, + function (UnionType $type) use ($context, $var_node, $suppress_issues): bool { + if (!$suppress_issues) { + if (Config::getValue('redundant_condition_detection') && $type->hasRealTypeSet()) { + $this->checkRedundantOrImpossibleTruthyCondition($var_node, $context, $type->getRealUnionType(), false); + } + if (Config::getValue('error_prone_truthy_condition_detection')) { + $this->checkErrorProneTruthyCast($var_node, $context, $type); + } + } + foreach ($type->getRealTypeSet() as $single_type) { + if ($single_type->isPossiblyFalsey()) { + return true; + } + } + return $type->containsFalsey() || !$type->hasRealTypeSet(); + }, + function (UnionType $type) use ($var_node, $context): UnionType { + // nonFalseyClone will always be non-empty because it returns non-empty-mixed + if ($type->containsTruthy()) { + return $type->nonFalseyClone(); + } + $fallback = $this->getTypesFallback($var_node, $context); + if (!$fallback) { + return $type->nonFalseyClone(); + } + return $fallback->nonFalseyClone(); + }, + $suppress_issues, + false + ); + } + + /** + * Warn about a scalar expression literal node that is always truthy or always falsey, in a place expecting a condition. + * @param int|string|float $node + */ + public function warnRedundantOrImpossibleScalar($node): void + { + // TODO: Add LiteralFloatType so that this can consistently warn about floats + $this->checkRedundantOrImpossibleTruthyCondition($node, $this->context, null, false); + } + + /** + * Check if the provided node has a comparison to truthy that's error prone. + * + * E.g. checking if an object|int is truthy - A more appropriate check may be is_object() + * + * @suppress PhanUndeclaredProperty did_check_redundant_condition + */ + private function checkErrorProneTruthyCast(Node $node, Context $context, UnionType $union_type): void + { + // Here, we only perform the redundant condition checks on whichever ran first, to avoid warning about both impossible and redundant conditions + if (isset($node->did_check_error_prone_truthy)) { + return; + } + $node->did_check_error_prone_truthy = true; + $has_array_or_object = false; + $has_falsey = false; + $has_truthy = false; + $has_string = false; + foreach ($union_type->getTypeSet() as $type) { + if ($type->isObject() || $type instanceof IterableType) { + $has_array_or_object = true; + continue; + } + if ($type->isPossiblyTruthy()) { + $has_truthy = true; + $has_falsey = $has_falsey || $type->withIsNullable(false)->isPossiblyFalsey(); + if (\get_class($type) === StringType::class) { + $has_string = true; + } + } else { + $has_falsey = $has_falsey || !($type instanceof NullType || $type instanceof FalseType); + } + } + if ($has_truthy && $has_falsey && $has_array_or_object) { + Issue::maybeEmit( + $this->code_base, + $context, + Issue::SuspiciousTruthyCondition, + $node->lineno, + ASTReverter::toShortString($node), + $union_type + ); + } + if ($has_string) { + Issue::maybeEmit( + $this->code_base, + $context, + Issue::SuspiciousTruthyString, + $node->lineno, + ASTReverter::toShortString($node), + $union_type + ); + } + } + + /** + * Check if the provided node has a redundant or impossible conditional. + * @param Node|string|int|float $node + * @suppress PhanUndeclaredProperty did_check_redundant_condition + */ + public function checkRedundantOrImpossibleTruthyCondition($node, Context $context, ?UnionType $type, bool $is_negated): void + { + if ($node instanceof Node) { + // Here, we only perform the redundant condition checks on whichever ran first, to avoid warning about both impossible and redundant conditions + if (isset($node->did_check_redundant_condition)) { + return; + } + $node->did_check_redundant_condition = true; + } elseif ($is_negated) { + return; + } + if (!$type) { + try { + $type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $node, false); + } catch (Exception $_) { + return; + } + if (!$type->hasRealTypeSet()) { + return; + } + $type = $type->getRealUnionType(); + } elseif ($type->isEmpty()) { + return; + } + // for the same expression + if (!$type->containsTruthy()) { + RedundantCondition::emitInstance( + $node, + $this->code_base, + $context, + $is_negated ? Issue::RedundantCondition : Issue::ImpossibleCondition, + [ + ASTReverter::toShortString($node), + $type->getRealUnionType(), + $is_negated ? 'falsey' : 'truthy' + ], + static function (UnionType $type): bool { + return !$type->containsTruthy(); + } + ); + } elseif (!$type->containsFalsey()) { + RedundantCondition::emitInstance( + $node, + $this->code_base, + $context, + $this->chooseIssueForUnconditionallyTrue($is_negated, $node), + [ + ASTReverter::toShortString($node), + $type->getRealUnionType(), + $is_negated ? 'falsey' : 'truthy' + ], + static function (UnionType $type): bool { + return !$type->containsFalsey(); + } + ); + } + } + + /** + * overridden in subclasses + * @param Node|mixed $node @unused-param + */ + protected function chooseIssueForUnconditionallyTrue(bool $is_negated, $node): string + { + return $is_negated ? Issue::ImpossibleCondition : Issue::RedundantCondition; + } + + final protected function removeNullFromVariable(Node $var_node, Context $context, bool $suppress_issues): Context + { + return $this->updateVariableWithConditionalFilter( + $var_node, + $context, + static function (UnionType $type): bool { + return $type->containsNullableOrUndefined(); + }, + function (UnionType $type) use ($var_node, $context): UnionType { + $result = $type->nonNullableClone()->withIsPossiblyUndefined(false); + if (!$result->isEmpty()) { + return $result; + } + $fallback = $this->getTypesFallback($var_node, $context); + if (!$fallback) { + return $result; + } + return $fallback->nonNullableClone(); + }, + $suppress_issues, + false + ); + } + + /** + * Returns the type after removing all types that are empty or don't support property or array access + * @param 1|2|3|4 $access_type ConditionVisitor::ACCESS_IS_* + */ + public static function asTypeSupportingAccess(UnionType $type, int $access_type): UnionType + { + $type = $type->asMappedListUnionType(/** @return list */ static function (Type $type) use ($access_type): array { + if ($access_type === ConditionVisitor::ACCESS_IS_OBJECT) { + if (!$type->isPossiblyObject()) { + return []; + } + } + if (!$type->isPossiblyTruthy()) { + // causes false positives when combining types + if ($type instanceof ArrayShapeType) { + // Convert array{} -> non-empty-array, null -> no types + // (useful guess with loops or references) + return UnionType::typeSetFromString('non-empty-array'); + } + return []; + } + if ($type instanceof ScalarType) { + if ($type instanceof StringType) { + if (\in_array($access_type, [ConditionVisitor::ACCESS_IS_OBJECT, ConditionVisitor::ACCESS_ARRAY_KEY_EXISTS, ConditionVisitor::ACCESS_STRING_DIM_SET], true)) { + return []; + } + if ($type instanceof LiteralStringType && $type->getValue() === '') { + // Can't access an offset of '' + return []; + } + return [$type->withIsNullable(false)]; + } + return []; + } + if ($type instanceof ResourceType) { + return []; + } + return [$type->asNonFalseyType()]; + }); + if (!$type->hasRealTypeSet()) { + return $type->withRealTypeSet(UnionType::typeSetFromString(ConditionVisitor::DEFAULTS_FOR_ACCESS_TYPE[$access_type])); + } + return $type; + } + + /** + * Remove empty types not supporting 0 or more levels of array/property access from the variable. + */ + final protected function removeTypesNotSupportingAccessFromVariable(Node $var_node, Context $context, int $access_type): Context + { + return $this->updateVariableWithConditionalFilter( + $var_node, + $context, + static function (UnionType $type) use ($access_type): bool { + return $type->hasPhpdocOrRealTypeMatchingCallback(static function (Type $type) use ($access_type): bool { + if ($type->isPossiblyFalsey()) { + return true; + } + if ($access_type === ConditionVisitor::ACCESS_IS_OBJECT) { + if (!$type->isPossiblyObject()) { + return true; + } + } + if ($type instanceof ResourceType) { + return true; + } + // TODO: Remove arrays if this is an access to a property + if ($type instanceof ScalarType) { + return !($type instanceof StringType); + } + return false; + }); + }, + static function (UnionType $type) use ($access_type): UnionType { + return self::asTypeSupportingAccess($type, $access_type); + }, + true, + false + ); + } + + /** + * @param int|string|float $value + */ + final protected function removeLiteralScalarFromVariable( + Node $var_node, + Context $context, + $value, + bool $strict_equality + ): Context { + if (!is_int($value) && !is_string($value)) { + return $context; + } + if ($strict_equality) { + if (is_int($value)) { + $cb = static function (Type $type) use ($value): bool { + return $type instanceof LiteralIntType && $type->getValue() === $value; + }; + } else { // string + $cb = static function (Type $type) use ($value): bool { + return $type instanceof LiteralStringType && $type->getValue() === $value; + }; + } + } else { + // Remove loosely equal types. + // TODO: Does this properly remove null for `$var != 0`? + $cb = static function (Type $type) use ($value): bool { + return $type instanceof LiteralTypeInterface && $type->getValue() == $value; + }; + } + return $this->updateVariableWithConditionalFilter( + $var_node, + $context, + static function (UnionType $union_type) use ($cb): bool { + return $union_type->hasPhpdocOrRealTypeMatchingCallback($cb); + }, + function (UnionType $union_type) use ($cb, $var_node, $context): UnionType { + $has_nullable = false; + foreach ($union_type->getTypeSet() as $type) { + if ($cb($type)) { + $union_type = $union_type->withoutType($type); + $has_nullable = $has_nullable || $type->isNullable(); + } + } + if ($has_nullable) { + if ($union_type->isEmpty()) { + return NullType::instance(false)->asPHPDocUnionType(); + } + return $union_type->nullableClone(); + } + if (!$union_type->isEmpty()) { + return $union_type; + } + + // repeat for the fallback + $fallback = $this->getTypesFallback($var_node, $context); + if (!$fallback) { + return $union_type; + } + foreach ($fallback->getTypeSet() as $type) { + if ($cb($type)) { + $fallback = $fallback->withoutType($type); + $has_nullable = $has_nullable || $type->isNullable(); + } + } + if ($has_nullable) { + if ($fallback->isEmpty()) { + return NullType::instance(false)->asPHPDocUnionType(); + } + return $fallback->nullableClone(); + } + return $fallback; + }, + false, + false + ); + } + + final protected function removeFalseFromVariable(Node $var_node, Context $context): Context + { + return $this->updateVariableWithConditionalFilter( + $var_node, + $context, + static function (UnionType $type): bool { + return $type->containsFalse(); + }, + function (UnionType $type) use ($var_node, $context): UnionType { + $result = $type->nonFalseClone(); + if (!$result->isEmpty()) { + return $result; + } + $fallback = $this->getTypesFallback($var_node, $context); + if (!$fallback) { + return $result; + } + return $fallback->nonFalseClone(); + }, + false, + false + ); + } + + final protected function removeTrueFromVariable(Node $var_node, Context $context): Context + { + return $this->updateVariableWithConditionalFilter( + $var_node, + $context, + static function (UnionType $type): bool { + return $type->containsTrue(); + }, + function (UnionType $type) use ($var_node, $context): UnionType { + $result = $type->nonTrueClone(); + if (!$result->isEmpty()) { + return $result; + } + $fallback = $this->getTypesFallback($var_node, $context); + if (!$fallback) { + return $result; + } + return $fallback->nonTrueClone(); + }, + false, + false + ); + } + + /** + * If the inferred UnionType makes $should_filter_cb return true + * (indicating there are Types to be removed from the UnionType or altered), + * then replace the UnionType with the modified UnionType which $filter_union_type_cb returns, + * and update the context. + * + * Note: It's expected that $should_filter_cb returns false on the new UnionType of that variable. + * + * @param Node $var_node a node of kind ast\AST_VAR, ast\AST_PROP, or ast\AST_DIM + * @param Closure(UnionType):bool $should_filter_cb + * @param Closure(UnionType):UnionType $filter_union_type_cb + */ + final protected function updateVariableWithConditionalFilter( + Node $var_node, + Context $context, + Closure $should_filter_cb, + Closure $filter_union_type_cb, + bool $suppress_issues, + bool $check_empty + ): Context { + try { + // Get the variable we're operating on + $variable = $this->getVariableFromScope($var_node, $context); + if (\is_null($variable)) { + if ($var_node->kind === ast\AST_DIM) { + return $this->updateDimExpressionWithConditionalFilter($var_node, $context, $should_filter_cb, $filter_union_type_cb, $suppress_issues, $check_empty); + } elseif ($var_node->kind === ast\AST_PROP) { + return $this->updatePropertyExpressionWithConditionalFilter($var_node, $context, $should_filter_cb, $filter_union_type_cb, $suppress_issues); + } + return $context; + } + + $union_type = $variable->getUnionType(); + if (!$should_filter_cb($union_type)) { + return $context; + } + + // Make a copy of the variable + $variable = clone($variable); + + $variable->setUnionType( + $filter_union_type_cb($union_type) + ); + + // Overwrite the variable with its new type in this + // scope without overwriting other scopes + $context = $context->withScopeVariable( + $variable + ); + } catch (IssueException $exception) { + if (!$suppress_issues) { + Issue::maybeEmitInstance($this->code_base, $context, $exception->getIssueInstance()); + } + } catch (\Exception $_) { + // Swallow it + } + return $context; + } + + /** + * @param Node $node a node of kind ast\AST_DIM + */ + final protected function updateDimExpressionWithConditionalFilter( + Node $node, + Context $context, + Closure $should_filter_cb, + Closure $filter_union_type_cb, + bool $suppress_issues, + bool $check_empty + ): Context { + $var_node = $node->children['expr']; + if (!($var_node instanceof Node)) { + return $context; + } + $var_name = self::getVarNameOfDimNode($var_node); + if (!is_string($var_name)) { + // TODO: Allow acting on properties + return $context; + } + if ($check_empty && $var_node->kind !== ast\AST_VAR) { + $var_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $var_node); + if (!($var_type->hasArrayShapeTypeInstances())) { + return $context; + } + } + try { + // Get the type of the field we're operating on, accounting for whether the field is possibly undefined + $old_field_type = (new UnionTypeVisitor($this->code_base, $context))->visitDim($node, true); + if (!$should_filter_cb($old_field_type)) { + return $context; + } + + // Give the field an unused stub name and compute the new type + $new_field_type = $filter_union_type_cb($old_field_type); + if ($old_field_type->isIdenticalTo($new_field_type)) { + return $context; + } + + return (new AssignmentVisitor( + $this->code_base, + // We clone the original context to avoid affecting the original context for the elseif. + // AssignmentVisitor modifies the provided context in place. + // + // There is a difference between `if (is_string($x['field']))` and `$x['field'] = remove_string_types($x['field'])` for the way the `elseif` should be analyzed. + $context->withClonedScope(), + $node, + $new_field_type + ))->__invoke($node); + } catch (IssueException $exception) { + if (!$suppress_issues) { + Issue::maybeEmitInstance($this->code_base, $context, $exception->getIssueInstance()); + } + } catch (\Exception $_) { + // Swallow it + } + return $context; + } + + /** + * Returns true if `$node` is an `ast\Node` representing the PHP variable `$this`. + * + * @param Node|string|int|float $node + */ + public static function isThisVarNode($node): bool + { + return $node instanceof Node && $node->kind === ast\AST_VAR && + $node->children['name'] === 'this'; + } + + /** + * Analyze an expression such as `assert(!is_int($this->prop_name))` + * and infer the effects on $this->prop_name in the local scope. + * + * @param Node $node a node of kind ast\AST_PROP + * @unused-param $suppress_issues + */ + final protected function updatePropertyExpressionWithConditionalFilter( + Node $node, + Context $context, + Closure $should_filter_cb, + Closure $filter_union_type_cb, + bool $suppress_issues + ): Context { + if (!self::isThisVarNode($node->children['expr'])) { + return $context; + } + $property_name = $node->children['prop']; + if (!is_string($property_name)) { + return $context; + } + return $this->modifyPropertyOfThisSimple( + $node, + static function (UnionType $type) use ($should_filter_cb, $filter_union_type_cb): UnionType { + if (!$should_filter_cb($type)) { + return $type; + } + return $filter_union_type_cb($type); + }, + $context + ); + } + + final protected function updateVariableWithNewType( + Node $var_node, + Context $context, + UnionType $new_union_type, + bool $suppress_issues, + bool $is_weak_type_assertion + ): Context { + if ($var_node->kind === ast\AST_PROP) { + return $this->modifyPropertySimple($var_node, function (UnionType $old_type) use ($new_union_type, $is_weak_type_assertion): UnionType { + if ($is_weak_type_assertion) { + return $this->combineTypesAfterWeakEqualityCheck($old_type, $new_union_type); + } else { + return $this->combineTypesAfterStrictEqualityCheck($old_type, $new_union_type); + } + }, $context); + } + // TODO: Support ast\AST_DIM + try { + // Get the variable we're operating on + $variable = $this->getVariableFromScope($var_node, $context); + if (\is_null($variable)) { + return $context; + } + + // Make a copy of the variable + $variable = clone($variable); + + if ($is_weak_type_assertion) { + $new_variable_type = $this->combineTypesAfterWeakEqualityCheck($variable->getUnionType(), $new_union_type); + } else { + $new_variable_type = $this->combineTypesAfterStrictEqualityCheck($variable->getUnionType(), $new_union_type); + } + $variable->setUnionType($new_variable_type); + + // Overwrite the variable with its new type in this + // scope without overwriting other scopes + $context = $context->withScopeVariable( + $variable + ); + } catch (IssueException $exception) { + if (!$suppress_issues) { + Issue::maybeEmitInstance($this->code_base, $context, $exception->getIssueInstance()); + } + } catch (\Exception $_) { + // Swallow it + } + return $context; + } + + protected function combineTypesAfterWeakEqualityCheck(UnionType $old_union_type, UnionType $new_union_type): UnionType + { + // TODO: Be more precise about these checks - e.g. forbid anything such as stdClass == false in the new type + if (!$old_union_type->hasRealTypeSet()) { + // This is a weak check of equality. We aren't sure of the real types + return $new_union_type->eraseRealTypeSet(); + } + if (!$new_union_type->hasRealTypeSet()) { + return $new_union_type->withRealTypeSet($old_union_type->getRealTypeSet()); + } + $new_real_union_type = $new_union_type->getRealUnionType(); + $combined_real_types = []; + foreach ($old_union_type->getRealTypeSet() as $type) { + if (!$type->asPHPDocUnionType()->hasAnyWeakTypeOverlap($new_real_union_type)) { + continue; + } + // @phan-suppress-next-line PhanAccessMethodInternal + // TODO: Implement Type->canWeakCastToUnionType? + if ($type->isPossiblyFalsey() && !$new_real_union_type->containsFalsey()) { + if ($type->isAlwaysFalsey()) { + continue; + } + // e.g. if asserting ?stdClass == true, then remove null + $type = $type->asNonFalseyType(); + } elseif ($type->isPossiblyTruthy() && !$new_real_union_type->containsTruthy()) { + if ($type->isAlwaysTruthy()) { + continue; + } + // e.g. if asserting ?stdClass == false, then remove stdClass and leave null + $type = $type->asNonTruthyType(); + } + if ($type instanceof LiteralTypeInterface) { + // If this is a literal type, we only want types that could possibly be loosely equal to this + foreach ($new_real_union_type->getTypeSet() as $other_type) { + if (!$other_type instanceof LiteralTypeInterface || $type->getValue() == $other_type->getValue()) { + $combined_real_types[] = $type; + continue 2; + } + } + continue; + } + $combined_real_types[] = $type; + } + if ($combined_real_types) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument TODO: Remove when intersection types are supported. + return $new_union_type->withRealTypeSet($combined_real_types); + } + return $new_union_type; + } + + protected function combineTypesAfterStrictEqualityCheck(UnionType $old_union_type, UnionType $new_union_type): UnionType + { + // TODO: Be more precise about these checks - e.g. forbid anything such as stdClass == false in the new type + if (!$new_union_type->hasRealTypeSet()) { + return $new_union_type->withRealTypeSet($old_union_type->getRealTypeSet()); + } + $new_real_union_type = $new_union_type->getRealUnionType(); + $combined_real_types = []; + foreach ($old_union_type->getRealTypeSet() as $type) { + if ($type->isPossiblyFalsey() && !$new_real_union_type->containsFalsey()) { + if ($type->isAlwaysFalsey()) { + continue; + } + // e.g. if asserting ?stdClass == true, then remove null + $type = $type->asNonFalseyType(); + } elseif ($type->isPossiblyTruthy() && !$new_real_union_type->containsTruthy()) { + if ($type->isAlwaysTruthy()) { + continue; + } + // e.g. if asserting ?stdClass == false, then remove stdClass and leave null + $type = $type->asNonTruthyType(); + } + if (!$type->asPHPDocUnionType()->canCastToDeclaredType($this->code_base, $this->context, $new_real_union_type)) { + continue; + } + $combined_real_types[] = $type; + } + if ($combined_real_types) { + return $new_union_type->withRealTypeSet($combined_real_types); + } + return $new_union_type; + } + + /** + * @param Node $var_node + * @param Node|int|float|string $expr + * @return Context - Context after inferring type from an expression such as `if ($x === 'literal')` + */ + final public function updateVariableToBeIdentical( + Node $var_node, + $expr, + Context $context = null + ): Context { + $context = $context ?? $this->context; + try { + $expr_type = UnionTypeVisitor::unionTypeFromLiteralOrConstant($this->code_base, $context, $expr); + if (!$expr_type) { + return $context; + } + } catch (\Exception $_) { + return $context; + } + return $this->updateVariableWithNewType($var_node, $context, $expr_type, true, false); + } + + /** + * @param Node $var_node + * @param Node|int|float|string $expr + * @return Context - Context after inferring type from an expression such as `if ($x == true)` + */ + final public function updateVariableToBeEqual( + Node $var_node, + $expr, + Context $context = null + ): Context { + $context = $context ?? $this->context; + try { + $expr_type = UnionTypeVisitor::unionTypeFromLiteralOrConstant($this->code_base, $context, $expr); + if (!$expr_type) { + return $context; + } + } catch (\Exception $_) { + return $context; + } + return $this->updateVariableWithNewType($var_node, $context, $expr_type, true, true); + } + + /** + * @param Node $var_node + * @param Node|int|float|string $expr + * @param int $flags (e.g. \ast\flags\BINARY_IS_SMALLER) + * @return Context - Context after inferring type from a comparison expression involving a variable such as `if ($x > 0)` + */ + final public function updateVariableToBeCompared( + Node $var_node, + $expr, + int $flags + ): Context { + $context = $this->context; + $var_name = $var_node->children['name'] ?? null; + // Don't analyze variables such as $$a + if (\is_string($var_name)) { + try { + $expr_type = UnionTypeVisitor::unionTypeFromLiteralOrConstant($this->code_base, $context, $expr); + if (!$expr_type) { + return $context; + } + $expr_value = $expr_type->asSingleScalarValueOrNullOrSelf(); + if (\is_object($expr_value)) { + return $context; + } + // Get the variable we're operating on + $variable = $this->getVariableFromScope($var_node, $context); + if (\is_null($variable)) { + return $context; + } + + // Make a copy of the variable + $variable = clone($variable); + + // TODO: Filter out nullable types + $union_type = $variable->getUnionType()->makeFromFilter(static function (Type $type) use ($expr_value, $flags): bool { + // @phan-suppress-next-line PhanAccessMethodInternal + return $type->canSatisfyComparison($expr_value, $flags); + }); + if ($union_type->containsNullable()) { + // @phan-suppress-next-line PhanAccessMethodInternal + if (!Type::performComparison(null, $expr_value, $flags)) { + // E.g. $x > 0 will remove the type null. + $union_type = $union_type->nonNullableClone(); + } + } + if ($union_type->hasPhpdocOrRealTypeMatchingCallback(static function (Type $type): bool { + return \get_class($type) === IntType::class; + })) { + // @phan-suppress-next-line PhanAccessMethodInternal + if (!Type::performComparison(0, $expr_value, $flags)) { + // E.g. $x > 0 will convert int to non-zero-int + $union_type = $union_type->asMappedUnionType(static function (Type $type): Type { + if (\get_class($type) === IntType::class) { + return NonZeroIntType::instance($type->isNullable()); + } + return $type; + }); + } + } + $variable->setUnionType($union_type); + + // Overwrite the variable with its new type in this + // scope without overwriting other scopes + $context = $context->withScopeVariable( + $variable + ); + return $context; + } catch (\Exception $_) { + // Swallow it (E.g. IssueException for undefined variable) + } + } + return $context; + } + + /** + * @param Node $var_node a node of type ast\AST_VAR, ast\AST_DIM (planned), or ast\AST_PROP + * @param Node|int|float|string $expr + * @return Context - Context after inferring type from an expression such as `if ($x !== 'literal')` + */ + final public function updateVariableToBeNotIdentical( + Node $var_node, + $expr, + Context $context = null + ): Context { + $context = $context ?? $this->context; + try { + if ($expr instanceof Node) { + $value = (new ContextNode($this->code_base, $context, $expr))->getEquivalentPHPValueForControlFlowAnalysis(); + if ($value instanceof Node) { + return $context; + } + if (\is_int($value) || \is_string($value)) { + return $this->removeLiteralScalarFromVariable($var_node, $context, $value, true); + } + if ($value === false) { + return $this->removeFalseFromVariable($var_node, $context); + } elseif ($value === true) { + return $this->removeTrueFromVariable($var_node, $context); + } elseif ($value === null) { + return $this->removeNullFromVariable($var_node, $context, false); + } + } else { + return $this->removeLiteralScalarFromVariable($var_node, $context, $expr, true); + } + } catch (\Exception $_) { + // Swallow it (E.g. IssueException for undefined variable) + } + return $context; + } + + /** + * @param Node $var_node + * @param Node|int|float|string $expr + * @return Context - Context after inferring type from an expression such as `if ($x != 'literal')` + * @suppress PhanSuspiciousTruthyCondition, PhanSuspiciousTruthyString didn't implement special handling of `if ($x != [...])` + */ + final public function updateVariableToBeNotEqual( + Node $var_node, + $expr, + Context $context = null + ): Context { + $context = $context ?? $this->context; + + $var_name = $var_node->children['name'] ?? null; + // http://php.net/manual/en/types.comparisons.php#types.comparisions-loose @phan-suppress-current-line PhanPluginPossibleTypoComment, UnusedSuppression + if (\is_string($var_name)) { + try { + if ($expr instanceof Node) { + $expr = (new ContextNode($this->code_base, $context, $expr))->getEquivalentPHPValueForControlFlowAnalysis(); + if ($expr instanceof Node) { + return $context; + } + if ($expr === false || $expr === null) { + return $this->removeFalseyFromVariable($var_node, $context, false); + } elseif ($expr === true) { + return $this->removeTrueFromVariable($var_node, $context); + } + } + // Remove all of the types which are loosely equal + if (is_int($expr) || is_string($expr)) { + $context = $this->removeLiteralScalarFromVariable($var_node, $context, $expr, false); + } + + if ($expr == false) { + // @phan-suppress-next-line PhanImpossibleCondition, PhanSuspiciousValueComparison FIXME should not set real type for loose equality checks + if ($expr == null) { + return $this->removeFalseyFromVariable($var_node, $context, false); + } + return $this->removeFalseFromVariable($var_node, $context); + } elseif ($expr == true) { // e.g. 1, "1", -1 + return $this->removeTrueFromVariable($var_node, $context); + } + } catch (\Exception $_) { + // Swallow it (E.g. IssueException for undefined variable) + } + } + return $context; + } + + /** + * @param Node|int|float|string $left + * @param Node|int|float|string $right + * @return Context - Context after inferring type from the negation of a condition such as `if ($x !== false)` + */ + public function analyzeAndUpdateToBeIdentical($left, $right): Context + { + return $this->analyzeBinaryConditionPattern( + $left, + $right, + new IdenticalCondition() + ); + } + + /** + * @param Node|int|float|string $left + * @param Node|int|float|string $right + * @return Context - Context after inferring type from the negation of a condition such as `if ($x != false)` + */ + public function analyzeAndUpdateToBeEqual($left, $right): Context + { + return $this->analyzeBinaryConditionPattern( + $left, + $right, + new EqualsCondition() + ); + } + + /** + * @param Node|int|float|string $left + * @param Node|int|float|string $right + * @return Context - Context after inferring type from an expression such as `if ($x !== false)` + */ + public function analyzeAndUpdateToBeNotIdentical($left, $right): Context + { + return $this->analyzeBinaryConditionPattern( + $left, + $right, + new NotIdenticalCondition() + ); + } + + /** + * @param Node|int|float|string $left + * @param Node|int|float|string $right + * @param BinaryCondition $condition + */ + protected function analyzeBinaryConditionPattern($left, $right, BinaryCondition $condition): Context + { + if ($left instanceof Node) { + $result = $this->analyzeBinaryConditionSide($left, $right, $condition); + if ($result !== null) { + return $result; + } + } + if ($right instanceof Node) { + $result = $this->analyzeBinaryConditionSide($right, $left, $condition); + if ($result !== null) { + return $result; + } + } + return $this->context; + } + + /** + * @param Node $var_node + * @param Node|int|string|float $expr_node + * @param BinaryCondition $condition + * @suppress PhanPartialTypeMismatchArgument + */ + private function analyzeBinaryConditionSide(Node $var_node, $expr_node, BinaryCondition $condition): ?Context + { + '@phan-var ConditionVisitorUtil|ConditionVisitorInterface $this'; + $kind = $var_node->kind; + if ($kind === ast\AST_VAR || $kind === ast\AST_DIM) { + return $condition->analyzeVar($this, $var_node, $expr_node); + } + if ($kind === ast\AST_PROP) { + if (self::isThisVarNode($var_node->children['expr']) && is_string($var_node->children['prop'])) { + return $condition->analyzeVar($this, $var_node, $expr_node); + } + return null; + } + if ($kind === ast\AST_CALL) { + $name = $var_node->children['expr']->children['name'] ?? null; + if (\is_string($name)) { + $name = \strtolower($name); + if ($name === 'get_class') { + $arg = $var_node->children['args']->children[0] ?? null; + if (!\is_null($arg)) { + return $condition->analyzeClassCheck($this, $arg, $expr_node); + } + } + return $condition->analyzeCall($this, $var_node, $expr_node); + } + } + $tmp = $var_node; + while (\in_array($kind, [ast\AST_ASSIGN, ast\AST_ASSIGN_OP, ast\AST_ASSIGN_REF], true)) { + $var = $tmp->children['var'] ?? null; + if (!$var instanceof Node) { + break; + } + $kind = $var->kind; + if ($kind === ast\AST_VAR) { + // @phan-suppress-next-line PhanPossiblyUndeclaredProperty + $this->context = (new BlockAnalysisVisitor($this->code_base, $this->context))->__invoke($tmp); + return $condition->analyzeVar($this, $var, $expr_node); + } + $tmp = $var; + } + // analyze `if (($a = $b) == true)` (etc.) but not `if ((list($x) = expr) == true)` + // The latter is really a check on expr, not on an array. + if (($tmp === $var_node || $tmp->kind !== ast\AST_ARRAY) && + ParseVisitor::isConstExpr($expr_node)) { + return $condition->analyzeComplexCondition($this, $tmp, $expr_node); + } + return null; + } + + /** + * Returns a context where the variable for $object_node has the class found in $expr_node + * + * @param Node|string|int|float $object_node + * @param Node|string|int|float|bool $expr_node + */ + public function analyzeClassAssertion($object_node, $expr_node): ?Context + { + if (!($object_node instanceof Node)) { + return null; + } + if ($expr_node instanceof Node) { + $expr_value = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr_node)->asSingleScalarValueOrNull(); + } else { + $expr_value = $expr_node; + } + if (!is_string($expr_value)) { + $expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr_node); + if (!$expr_type->canCastToUnionType(UnionType::fromFullyQualifiedPHPDocString('string|false'))) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeComparisonToInvalidClassType, + $this->context->getLineNumberStart(), + $expr_type, + 'false|string' + ); + } + // TODO: Could warn about invalid assertions + return null; + } + $fqsen_string = '\\' . $expr_value; + try { + $fqsen = FullyQualifiedClassName::fromFullyQualifiedString($fqsen_string); + } catch (FQSENException $_) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeComparisonToInvalidClass, + $this->context->getLineNumberStart(), + StringUtil::encodeValue($expr_value) + ); + + return null; + } + $expr_type = \is_string($expr_node) ? $fqsen->asType()->asRealUnionType() : $fqsen->asType()->asPHPDocUnionType(); + + $var_name = $object_node->children['name'] ?? null; + // Don't analyze variables such as $$a + if (!\is_string($var_name)) { + return null; + } + try { + // Get the variable we're operating on + $variable = $this->getVariableFromScope($object_node, $this->context); + if (\is_null($variable)) { + return null; + } + // Make a copy of the variable + $variable = clone($variable); + + $variable->setUnionType($expr_type); + + // Overwrite the variable with its new type in this + // scope without overwriting other scopes + return $this->context->withScopeVariable( + $variable + ); + } catch (\Exception $_) { + // Swallow it (E.g. IssueException for undefined variable) + } + return null; + } + + /** + * @param Node|int|float|string $left + * @param Node|int|float|string $right + * @return Context - Context after inferring type from an expression such as `if ($x == 'literal')` + */ + public function analyzeAndUpdateToBeNotEqual($left, $right): Context + { + return $this->analyzeBinaryConditionPattern( + $left, + $right, + new NotEqualsCondition() + ); + } + + /** + * @param Node|int|float|string $left + * @param Node|int|float|string $right + * @return Context - Context after inferring type from a comparison expression such as `if ($x['field'] > 0)` + */ + protected function analyzeAndUpdateToBeCompared($left, $right, int $flags): Context + { + return $this->analyzeBinaryConditionPattern( + $left, + $right, + new ComparisonCondition($flags) + ); + } + + + /** + * @return ?Variable - Returns null if the variable is undeclared and ignore_undeclared_variables_in_global_scope applies. + * or if assertions won't be applied? + * @throws IssueException if variable is undeclared and not ignored. + * @see UnionTypeVisitor::visitVar() + */ + final public function getVariableFromScope(Node $var_node, Context $context): ?Variable + { + if ($var_node->kind !== ast\AST_VAR) { + return null; + } + $var_name_node = $var_node->children['name'] ?? null; + + if ($var_name_node instanceof Node) { + // This is nonsense. Give up, but check if it's a type other than int/string. + // (e.g. to catch typos such as $$this->foo = bar;) + $name_node_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $var_name_node, true); + static $int_or_string_type; + if ($int_or_string_type === null) { + $int_or_string_type = UnionType::fromFullyQualifiedPHPDocString('?int|?string'); + } + if (!$name_node_type->canCastToUnionType($int_or_string_type)) { + Issue::maybeEmit($this->code_base, $context, Issue::TypeSuspiciousIndirectVariable, $var_name_node->lineno ?? 0, (string)$name_node_type); + } + + return null; + } + + $variable_name = (string)$var_name_node; + + if (!$context->getScope()->hasVariableWithName($variable_name)) { + // FIXME other uses were not sound for $argv outside of global scope. + $is_in_global_scope = $context->isInGlobalScope(); + $new_type = Variable::getUnionTypeOfHardcodedVariableInScopeWithName($variable_name, $is_in_global_scope); + if ($new_type) { + $variable = new Variable( + $context->withLineNumberStart($var_node->lineno), + $variable_name, + $new_type, + 0 + ); + $context->addScopeVariable($variable); + return $variable; + } + if (!($var_node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF)) { + if ($is_in_global_scope) { + if (!Config::getValue('ignore_undeclared_variables_in_global_scope')) { + Issue::maybeEmitWithParameters( + $this->code_base, + $context, + Variable::chooseIssueForUndeclaredVariable($context, $variable_name), + $var_node->lineno, + [$variable_name], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $context, $variable_name) + ); + } + } else { + throw new IssueException( + Issue::fromType(Variable::chooseIssueForUndeclaredVariable($context, $variable_name))( + $context->getFile(), + $var_node->lineno, + [$variable_name], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $context, $variable_name) + ) + ); + } + } + $variable = new Variable( + $context, + $variable_name, + UnionType::empty(), + 0 + ); + $context->addScopeVariable($variable); + return $variable; + } + return $context->getScope()->getVariableByName( + $variable_name + ); + } + + /** + * Fetches the function name. Does not check for function uses or namespaces. + * @param Node $node a node of kind ast\AST_CALL + * @return ?string (null if function name could not be found) + */ + final public static function getFunctionName(Node $node): ?string + { + $expr = $node->children['expr']; + if (!($expr instanceof Node)) { + return null; + } + $raw_function_name = $expr->children['name'] ?? null; + if (!\is_string($raw_function_name)) { + return null; + } + return $raw_function_name; + } + + /** + * Generate a union type by excluding matching types in $excluded_type from $affected_type + */ + public static function excludeMatchingTypes(CodeBase $code_base, UnionType $affected_type, UnionType $excluded_type): UnionType + { + if ($affected_type->isEmpty() || $excluded_type->isEmpty()) { + return $affected_type; + } + + foreach ($excluded_type->getTypeSet() as $type) { + if ($type instanceof NullType) { + $affected_type = $affected_type->nonNullableClone(); + } elseif ($type instanceof FalseType) { + $affected_type = $affected_type->nonFalseClone(); + } elseif ($type instanceof TrueType) { + $affected_type = $affected_type->nonTrueClone(); + } else { + continue; + } + // TODO: Do a better job handling LiteralStringType and LiteralIntType + $excluded_type = $excluded_type->withoutType($type); + } + if ($excluded_type->isEmpty()) { + return $affected_type; + } + return $affected_type->makeFromFilter(static function (Type $type) use ($code_base, $excluded_type): bool { + return $type instanceof MixedType || !$type->asExpandedTypes($code_base)->canCastToUnionType($excluded_type); + }); + } + + /** + * Returns this ConditionVisitorUtil's CodeBase. + * This is needed by subclasses of BinaryCondition. + */ + public function getCodeBase(): CodeBase + { + return $this->code_base; + } + + /** + * Returns this ConditionVisitorUtil's Context. + * This is needed by subclasses of BinaryCondition. + */ + public function getContext(): Context + { + return $this->context; + } + + /** + * @param Node|string|int|float $node + * @param Closure(CodeBase,Context,Variable,list):void $type_modification_callback + * A closure acting on a Variable instance (usually not really a variable) to modify its type + * @param Context $context + * @param list $args + */ + protected function modifyComplexExpression($node, Closure $type_modification_callback, Context $context, array $args): Context + { + for (;;) { + if (!$node instanceof Node) { + return $context; + } + switch ($node->kind) { + case ast\AST_DIM: + return $this->modifyComplexDimExpression($node, $type_modification_callback, $context, $args); + case ast\AST_PROP: + if (self::isThisVarNode($node->children['expr'])) { + return $this->modifyPropertyOfThis($node, $type_modification_callback, $context, $args); + } + return $context; + case ast\AST_ASSIGN: + case ast\AST_ASSIGN_REF: + $var_node = $node->children['var']; + if (!$var_node instanceof Node) { + return $context; + } + // Act on the left (or right) hand side of the assignment instead. That side may be a regular variable. + if ($var_node->kind === ast\AST_ARRAY) { + $node = $node->children['expr']; + } else { + $node = $var_node; + } + continue 2; + case ast\AST_VAR: + $variable = $this->getVariableFromScope($node, $context); + if (\is_null($variable)) { + return $context; + } + // Make a copy of the variable + $variable = clone($variable); + $type_modification_callback($this->code_base, $context, $variable, $args); + // Overwrite the variable with its new type + return $context->withScopeVariable( + $variable + ); + case ast\AST_ASSIGN_OP: + // Be conservative - analyze `cond(++$x)` but not `cond($x++) + // case ast\AST_POST_INC: + // case ast\AST_POST_DEC: + case ast\AST_PRE_INC: + case ast\AST_PRE_DEC: + $node = $node->children['var']; + if (!$node instanceof Node) { + return $context; + } + continue 2; + default: + return $context; + } + } + } + + /** + * @param Node $node a node of kind ast\AST_DIM (e.g. the argument of is_array($x['field'])) + * @param Closure(CodeBase,Context,Variable,list):void $type_modification_callback + * A closure acting on a Variable instance (not really a variable) to modify its type + * + * This is a function such as is_array, is_null (questionable), etc. + * @param Context $context + * @param list $args + */ + protected function modifyComplexDimExpression(Node $node, Closure $type_modification_callback, Context $context, array $args): Context + { + $var_name = $this->getVarNameOfDimNode($node->children['expr']); + if (!is_string($var_name)) { + return $context; + } + // Give the field an unused stub name and compute the new type + $old_field_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $node); + $field_variable = new Variable($context, "__phan", $old_field_type, 0); + $type_modification_callback($this->code_base, $context, $field_variable, $args); + $new_field_type = $field_variable->getUnionType(); + if ($new_field_type->isIdenticalTo($old_field_type)) { + return $context; + } + // Treat if (is_array($x['field'])) similarly to `$x['field'] = some_function_returning_array() + // (But preserve anything known about array types of $x['field']) + return (new AssignmentVisitor( + $this->code_base, + // We clone the original context to avoid affecting the original context for the elseif. + // AssignmentVisitor modifies the provided context in place. + // + // There is a difference between `if (is_string($x['field']))` and `$x['field'] = (some string)` for the way the `elseif` should be analyzed. + $context->withClonedScope(), + $node, + $new_field_type + ))->__invoke($node); + } + + /** + * Return a context with overrides for the type of a property in the local scope, + * caused by a function accepting $args. + * + * @param Node $node a node of kind ast\AST_PROP (e.g. the argument of is_array($this->prop_name)) + * @param Closure(CodeBase,Context,Variable,list):void $type_modification_callback + * A closure acting on a Variable instance (not really a variable) to modify its type + * + * This is a function such as is_array, is_null, etc. + * @param Context $context + * @param list $args + */ + protected function modifyPropertyOfThis(Node $node, Closure $type_modification_callback, Context $context, array $args): Context + { + $property_name = $node->children['prop']; + if (!is_string($property_name)) { + return $context; + } + // Give the property a type and compute the new type + $old_property_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $node); + $property_variable = new Variable($context, "__phan", $old_property_type, 0); + $type_modification_callback($this->code_base, $context, $property_variable, $args); + $new_property_type = $property_variable->getUnionType(); + if ($new_property_type->isIdenticalTo($old_property_type)) { + return $context; + } + return $context->withThisPropertySetToTypeByName($property_name, $new_property_type); + } + + /** + * Return a context with overrides for the type of a property of $this in the local scope. + * + * @param Node $node a node of kind ast\AST_PROP (e.g. the argument of is_array($this->prop_name)) + * @param Closure(UnionType):UnionType $type_mapping_callback + * Given a union type, returns the resulting union type. + * @param Context $context + */ + protected function modifyPropertyOfThisSimple(Node $node, Closure $type_mapping_callback, Context $context): Context + { + $property_name = $node->children['prop']; + if (!is_string($property_name)) { + return $context; + } + // Give the property a type and compute the new type + $old_property_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $node); + $new_property_type = $type_mapping_callback($old_property_type); + if ($new_property_type->isIdenticalTo($old_property_type)) { + // This didn't change anything + return $context; + } + return $context->withThisPropertySetToTypeByName($property_name, $new_property_type); + } + + /** + * @param Node $node a node of kind ast\AST_PROP (e.g. the argument of is_array($this->prop_name)) + * This is a no-op of the expression is not $this. + * @param Closure(UnionType):UnionType $type_mapping_callback + * Given a union type, returns the resulting union type. + * @param Context $context + */ + protected function modifyPropertySimple(Node $node, Closure $type_mapping_callback, Context $context): Context + { + if (!self::isThisVarNode($node->children['expr'])) { + return $context; + } + return self::modifyPropertyOfThisSimple($node, $type_mapping_callback, $context); + } + + /** + * @param Node|string|int|float $node + * @return ?string the name of the variable in a chain of field accesses such as $varName['field'][$i] + */ + private static function getVarNameOfDimNode($node): ?string + { + // Loop to support getting the var name in is_array($x['field'][0]) + while (true) { + if (!($node instanceof Node)) { + return null; + } + if ($node->kind === ast\AST_VAR) { + break; + } + if ($node->kind === ast\AST_DIM) { + $node = $node->children['expr']; + if (!$node instanceof Node) { + return null; + } + continue; + } + if ($node->kind === ast\AST_PROP) { + if (is_string($node->children['prop']) && self::isThisVarNode($node->children['expr'])) { + return 'this'; + } + } + + // TODO: Handle more than one level of nesting + return null; + } + $var_name = $node->children['name']; + return is_string($var_name) ? $var_name : null; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ContextMergeVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/ContextMergeVisitor.php new file mode 100644 index 000000000..b9e2f832c --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ContextMergeVisitor.php @@ -0,0 +1,449 @@ + + * A list of the contexts returned after depth-first + * parsing of all first-level children of this node + */ + private $child_context_list; + + /** + * @param Context $context + * The context of the parser at the node for which we'd + * like to determine a type + * + * @param list $child_context_list + * A list of the contexts returned after depth-first + * parsing of all first-level children of this node + */ + public function __construct( + Context $context, + array $child_context_list + ) { + $this->context = $context; + $this->child_context_list = $child_context_list; + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node @unused-param + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visit(Node $node): Context + { + // TODO: if ($this->context->isInGlobalScope()) { + // copy local to global + // } + + return \end($this->child_context_list) ?: $this->context; + } + + /** + * Merges the only try block of a try/catch node into the parent context. + * This acts as though the entire block succeeds or throws on the first statement, which isn't necessarily the case. + * + * visitTry() was split out into multiple functions for the following reasons: + * + * 1. The try{} block affects the Context of catch blocks (and finally block), if any + * 2. The catch blocks affect the Context of the finally block, if any + * + * TODO: Look at ways to improve accuracy based on inferences of the exit status of the node? + */ + public function mergeTryContext(Node $node): Context + { + if (\count($this->child_context_list) !== 1) { + throw new AssertionError("Expected one child context in " . __METHOD__); + } + + // Get the list of scopes for each branch of the + // conditional + $context = $this->context; + $try_context = $this->child_context_list[0]; + + if (self::willRemainingStatementsBeAnalyzedAsIfTryMightFail($node)) { + return $this->combineScopeList([ + $context->getScope(), + $try_context->getScope() + ]); + } + return $try_context; + } + + private static function willRemainingStatementsBeAnalyzedAsIfTryMightFail(Node $node): bool + { + if ($node->children['finally'] !== null) { + // We want to analyze finally as if the try block (and one or more of the catch blocks) was or wasn't executed. + // ... This isn't optimal. + // A better solution would be to analyze finally{} twice, + // 1. As if try could fail + // 2. As if try did not fail, using the latter to analyze statements after the finally{}. + return true; + } + // E.g. after analyzing the following code: + // try { $x = expr(); } catch (Exception $e) { echo "Caught"; return; } catch (OtherException $e) { continue; } + // Phan should infer that $x is guaranteed to be defined. + foreach ($node->children['catches']->children ?? [] as $catch_node) { + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable, PhanPossiblyUndeclaredProperty this is never null + if (BlockExitStatusChecker::willUnconditionallySkipRemainingStatements($catch_node->children['stmts'])) { + return false; + } + } + return true; + } + + /** + * Returns a context resulting from merging the possible variable types from the catch statements + * that will fall through. + */ + public function mergeCatchContext(Node $node): Context + { + if (\count($this->child_context_list) < 2) { + throw new AssertionError("Expected at least two contexts in " . __METHOD__); + } + // Get the list of scopes for each branch of the + // conditional + $scope_list = \array_map(static function (Context $context): Scope { + return $context->getScope(); + }, $this->child_context_list); + + $catch_scope_list = []; + $catch_nodes = $node->children['catches']->children; + foreach ($catch_nodes as $i => $catch_node) { + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable, PhanPossiblyUndeclaredProperty this is never null + if (!BlockExitStatusChecker::willUnconditionallySkipRemainingStatements($catch_node->children['stmts'])) { + $catch_scope_list[] = $scope_list[$i + 1]; + } + } + // TODO: Check if try node unconditionally returns. + + // Merge in the types for any variables found in a catch. + // if ($node->children['finally'] !== null) { + // If we have to analyze a finally statement later, + // then be conservative and assume the try statement may or may not have failed. + // E.g. the below example must have a inferred type of string|false + // $x = new stdClass(); try {...; $x = (string)fn(); } catch(Exception $e) { $x = false; } + // $try_scope = $this->context->getScope(); + // } else { + // If we don't have to worry about analyzing the finally statement, then assume that the entire try statement succeeded or the a catch statement succeeded. + // @phan-suppress-next-line PhanPossiblyNonClassMethodCall + $try_scope = \reset($this->child_context_list)->getScope(); + // } + + if (!$catch_scope_list) { + // All of the catch statements will unconditionally rethrow or return. + // So, after the try and catch blocks (finally is analyzed separately), + // the context is the same as if the try block finished successfully. + return $this->context->withScope($try_scope); + } + + if (\count($catch_scope_list) > 1) { + $catch_scope = $this->combineScopeList($catch_scope_list)->getScope(); + } else { + $catch_scope = \reset($catch_scope_list); + } + + // TODO: Use getVariableMapExcludingScope + foreach ($try_scope->getVariableMap() as $variable_name => $variable) { + $variable_name = (string)$variable_name; // e.g. ${42} + // Merge types if try and catch have a variable in common + $catch_variable = $catch_scope->getVariableByNameOrNull( + $variable_name + ); + if ($catch_variable) { + $variable->setUnionType($variable->getUnionType()->withUnionType( + $catch_variable->getUnionType() + )); + } + } + + // Look for variables that exist in catch, but not try + foreach ($catch_scope->getVariableMap() as $variable_name => $variable) { + $variable_name = (string)$variable_name; + if (!$try_scope->hasVariableWithName($variable_name)) { + $type = $variable->getUnionType(); + if (!$type->containsNullableLabeled()) { + $type = $type->withType(NullType::instance(false)); + } + // Note that it can be null + // TODO: This still infers the wrong type when there are multiple catch blocks. + // Combine all of the catch blocks into one context and merge with that instead? + $variable->setUnionType($type->withIsPossiblyUndefined(true)); + + // Add it to the try scope + $try_scope->addVariable($variable); + } + } + + // Set the new scope with only the variables and types + // that are common to all branches + return $this->context->withScope($try_scope); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitIf(Node $node): Context + { + // Get the list of scopes for each branch of the + // conditional + $scope_list = \array_map(static function (Context $context): Scope { + return $context->getScope(); + }, $this->child_context_list); + + $has_else = self::hasElse($node->children); + + // If we're not guaranteed to hit at least one + // branch, mark the incoming scope as a possibility + if (!$has_else) { + $scope_list[] = $this->context->getScope(); + } + + // If there weren't multiple branches, continue on + // as if the conditional never happened + if (\count($scope_list) < 2) { + // @phan-suppress-next-line PhanPossiblyFalseTypeReturn child_context_list is not empty + return \reset($this->child_context_list); + } + + return $this->combineScopeList($scope_list); + } + + /** + * Similar to visitIf, but only includes contexts up to (and including) the first context inferred to be unconditionally true. + */ + public function mergePossiblySingularChildContextList(): Context + { + // Get the list of scopes for each branch of the + // conditional + $scope_list = \array_map(static function (Context $context): Scope { + return $context->getScope(); + }, $this->child_context_list); + + // If there weren't multiple branches, continue on + // as if the conditional never happened + if (\count($scope_list) < 2) { + // @phan-suppress-next-line PhanPossiblyFalseTypeReturn child_context_list is not empty + return \reset($this->child_context_list); + } + + return $this->combineScopeList($scope_list); + } + + /** + * @param array $children children of a Node of kind AST_IF + */ + private static function hasElse(array $children): bool + { + foreach ($children as $child_node) { + if ($child_node instanceof Node + && \is_null($child_node->children['cond'])) { + return true; + } + } + return false; + } + + /** + * A generic helper method to merge multiple Contexts. (e.g. for use outside of BlockAnalysisVisitor) + * If you wish to include the base context, add it to $child_context_list in the constructor of ContextMergeVisitor. + */ + public function combineChildContextList(): Context + { + $child_context_list = $this->child_context_list; + if (\count($child_context_list) < 2) { + throw new AssertionError("Expected at least two child contexts in " . __METHOD__); + } + $scope_list = \array_map(static function (Context $context): Scope { + return $context->getScope(); + }, $child_context_list); + return $this->combineScopeList($scope_list); + } + + /** + * Returns a new scope which combines the parent scope with a list of 2 or more child scopes + * (one of those scopes is permitted to be the parent scope) + * @param list $scope_list + * @suppress PhanAccessPropertyInternal Repeatedly using ConfigPluginSet::$mergeVariableInfoClosure + */ + public function combineScopeList(array $scope_list): Context + { + if (\count($scope_list) < 2) { + throw new AssertionError("Expected at least two child contexts in " . __METHOD__); + } + // Get a list of all variables in all scopes + $variable_map = Scope::getDifferingVariables($scope_list); + if (!$variable_map) { + return $this->context->withClonedScope(); + } + + // A function that determines if a variable is defined on + // every branch + $is_defined_on_all_branches = + function (string $variable_name) use ($scope_list): bool { + foreach ($scope_list as $scope) { + $variable = $scope->getVariableByNameOrNull($variable_name); + if (\is_object($variable)) { + if (!$variable->getUnionType()->isPossiblyUndefined()) { + continue; + } + // fall through and check if this is a superglobal or global + } + // When there are conditions on superglobals or hardcoded globals, + // then one scope will have a copy of the variable but not the other. + if (Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())) { + $scope->addVariable(new Variable( + $this->context, + $variable_name, + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable + Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name), + 0 + )); + return true; + } + return false; + } + return true; + }; + + // Get the intersection of all types for all versions of + // the variable from every side of the branch + $union_type = + static function (string $variable_name) use ($scope_list): UnionType { + $previous_type = null; + $type_list = []; + // Get a list of all variables with the given name from + // each scope + foreach ($scope_list as $scope) { + $variable = $scope->getVariableByNameOrNull($variable_name); + if (\is_null($variable)) { + continue; + } + + $type = $variable->getUnionType(); + // Frequently, a branch won't even modify a variable's type. + // The immutable UnionType might have the exact same instance + if ($type !== $previous_type) { + $type_list[] = $type; + + $previous_type = $type; + } + } + + if (\count($type_list) < 2) { + return $type_list[0] ?? UnionType::empty(); + } else { + // compute the un-normalized types + $result = UnionType::merge($type_list, $variable_name !== Context::VAR_NAME_THIS_PROPERTIES); + } + + $result_count = $result->typeCount(); + foreach ($type_list as $type) { + if ($type->typeCount() < $result_count) { + // normalize it if any of the types varied + // (i.e. one of the types lacks types in the type union) + // + // This is useful to avoid ending up with "bool|?false|true" (Will convert to "?bool") + return $result->asNormalizedTypes(); + } + } + // Otherwise, don't normalize it - The different contexts didn't differ in the union types + return $result; + }; + + // Clone the incoming scope so we can modify it + // with the outgoing merged scope + $scope = clone($this->context->getScope()); + + foreach ($variable_map as $name => $variable) { + $name = (string)$name; + // Skip variables that are only partially defined + if (!$is_defined_on_all_branches($name)) { + if ($name === Context::VAR_NAME_THIS_PROPERTIES) { + $type = $union_type($name)->asNormalizedTypes()->asMappedUnionType(static function (Type $type): Type { + if (!$type instanceof ArrayShapeType) { + return $type; + } + $new_field_types = []; + foreach ($type->getFieldTypes() as $field_name => $value) { + $new_field_types[$field_name] = $value->isDefinitelyUndefined() ? $value : $value->withIsPossiblyUndefined(true); + } + return ArrayShapeType::fromFieldTypes($new_field_types, $type->isNullable()); + }); + $variable = clone($variable); + $variable->setUnionType($type); + $scope->addVariable($variable); + // there are no overrides for $this on at least one branch. + // TODO: Could try to combine local overrides with the defaults. + continue; + } + $variable = clone($variable); + $variable->setUnionType($union_type($name)->nullableClone()->withIsPossiblyUndefined(true)); + if (ConfigPluginSet::$mergeVariableInfoClosure) { + // @phan-suppress-next-line PhanTypePossiblyInvalidCallable + (ConfigPluginSet::$mergeVariableInfoClosure)($variable, $scope_list, false); + } + $scope->addVariable($variable); + continue; + } + + // Limit the type of the variable to the subset + // of types that are common to all branches + $variable = clone($variable); + + $variable->setUnionType( + $union_type($name) + ); + if (ConfigPluginSet::$mergeVariableInfoClosure) { + // @phan-suppress-next-line PhanTypePossiblyInvalidCallable + (ConfigPluginSet::$mergeVariableInfoClosure)($variable, $scope_list, true); + } + + // Add the variable to the outgoing scope + $scope->addVariable($variable); + } + + // Set the new scope with only the variables and types + // that are common to all branches + return $this->context->withScope($scope); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/DuplicateClassAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/DuplicateClassAnalyzer.php new file mode 100644 index 000000000..795930cd5 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/DuplicateClassAnalyzer.php @@ -0,0 +1,98 @@ +getFQSEN()->isAlternate()) { + return; + } + + $original_fqsen = $clazz->getFQSEN()->getCanonicalFQSEN(); + + // @phan-suppress-next-line PhanPartialTypeMismatchArgument static method has ambiguity + if (!$code_base->hasClassWithFQSEN($original_fqsen)) { + // If there's a missing class we'll catch that + // elsewhere + return; + } + + // Get the original class + // @phan-suppress-next-line PhanPartialTypeMismatchArgument static method has ambiguity + $original_class = $code_base->getClassByFQSEN($original_fqsen); + + // Check to see if the original definition was from + // an internal class + if ($original_class->isPHPInternal()) { + if (!$clazz->checkHasSuppressIssueAndIncrementCount(Issue::RedefineClassInternal)) { + Issue::maybeEmit( + $code_base, + $clazz->getContext(), + Issue::RedefineClassInternal, + $clazz->getFileRef()->getLineNumberStart(), + (string)$clazz, + $clazz->getFileRef()->getFile(), + $clazz->getFileRef()->getLineNumberStart(), + (string)$original_class + ); + } + // Otherwise, print the coordinates of the original + // definition + } else { + if (!$clazz->checkHasSuppressIssueAndIncrementCount(Issue::RedefineClass)) { + Issue::maybeEmit( + $code_base, + $clazz->getContext(), + Issue::RedefineClass, + $clazz->getFileRef()->getLineNumberStart(), + (string)$clazz, + $clazz->getFileRef()->getFile(), + $clazz->getFileRef()->getLineNumberStart(), + (string)$original_class, + $original_class->getFileRef()->getFile(), + $original_class->getFileRef()->getLineNumberStart() + ); + } + // If there are 3 classes with the same namespace and name, + // warn *once* about the first (user-defined) class being a duplicate. + // NOTE: This won't work very well in language server mode. + if ($clazz->getFQSEN()->getAlternateId() === 1) { + if (!$original_class->checkHasSuppressIssueAndIncrementCount(Issue::RedefineClass)) { + Issue::maybeEmit( + $code_base, + $original_class->getContext(), + Issue::RedefineClass, + $original_class->getFileRef()->getLineNumberStart(), + (string)$original_class, + $original_class->getFileRef()->getFile(), + $original_class->getFileRef()->getLineNumberStart(), + (string)$clazz, + $clazz->getFileRef()->getFile(), + $clazz->getFileRef()->getLineNumberStart() + ); + } + } + } + + return; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/DuplicateFunctionAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/DuplicateFunctionAnalyzer.php new file mode 100644 index 000000000..14cea84b2 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/DuplicateFunctionAnalyzer.php @@ -0,0 +1,93 @@ +getFQSEN(); + + if (!$fqsen->isAlternate()) { + return; + } + + $original_fqsen = $fqsen->getCanonicalFQSEN(); + + if ($original_fqsen instanceof FullyQualifiedFunctionName) { + if (!$code_base->hasFunctionWithFQSEN($original_fqsen)) { + return; + } + + $original_method = $code_base->getFunctionByFQSEN( + $original_fqsen + ); + } else { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + if (!$code_base->hasMethodWithFQSEN($original_fqsen)) { + return; + } + + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $original_method = $code_base->getMethodByFQSEN($original_fqsen); + } + + $method_name = $method->getName(); + + if ($original_method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::RedefineFunctionInternal, + $method->getFileRef()->getLineNumberStart(), + $method_name, + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } else { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::RedefineFunction, + $method->getFileRef()->getLineNumberStart(), + $method_name, + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart(), + $original_method->getFileRef()->getFile(), + $original_method->getFileRef()->getLineNumberStart() + ); + // If there are 3 functions with the same namespace and name, + // warn *once* about the first functions being a duplicate. + // NOTE: This won't work very well in language server mode. + if ($fqsen->getAlternateId() === 1) { + Issue::maybeEmit( + $code_base, + $original_method->getContext(), + Issue::RedefineFunction, + $original_method->getFileRef()->getLineNumberStart(), + $original_method->getName(), + $original_method->getFileRef()->getFile(), + $original_method->getFileRef()->getLineNumberStart(), + $method->getFileRef()->getFile(), + $method->getFileRef()->getLineNumberStart() + ); + } + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/FallbackMethodTypesVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/FallbackMethodTypesVisitor.php new file mode 100644 index 000000000..459fed814 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/FallbackMethodTypesVisitor.php @@ -0,0 +1,258 @@ +> the list of union types assigned to a variable */ + public $known_types = []; + /** @var associative-array the set of variables with unknown types */ + public $unknowns = []; + + public function __construct(CodeBase $code_base, Context $context) + { + $this->code_base = $code_base; + $this->context = $context; + } + + /** + * Conservatively infers types from all assignments seen and parameters/closure use variables. + * Gives up when non-literals are seen. + * @return array + */ + public static function inferTypes(CodeBase $code_base, FunctionInterface $func): array + { + $function_node = $func->getNode(); + if (!$function_node instanceof Node) { + // XXX this won't work in --quick due to not storing nodes. + return []; + } + $stmts = $function_node->children['stmts'] ?? null; + if (!$stmts instanceof Node) { + return []; + } + try { + $visitor = new self($code_base, $func->getContext()); + $visitor->visit($stmts); + foreach ($func->getParameterList() as $param) { + $visitor->associateType($param->getName(), $param->getUnionType()); + } + foreach ($function_node->children['uses']->children ?? [] as $use) { + // @phan-suppress-next-line PhanPossiblyUndeclaredProperty + $visitor->unknowns[$use->children['name']] = true; + } + $result = []; + foreach (\array_diff_key($visitor->known_types, $visitor->unknowns) as $key => $union_types) { + $result[$key] = UnionType::merge($union_types)->asNormalizedTypes(); + } + // echo json_encode(array_map('strval', $result)); + return $result; + } catch (NodeException $_) { + return []; + } + } + + /** + * @override + * @unused-param $node + * @return void + */ + public function visitClass(Node $node) + { + } + + /** + * @override + * @unused-param $node + * @return void + */ + public function visitFuncDecl(Node $node) + { + } + + /** + * @override + * @return void + */ + public function visitClosure(Node $node) + { + // handle uses by ref - treat as having unknown types for now. + // TODO: Could recurse for more accuracy + foreach ($node->children['uses']->children ?? [] as $c) { + if ($c instanceof Node && ($c->flags & ast\flags\CLOSURE_USE_REF)) { + $this->unknowns[$c->children['name']] = true; + } + } + } + + /** + * @override + * @unused-param $node + * @return void + */ + public function visitArrowFunc(Node $node) + { + } + + /** + * @override + * @return void + */ + public function visit(Node $node) + { + foreach ($node->children as $c) { + if ($c instanceof Node) { + $this->__invoke($c); + } + } + } + + /** + * @override + * @return void + */ + public function visitAssign(Node $node) + { + // \Phan\Debug::printNode($node); + $var = $node->children['var']; + if (!($var instanceof Node) || $var->kind !== ast\AST_VAR) { + $this->excludeReferencedVariables($node); + return; + } + $var_name = $var->children['name']; + if (!is_scalar($var_name)) { + $this->excludeReferencedVariables($node); + return; + } + $expr = $node->children['expr']; + $this->associateTypeWithExpression($var_name, $expr); + $this->visit($node); + } + + /** + * @override + * @return void + */ + public function visitAssignRef(Node $node) + { + $this->excludeReferencedVariables($node); + } + + private function excludeReferencedVariables(Node $node): void + { + switch ($node->kind) { + case ast\AST_VAR: + $name = $node->children['name']; + if (!is_scalar($name)) { + throw new NodeException($node, "Dynamic reference not supported"); + } + $this->unknowns[$name] = true; + return; + case ast\AST_CLOSURE: + $this->visitClosure($node); + return; + case ast\AST_FUNC_DECL: + case ast\AST_ARROW_FUNC: + case ast\AST_CLASS: + return; + } + } + + /** + * @param int|string|float|bool $var_name + * @param int|string|float|Node $expr + */ + private function associateTypeWithExpression($var_name, $expr): void + { + if (isset($this->unknowns[$var_name])) { + // No point in checking. + return; + } + if (!$expr instanceof Node) { + $this->associateType($var_name, Type::fromObject($expr)->asRealUnionType()); + return; + } + $type = $this->determineUnionType($expr); + if ($type instanceof UnionType) { + $this->associateType($var_name, $type); + } else { + $this->unknowns[$var_name] = true; + } + } + + private function determineUnionType(Node $expr): ?UnionType + { + try { + if (ParseVisitor::isConstExpr($expr)) { + return (new UnionTypeVisitor($this->code_base, $this->context, false))->__invoke($expr); + } + return (new FallbackUnionTypeVisitor($this->code_base, $this->context))->__invoke($expr); + } catch (Exception $_) { + } + // TODO: Handle binary ops such as %, >, ternary, etc. + return null; + } + + /** + * @param int|string|float|bool $var_name + */ + private function associateType($var_name, UnionType $type): void + { + if (!$type->isEmpty()) { + $this->known_types[$var_name][] = $type; + } else { + $this->unknowns[$var_name] = true; + } + } + + /** + * Returns a representation of this visitor suitable for debugging + * @suppress PhanUnreferencedPublicMethod + */ + public function getDebugRepresentation(): string + { + return "FallbackTypeVisitor: " . json_encode([ + 'known_types' => array_map( + /** + * @param list $union_types + * @return list + */ + static function (array $union_types): array { + return array_map(static function (UnionType $type): string { + return $type->getDebugRepresentation(); + }, $union_types); + }, + $this->known_types + ), + 'unknowns' => $this->unknowns, + ], \JSON_PRETTY_PRINT); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/GotoAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/GotoAnalyzer.php new file mode 100644 index 000000000..12b147dba --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/GotoAnalyzer.php @@ -0,0 +1,71 @@ + $parent_node_list + * @return array + */ + public static function getLabelSet(array $parent_node_list): array + { + // Find the AST_STMT_LIST that is the root of the function-like + $prev_node = null; + for ($i = \count($parent_node_list) - 1; $i >= 0; $i--) { + $node = $parent_node_list[$i]; + if (\in_array($node->kind, [ast\AST_FUNC_DECL, ast\AST_CLOSURE, ast\AST_METHOD], true)) { + break; + } + $prev_node = $node; + } + if (!$prev_node) { + return []; + } + // $prev_node is the AST_STMT_LIST in the global scope or the nearest function-like scope. + // @phan-suppress-next-line PhanUndeclaredProperty deliberately adding this to a dynamic property to avoid recomputing it for large function bodies. + return $prev_node->used_label_set ?? ($prev_node->used_label_set = self::computeLabelSet($prev_node)); + } + + /** + * @return array the set of labels that are used by "goto label" in this function-like scope or global scope. + */ + private static function computeLabelSet(Node $node): array + { + $result = []; + $nodes = []; + while (true) { + $kind = $node->kind; + // fprintf(STDERR, "Processing node of kind %s\n", ast\get_kind_name($kind)); + switch ($kind) { + case ast\AST_FUNC_DECL: + case ast\AST_CLOSURE: + case ast\AST_METHOD: + case ast\AST_CLASS: + break; + case ast\AST_GOTO: + $result[(string)$node->children['label']] = true; + break; + default: + foreach ($node->children as $child_node) { + if ($child_node instanceof Node) { + $nodes[] = $child_node; + } + } + } + if (\count($nodes) === 0) { + return $result; + } + $node = \array_pop($nodes); + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/LoopConditionVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/LoopConditionVisitor.php new file mode 100644 index 000000000..3c216b2f3 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/LoopConditionVisitor.php @@ -0,0 +1,105 @@ +loop_condition_node = $loop_condition_node; + $this->allow_false = $allow_false; + $this->loop_body_unconditionally_proceeds = $loop_body_unconditionally_proceeds; + } + + public function checkRedundantOrImpossibleTruthyCondition($node, Context $context, ?UnionType $type, bool $is_negated): void + { + if (!$this->loop_body_unconditionally_proceeds && $node === $this->loop_condition_node) { + // Don't warn about `while (1)` or `while (true)` + if ($node instanceof Node) { + if ($node->kind === ast\AST_CONST) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal + $node_name = \strtolower($node->children['name']->children['name'] ?? ''); + if ($node_name === 'true' || ($this->allow_false && $node_name === 'false')) { + return; + } + } + } elseif (\is_int($node)) { + if ($node || $this->allow_false) { + return; + } + } + } + parent::checkRedundantOrImpossibleTruthyCondition($node, $context, $type, $is_negated); + } + + /** + * @override + * @param Node|mixed $node + */ + protected function chooseIssueForUnconditionallyTrue(bool $is_negated, $node): string + { + if (!$is_negated && $node === $this->loop_condition_node) { + return Issue::InfiniteLoop; + } + return parent::chooseIssueForUnconditionallyTrue($is_negated, $node); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitExprList(Node $node): Context + { + $children = $node->children; + $count = \count($children); + if ($count > 1) { + foreach ($children as $sub_node) { + --$count; + if ($count > 0 && $sub_node instanceof Node) { + $this->checkVariablesDefined($sub_node); + } + } + } + // Only analyze the last expression in the expression list for conditions. + $last_expression = \end($children); + if ($node === $this->loop_condition_node) { + // @phan-suppress-next-line PhanPartialTypeMismatchProperty + $this->loop_condition_node = $last_expression; + } + if ($last_expression instanceof Node) { + return $this->__invoke($last_expression); + } elseif (Config::getValue('redundant_condition_detection')) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument, PhanTypeMismatchArgumentNullable + $this->checkRedundantOrImpossibleTruthyCondition($last_expression, $this->context, null, false); + } + return $this->context; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/NegatedConditionVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/NegatedConditionVisitor.php new file mode 100644 index 000000000..dcbc22f55 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/NegatedConditionVisitor.php @@ -0,0 +1,1066 @@ +code_base = $code_base; + $this->context = $context; + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visit(Node $node): Context + { + $this->checkVariablesDefined($node); + if (Config::getValue('redundant_condition_detection')) { + $this->checkRedundantOrImpossibleTruthyCondition($node, $this->context, null, true); + } + return $this->context; + } + + /** + * Check if variables from within a generic condition are defined. + * @param Node $node + * A node to parse + */ + private function checkVariablesDefined(Node $node): void + { + while ($node->kind === ast\AST_UNARY_OP) { + $node = $node->children['expr']; + if (!($node instanceof Node)) { + return; + } + } + // Get the type just to make sure everything + // is defined. + UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node, + true + ); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitBinaryOp(Node $node): Context + { + $flags = $node->flags ?? 0; + switch ($flags) { + case flags\BINARY_BOOL_OR: + return $this->analyzeShortCircuitingOr($node->children['left'], $node->children['right']); + case flags\BINARY_BOOL_AND: + return $this->analyzeShortCircuitingAnd($node->children['left'], $node->children['right']); + case flags\BINARY_IS_IDENTICAL: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeNotIdentical($node->children['left'], $node->children['right']); + case flags\BINARY_IS_EQUAL: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeNotEqual($node->children['left'], $node->children['right']); + case flags\BINARY_IS_NOT_IDENTICAL: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeIdentical($node->children['left'], $node->children['right']); + case flags\BINARY_IS_NOT_EQUAL: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeEqual($node->children['left'], $node->children['right']); + case flags\BINARY_IS_GREATER: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeCompared($node->children['left'], $node->children['right'], flags\BINARY_IS_SMALLER_OR_EQUAL); + case flags\BINARY_IS_GREATER_OR_EQUAL: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeCompared($node->children['left'], $node->children['right'], flags\BINARY_IS_SMALLER); + case flags\BINARY_IS_SMALLER: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeCompared($node->children['left'], $node->children['right'], flags\BINARY_IS_GREATER_OR_EQUAL); + case flags\BINARY_IS_SMALLER_OR_EQUAL: + $this->checkVariablesDefined($node); + return $this->analyzeAndUpdateToBeCompared($node->children['left'], $node->children['right'], flags\BINARY_IS_GREATER); + default: + $this->checkVariablesDefined($node); + return $this->context; + } + } + + /** + * Helper method + * @param Node|string|int|float $left + * a Node or non-node to parse (possibly an AST literal) + * + * @param Node|string|int|float $right + * a Node or non-node to parse (possibly an AST literal) + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the negation of the short-circuiting and. + * + * @suppress PhanSuspiciousTruthyString deliberate cast of literal to boolean + */ + private function analyzeShortCircuitingAnd($left, $right): Context + { + // Analyze expressions such as if (!(is_string($x) || is_int($x))) + // which would be equivalent to if (!is_string($x)) { if (!is_int($x)) { ... }} + + // Aside: If left/right is not a node, left/right is a literal such as a number/string, and is either always truthy or always falsey. + // Inside of this conditional may be dead or redundant code. + + // Aside: If left/right is not a node, left/right is a literal such as a number/string, and is either always truthy or always falsey. + // Inside of this conditional may be dead or redundant code. + if (!($left instanceof Node)) { + if (!$left) { + return $this->context; + } + if (!$right instanceof Node) { + return $this->context; + } + return $this($right); + } + if (!($right instanceof Node)) { + if (!$right) { + return $this->context; + } + return $this($left); + } + $code_base = $this->code_base; + $context = $this->context; + $left_false_context = (new NegatedConditionVisitor($code_base, $context))($left); + $left_true_context = (new ConditionVisitor($code_base, $context))($left); + // We analyze the right-hand side of `cond($x) && cond2($x)` as if `cond($x)` was true. + $right_false_context = (new NegatedConditionVisitor($code_base, $left_true_context))($right); + // When the NegatedConditionVisitor is false, at least one of the left or right contexts must be false. + // (NegatedConditionVisitor returns a context for when the input Node's value was falsey) + return (new ContextMergeVisitor($context, [$left_false_context, $right_false_context]))->combineChildContextList(); + } + + /** + * @param Node|string|int|float $left + * a Node or non-node to parse (possibly an AST literal) + * + * @param Node|string|int|float $right + * a Node or non-node to parse (possibly an AST literal) + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the negation of the short-circuiting or. + */ + private function analyzeShortCircuitingOr($left, $right): Context + { + // Analyze expressions such as if (!(is_string($x) || is_int($x))) + // which would be equivalent to if (!is_string($x)) { if (!is_int($x)) { ... }} + + // Aside: If left/right is not a node, left/right is a literal such as a number/string, and is either always truthy or always falsey. + // Inside of this conditional may be dead or redundant code. + if ($left instanceof Node) { + $this->context = $this($left); + } + if ($right instanceof Node) { + return $this($right); + } + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitUnaryOp(Node $node): Context + { + $expr_node = $node->children['expr']; + $flags = $node->flags; + if ($flags !== flags\UNARY_BOOL_NOT) { + if (Config::getValue('redundant_condition_detection')) { + $this->checkRedundantOrImpossibleTruthyCondition($node, $this->context, null, true); + } + if ($expr_node instanceof Node) { + if ($flags === flags\UNARY_SILENCE) { + return $this->__invoke($expr_node); + } + $this->checkVariablesDefined($expr_node); + } + return $this->context; + } + // TODO: Emit dead code issue for non-nodes + if ($expr_node instanceof Node) { + // The negated version of a NegatedConditionVisitor is a ConditionVisitor. + return (new ConditionVisitor($this->code_base, $this->context))($expr_node); + } elseif (Config::getValue('redundant_condition_detection')) { + // Check `scalar` of `if (!scalar)` + $this->checkRedundantOrImpossibleTruthyCondition($expr_node, $this->context, null, false); + } + return $this->context; + } + + /** + * Look at elements of the form `is_array($v)` and modify + * the type of the variable to negate that check. + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitCall(Node $node): Context + { + $raw_function_name = self::getFunctionName($node); + if (!\is_string($raw_function_name)) { + return $this->context; + } + $args = $node->children['args']->children; + + $function_name = \strtolower(\ltrim($raw_function_name, '\\')); + if ($function_name === 'array_key_exists') { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + return $this->analyzeArrayKeyExistsNegation($args); + } + static $map; + if ($map === null) { + $map = self::createNegationCallbackMap(); + } + $type_modification_callback = $map[$function_name] ?? null; + if ($type_modification_callback === null) { + return $this->context; + } + $first_arg = $args[0] ?? null; + if (!($first_arg instanceof Node && $first_arg->kind === ast\AST_VAR)) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument, PhanTypeMismatchArgumentNullable + return $this->modifyComplexExpression($first_arg, $type_modification_callback, $this->context, $args); + } + + $context = $this->context; + + try { + // Get the variable we're operating on + $variable = $this->getVariableFromScope($first_arg, $context); + + if (\is_null($variable)) { + return $context; + } + + // Make a copy of the variable + $variable = clone($variable); + + // Modify the types of that variable. + $type_modification_callback($this->code_base, $context, $variable, $args); + + // Overwrite the variable with its new type in this + // scope without overwriting other scopes + $context = $context->withScopeVariable( + $variable + ); + } catch (IssueException $exception) { + Issue::maybeEmitInstance($this->code_base, $context, $exception->getIssueInstance()); + } catch (\Exception $_) { + // Swallow it (E.g. IssueException for undefined variable) + } + + return $context; + } + + public function visitVar(Node $node): Context + { + $this->checkVariablesDefined($node); + return $this->removeTruthyFromVariable($node, $this->context, false, false); + } + + /** + * @param Node $node + * A node to parse, with kind ast\AST_NULLABLE_PROP (e.g. `if (!$this?->prop_name)`) + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitNullsafeProp(Node $node): Context + { + // TODO: Adjust this for values other than $this, e.g. to imply the expression is null or an object + return $this->visitProp($node); + } + + /** + * @param Node $node + * A node to parse, with kind ast\AST_PROP (e.g. `if (!$this->prop_name)`) + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitProp(Node $node): Context + { + $expr_node = $node->children['expr']; + if (!($expr_node instanceof Node)) { + return $this->context; + } + if ($expr_node->kind !== ast\AST_VAR || $expr_node->children['name'] !== 'this') { + return $this->context; + } + if (!\is_string($node->children['prop'])) { + return $this->context; + } + return $this->modifyPropertyOfThisSimple( + $node, + function (UnionType $type) use ($node): UnionType { + if (Config::getValue('error_prone_truthy_condition_detection')) { + $this->checkErrorProneTruthyCast($node, $this->context, $type); + } + return $type->nonTruthyClone(); + }, + $this->context + ); + } + + /** + * @param list $args + */ + private function analyzeArrayKeyExistsNegation(array $args): Context + { + if (\count($args) !== 2) { + return $this->context; + } + $var_node = $args[1]; + if (!($var_node instanceof Node)) { + return $this->context; + } + return $this->updateVariableWithConditionalFilter( + $var_node, + $this->context, + static function (UnionType $_): bool { + return true; + }, + function (UnionType $type) use ($args): UnionType { + if ($type->hasTopLevelArrayShapeTypeInstances()) { + return $this->withNullOrUnsetArrayShapeTypes($type, $args[0], $this->context, true); + } + return $type; + }, + true, + false + ); + } + + // TODO: empty, isset + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitInstanceof(Node $node): Context + { + //$this->checkVariablesDefined($node); + // Only look at things of the form + // `$variable instanceof ClassName` + $expr_node = $node->children['expr']; + $context = $this->context; + if (!($expr_node instanceof Node)) { + return $context; + } + $class_node = $node->children['class']; + if (!($class_node instanceof Node)) { + return $context; + } + if ($expr_node->kind !== ast\AST_VAR) { + return $this->modifyComplexExpression( + $expr_node, + /** + * @param list $args + * @suppress PhanUnusedClosureParameter + */ + function (CodeBase $code_base, Context $context, Variable $variable, array $args) use ($class_node): void { + $union_type = $this->computeNegatedInstanceofType($variable->getUnionType(), $class_node); + if ($union_type) { + $variable->setUnionType($union_type); + } + }, + $context, + [] + ); + } + + $code_base = $this->code_base; + + try { + // Get the variable we're operating on + $variable = $this->getVariableFromScope($expr_node, $context); + if (\is_null($variable)) { + return $context; + } + + // Get the type that we're checking it against + $new_variable_type = $this->computeNegatedInstanceofType($variable->getUnionType(), $class_node); + if (!$new_variable_type) { + // We don't know what it asserted it wasn't. + return $context; + } + + // TODO: Assert that instanceof right-hand type is valid in NegatedConditionVisitor as well + + // Make a copy of the variable + $variable = clone($variable); + // See https://secure.php.net/instanceof - + $variable->setUnionType($new_variable_type); + + // Overwrite the variable with its new type + $context = $context->withScopeVariable( + $variable + ); + } catch (IssueException $exception) { + Issue::maybeEmitInstance($code_base, $context, $exception->getIssueInstance()); + } catch (\Exception $_) { + // Swallow it + } + + return $context; + } + + /** + * Compute the type of $union_type after asserting `!(expr instanceof $class_node)` + * @param Node|string|int|float $class_node + */ + private function computeNegatedInstanceofType(UnionType $union_type, $class_node): ?UnionType + { + $right_hand_union_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $class_node + )->objectTypes(); + + if ($right_hand_union_type->typeCount() !== 1) { + return null; + } + $right_hand_type = $right_hand_union_type->getTypeSet()[0]; + if (!$right_hand_type->isObjectWithKnownFQSEN()) { + return null; + } + return $union_type->withoutSubclassesOf($this->code_base, $right_hand_type); + } + + /* + private function analyzeNegationOfVariableIsA(array $args, Context $context) : Context + { + // TODO: implement + return $context; + } + */ + + /** + * @return array (NegatedConditionVisitor $cv, Node $var_node, Context $context) -> Context + * @phan-return array + */ + private static function createNegationCallbackMap(): array + { + /** @param list $unused_args */ + $remove_null_cb = static function (CodeBase $unused_code_base, Context $unused_context, Variable $variable, array $unused_args): void { + $variable->setUnionType($variable->getUnionType()->nonNullableClone()); + }; + + // Remove any Types from UnionType that are subclasses of $base_class_name + $make_basic_negated_assertion_callback = static function (string $base_class_name): Closure { + /** + * @param list $unused_args + */ + return static function (CodeBase $unused_code_base, Context $unused_context, Variable $variable, array $unused_args) use ($base_class_name): void { + $variable->setUnionType($variable->getUnionType()->asMappedListUnionType(/** @return list */ static function (Type $type) use ($base_class_name): array { + if ($type instanceof $base_class_name) { + // This is the type we don't want + if ($type->isNullable()) { + static $null_type_set; + return $null_type_set ?? ($null_type_set = UnionType::typeSetFromString('null')); + } + return []; + } + return [$type]; + })->asNormalizedTypes()); + }; + }; + $remove_float_callback = $make_basic_negated_assertion_callback(FloatType::class); + $remove_int_callback = $make_basic_negated_assertion_callback(IntType::class); + /** + * @param Closure(Type):bool $type_filter + * @return Closure(CodeBase, Context, Variable, array):void + */ + $remove_conditional_function_callback = static function (Closure $type_filter): Closure { + /** + * @param list $unused_args + */ + return static function (CodeBase $unused_code_base, Context $unused_context, Variable $variable, array $unused_args) use ($type_filter): void { + $union_type = $variable->getUnionType(); + if (!$union_type->hasTypeMatchingCallback($type_filter)) { + return; + } + $new_type_builder = new UnionTypeBuilder(); + $has_null = false; + $has_other_nullable_types = false; + // Add types which are not scalars + foreach ($union_type->getTypeSet() as $type) { + if ($type_filter($type)) { + // e.g. mixed|SomeClass can be null because mixed can be null. + $has_null = $has_null || $type->isNullable(); + continue; + } + $has_other_nullable_types = $has_other_nullable_types || $type->isNullable(); + $new_type_builder->addType($type); + } + // Add Null if some of the rejected types were were nullable, and none of the accepted types were nullable + if ($has_null && !$has_other_nullable_types) { + $new_type_builder->addType(NullType::instance(false)); + } + // TODO: Infer real type sets as well? + $variable->setUnionType($new_type_builder->getPHPDocUnionType()); + }; + }; + $remove_scalar_callback = $remove_conditional_function_callback(static function (Type $type): bool { + return $type instanceof ScalarType && !($type instanceof NullType); + }); + $remove_numeric_callback = $remove_conditional_function_callback(static function (Type $type): bool { + return $type instanceof IntType || $type instanceof FloatType; + }); + $remove_bool_callback = $remove_conditional_function_callback(static function (Type $type): bool { + return $type->isInBoolFamily(); + }); + /** @param list $unused_args */ + $remove_callable_callback = static function (CodeBase $unused_code_base, Context $unused_context, Variable $variable, array $unused_args): void { + $variable->setUnionType($variable->getUnionType()->asMappedListUnionType(/** @return list */ static function (Type $type): array { + if ($type->isCallable()) { + if ($type->isNullable()) { + static $null_type_set; + return $null_type_set ?? ($null_type_set = UnionType::typeSetFromString('null')); + } + return []; + } + return [$type]; + })->asNormalizedTypes()); + }; + // TODO: Would withStaticResolvedInContext make sense for ruling out self in Countable? + /** @param list $unused_args */ + $remove_countable_callback = static function (CodeBase $code_base, Context $unused_context, Variable $variable, array $unused_args): void { + $variable->setUnionType($variable->getUnionType()->asMappedListUnionType(/** @return list */ static function (Type $type) use ($code_base): array { + if ($type->isCountable($code_base)) { + if ($type->isNullable()) { + static $null_type_set; + return $null_type_set ?? ($null_type_set = UnionType::typeSetFromString('null')); + } + return []; + } + return [$type]; + })->asNormalizedTypes()); + }; + /** @param list $unused_args */ + $zero_count_callback = static function (CodeBase $unused_code_base, Context $unused_context, Variable $variable, array $unused_args): void { + $variable->setUnionType($variable->getUnionType()->asMappedListUnionType(/** @return list */ static function (Type $type): array { + if ($type->isPossiblyObject()) { + // TODO: Could cast iterable to Traversable|array{} + return [$type]; + } + if (!$type->isPossiblyFalsey()) { + return []; + } + return [$type->asNonTruthyType()]; + })->asNormalizedTypes()); + }; + /** @param list $unused_args */ + $remove_array_callback = static function (CodeBase $unused_code_base, Context $unused_context, Variable $variable, array $unused_args): void { + $union_type = $variable->getUnionType(); + $variable->setUnionType(UnionType::of( + self::filterNonArrayTypes($union_type->getTypeSet()), + self::filterNonArrayTypes($union_type->getRealTypeSet()) + )); + }; + /** @param list $unused_args */ + $remove_object_callback = static function (CodeBase $unused_code_base, Context $unused_context, Variable $variable, array $unused_args): void { + $variable->setUnionType($variable->getUnionType()->asMappedListUnionType(/** @return list */ static function (Type $type): array { + if ($type->isObject()) { + if ($type->isNullable()) { + static $null_type_set; + return $null_type_set ?? ($null_type_set = UnionType::typeSetFromString('null')); + } + return []; + } + + if (\get_class($type) === IterableType::class) { + // An iterable that is not an array must be a Traversable + return [ArrayType::instance($type->isNullable())]; + } + return [$type]; + })->asNormalizedTypes()); + }; + + return [ + 'count' => $zero_count_callback, + 'is_null' => $remove_null_cb, + 'is_array' => $remove_array_callback, + 'is_bool' => $remove_bool_callback, + 'is_callable' => $remove_callable_callback, + 'is_countable' => $remove_countable_callback, + 'is_double' => $remove_float_callback, + 'is_float' => $remove_float_callback, + 'is_int' => $remove_int_callback, + 'is_integer' => $remove_int_callback, + 'is_iterable' => $make_basic_negated_assertion_callback(IterableType::class), // TODO: Could keep basic array types and classes extending iterable + 'is_long' => $remove_int_callback, + 'is_numeric' => $remove_numeric_callback, + 'is_object' => $remove_object_callback, + 'is_real' => $remove_float_callback, + 'is_resource' => $make_basic_negated_assertion_callback(ResourceType::class), + 'is_scalar' => $remove_scalar_callback, + 'is_string' => $make_basic_negated_assertion_callback(StringType::class), + ]; + } + + /** + * @param list $type_set + * @return list which may contain duplicates + */ + private static function filterNonArrayTypes(array $type_set): array + { + $new_types = []; + $has_null = false; + $has_other_nullable_types = false; + // Add types which are not callable + foreach ($type_set as $type) { + if ($type instanceof ArrayType) { + $has_null = $has_null || $type->isNullable(); + continue; + } + + $has_other_nullable_types = $has_other_nullable_types || $type->isNullable(); + + if (\get_class($type) === IterableType::class) { + // An iterable that is not an object must be an array + $new_types[] = Type::traversableInstance()->withIsNullable($type->isNullable()); + continue; + } + $new_types[] = $type; + } + // Add Null if some of the rejected types were were nullable, and none of the accepted types were nullable + if ($has_null && !$has_other_nullable_types) { + $new_types[] = NullType::instance(false); + } + return $new_types; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitIsset(Node $node): Context + { + $var_node = $node->children['var']; + if (!($var_node instanceof Node)) { + return $this->context; + } + if (($var_node->kind ?? null) !== ast\AST_VAR) { + return $this->checkComplexIsset($var_node); + } + // if (!isset($x)) means that $x is definitely null + return $this->updateVariableWithNewType($var_node, $this->context, NullType::instance(false)->asRealUnionType(), true, false); + } + + /** + * Analyze expressions such as $x['offset'] inside of a negated isset type check + */ + public function checkComplexIsset(Node $var_node): Context + { + $context = $this->context; + if ($var_node->kind === ast\AST_DIM) { + $expr_node = $var_node; + do { + $parent_node = $expr_node; + $expr_node = $expr_node->children['expr']; + if (!($expr_node instanceof Node)) { + return $context; + } + } while ($expr_node->kind === ast\AST_DIM); + + if ($expr_node->kind === ast\AST_VAR) { + $var_name = $expr_node->children['name']; + if (!\is_string($var_name)) { + return $context; + } + if ($context->getScope()->hasVariableWithName($var_name)) { + $variable = $context->getScope()->getVariableByName($var_name); + } else { + $new_type = Variable::getUnionTypeOfHardcodedVariableInScopeWithName($var_name, $context->isInGlobalScope()); + if (!$new_type) { + // e.g. assert(!isset($x['key'])) - $x may still be undefined. + return $context; + } + $variable = new Variable( + $context->withLineNumberStart($var_node->lineno), + $var_name, + $new_type, + 0 + ); + $context->getScope()->addVariable($variable); + } + $var_node_union_type = $variable->getUnionType(); + + if ($var_node_union_type->hasTopLevelArrayShapeTypeInstances()) { + $new_union_type = $this->withNullOrUnsetArrayShapeTypes($var_node_union_type, $parent_node->children['dim'], $context, false); + if ($new_union_type !== $var_node_union_type) { + $variable = clone($variable); + $variable->setUnionType($new_union_type); + $context = $context->withScopeVariable($variable); + } + $this->context = $context; + } + } + } elseif ($var_node->kind === ast\AST_PROP) { + $context = $this->modifyPropertySimple($var_node, static function (UnionType $_): UnionType { + return NullType::instance(false)->asPHPDocUnionType(); + }, $context); + } + return $context; + } + + /** + * @param UnionType $union_type the union type being modified by inferences from negated isset or array_key_exists + * @param Node|string|float|int|bool $dim_node represents the dimension being accessed. (E.g. can be a literal or an AST_CONST, etc. + * @param Context $context the context with inferences made prior to this condition + */ + private function withNullOrUnsetArrayShapeTypes(UnionType $union_type, $dim_node, Context $context, bool $remove_offset): UnionType + { + $dim_value = $dim_node instanceof Node ? (new ContextNode($this->code_base, $context, $dim_node))->getEquivalentPHPScalarValue() : $dim_node; + // TODO: detect and warn about null + if (!\is_scalar($dim_value)) { + return $union_type; + } + + $dim_union_type = UnionTypeVisitor::resolveArrayShapeElementTypesForOffset($union_type, $dim_value); + if (!$dim_union_type) { + // There are other types, this dimension does not exist yet. + // Whether or not the union type already has array shape types, don't change the type + return $union_type; + } + if ($remove_offset) { + return $union_type->withoutArrayShapeField($dim_value); + } else { + static $null_and_possibly_undefined = null; + if ($null_and_possibly_undefined === null) { + $null_and_possibly_undefined = NullType::instance(false)->asPHPDocUnionType()->withIsPossiblyUndefined(true); + } + + return ArrayType::combineArrayShapeTypesWithField($union_type, $dim_value, $null_and_possibly_undefined); + } + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitEmpty(Node $node): Context + { + $context = $this->context; + $var_node = $node->children['expr']; + if (!($var_node instanceof Node)) { + return $context; + } + // e.g. if (!empty($x)) + if ($var_node->kind === ast\AST_VAR) { + // Don't check if variables are defined - don't emit notices for if (!empty($x)) {}, etc. + $var_name = $var_node->children['name']; + if (\is_string($var_name)) { + if (!$context->getScope()->hasVariableWithName($var_name)) { + $new_type = Variable::getUnionTypeOfHardcodedVariableInScopeWithName($var_name, $context->isInGlobalScope()); + if ($new_type) { + $new_type = $new_type->nonFalseyClone(); + } else { + $new_type = UnionType::empty(); + } + // Support analyzing cases such as `if (!empty($x)) { use($x); }`, or `assert(!empty($x))` + // (In the PHP language, empty($x) is equivalent to (!isset($x) || !$x)) + $context->setScope($context->getScope()->withVariable(new Variable( + $context->withLineNumberStart($var_node->lineno ?? 0), + $var_name, + $new_type, + 0 + ))); + } + return $this->removeFalseyFromVariable($var_node, $context, true); + } + } elseif ($var_node->kind === ast\AST_PROP) { + // e.g. $var_node is the representation of $this->prop or $x->prop. + $context = $this->removeFalseyFromVariable($var_node, $context, true); + $expr = $var_node->children['expr']; + if ($expr instanceof Node) { + // Also imply $x is an object after !empty($x->prop) + return $this->removeTypesNotSupportingAccessFromVariable($expr, $context, ConditionVisitor::ACCESS_IS_OBJECT); + } + return $context; + } else { + $context = $this->checkComplexNegatedEmpty($var_node); + } + $this->checkVariablesDefined($node); + return $context; + } + + private function checkComplexNegatedEmpty(Node $var_node): Context + { + $context = $this->context; + // TODO: !empty($obj->prop['offset']) should imply $obj is not null (removeNullFromVariable) + if ($var_node->kind === ast\AST_DIM) { + $expr_node = $var_node; + do { + $parent_node = $expr_node; + $expr_node = $expr_node->children['expr']; + if (!($expr_node instanceof Node)) { + return $context; + } + } while ($expr_node->kind === ast\AST_DIM); + + if ($expr_node->kind === ast\AST_VAR) { + $var_name = $expr_node->children['name']; + if (!\is_string($var_name)) { + return $context; + } + if (!$context->getScope()->hasVariableWithName($var_name)) { + $new_type = Variable::getUnionTypeOfHardcodedVariableInScopeWithName($var_name, $context->isInGlobalScope()); + if (!$new_type || !$new_type->hasArrayLike()) { + $new_type = ArrayType::instance(false)->asPHPDocUnionType(); + } + $new_type = $new_type->nonFalseyClone(); + // Support analyzing cases such as `if (!empty($x['key'])) { use($x); }`, or `assert(!empty($x['key']))` + // (Assume that this is an array, not ArrayAccess or a string, as a heuristic) + $context->setScope($context->getScope()->withVariable(new Variable( + $context->withLineNumberStart($expr_node->lineno ?? 0), + $var_name, + $new_type, + 0 + ))); + return $context; + } + $context = $this->removeFalseyFromVariable($expr_node, $context, true); + + $variable = $context->getScope()->getVariableByName($var_name); + $var_node_union_type = $variable->getUnionType(); + + if ($var_node_union_type->hasTopLevelArrayShapeTypeInstances()) { + $context = $this->withNonFalseyArrayShapeTypes($variable, $parent_node->children['dim'], $context, true); + } + $this->context = $context; + } + } + return $this->context; + } + + /** + * @param Variable $variable the variable being modified by inferences from !empty + * @param Node|string|float|int|bool $dim_node represents the dimension being accessed. (E.g. can be a literal or an AST_CONST, etc. + * @param Context $context the context with inferences made prior to this condition + * + * @param bool $non_nullable if an offset is created, will it be non-nullable? + */ + private function withNonFalseyArrayShapeTypes(Variable $variable, $dim_node, Context $context, bool $non_nullable): Context + { + $dim_value = $dim_node instanceof Node ? (new ContextNode($this->code_base, $this->context, $dim_node))->getEquivalentPHPScalarValue() : $dim_node; + // TODO: detect and warn about null + if (!\is_scalar($dim_value)) { + return $context; + } + + $union_type = $variable->getUnionType(); + $dim_union_type = UnionTypeVisitor::resolveArrayShapeElementTypesForOffset($union_type, $dim_value); + if (!$dim_union_type) { + // There are other types, this dimension does not exist yet + if (!$union_type->hasTopLevelArrayShapeTypeInstances()) { + return $context; + } + $new_union_type = ArrayType::combineArrayShapeTypesWithField($union_type, $dim_value, MixedType::instance(false)->asPHPDocUnionType()); + $variable = clone($variable); + $variable->setUnionType($new_union_type); + return $context->withScopeVariable( + $variable + ); + // TODO finish + } elseif ($dim_union_type->containsNullableOrUndefined() || $dim_union_type->containsFalsey()) { + if (!$non_nullable) { + // The offset in question already exists in the array shape type, and we won't be changing it. + // (E.g. array_key_exists('key', $x) where $x is array{key:?int,other:string}) + return $context; + } + + $variable = clone($variable); + + $variable->setUnionType( + ArrayType::combineArrayShapeTypesWithField($union_type, $dim_value, $dim_union_type->nonFalseyClone()) + ); + + // Overwrite the variable with its new type in this + // scope without overwriting other scopes + return $context->withScopeVariable( + $variable + ); + // TODO finish + } + return $context; + } + + /** + * @param Node $node + * A node to parse + * (Should be useful when analyzing for loops with no breaks (`for (; !is_string($x); ){...}, in the future)) + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitExprList(Node $node): Context + { + $children = $node->children; + $count = \count($children); + if ($count > 1) { + foreach ($children as $sub_node) { + --$count; + if ($count > 0 && $sub_node instanceof Node) { + $this->checkVariablesDefined($sub_node); + } + } + } + // Only analyze the last expression in the expression list for (negation of) conditions. + $last_expression = \end($node->children); + if ($last_expression instanceof Node) { + return $this($last_expression); + } else { + // TODO: emit no-op warning + return $this->context; + } + } + + /** + * Useful for analyzing `if ($x = foo() && $x->method())` + * + * TODO: Convert $x to empty/false/null types + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitAssign(Node $node): Context + { + $context = (new BlockAnalysisVisitor($this->code_base, $this->context))->visitAssign($node); + $left = $node->children['var']; + if (!($left instanceof Node)) { + // Other code should warn about this invalid AST + return $context; + } + if ($left->kind === ast\AST_ARRAY) { + $expr_node = $node->children['expr']; + if ($expr_node instanceof Node) { + return (new self($this->code_base, $context))->__invoke($expr_node); + } + return $context; + } + return (new self($this->code_base, $context))->__invoke($left); + } + + /** + * Useful for analyzing `if ($x =& foo() && $x->method())` + * TODO: Convert $x to empty/false/null types + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitAssignRef(Node $node): Context + { + $context = (new BlockAnalysisVisitor($this->code_base, $this->context))->visitAssignRef($node); + $left = $node->children['var']; + if (!($left instanceof Node)) { + // Other code should warn about this invalid AST + return $context; + } + return (new self($this->code_base, $context))->__invoke($left); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ParameterTypesAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/ParameterTypesAnalyzer.php new file mode 100644 index 000000000..75e20e45a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ParameterTypesAnalyzer.php @@ -0,0 +1,1368 @@ +isFromPHPDoc()) { + self::analyzeRealSignatureCompatibility($code_base, $method, $minimum_target_php_version); + } + + // Look at each parameter to make sure their types + // are valid + $is_optional_seen = false; + foreach ($method->getParameterList() as $i => $parameter) { + if ($parameter->getFlags() & Parameter::PARAM_MODIFIER_VISIBILITY_FLAGS) { + if ($method instanceof Method && strcasecmp($method->getName(), '__construct') === 0) { + if (Config::get_closest_minimum_target_php_version_id() < 80000) { + Issue::maybeEmit( + $code_base, + $parameter->createContext($method), + Issue::CompatibleConstructorPropertyPromotion, + $parameter->getFileRef()->getLineNumberStart(), + $parameter, + $method->getRepresentationForIssue(true) + ); + } + } else { + // emit an InvalidNode warning for non-constructors (closures, global functions, other methods) + Issue::maybeEmit( + $code_base, + $parameter->createContext($method), + Issue::InvalidNode, + $parameter->getFileRef()->getLineNumberStart(), + "Cannot use visibility modifier on parameter $parameter of non-constructor " . $method->getRepresentationForIssue(true) + ); + } + } + $union_type = $parameter->getUnionType(); + + if ($parameter->isOptional()) { + $is_optional_seen = true; + } else { + if ($is_optional_seen) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::ParamReqAfterOpt, + $parameter->getFileRef()->getLineNumberStart(), + '(' . $parameter->toStubString() . ')', + '(' . $method->getParameterList()[$i - 1]->toStubString() . ')' + ); + } + } + + // Look at each type in the parameter's Union Type + foreach ($union_type->getReferencedClasses() as $outer_type => $type) { + // If it's a reference to self, its OK + if ($method instanceof Method && $type instanceof StaticOrSelfType) { + continue; + } + + if ($type instanceof TemplateType) { + if ($method instanceof Method) { + if ($method->isStatic() && !$method->declaresTemplateTypeInComment($type)) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::TemplateTypeStaticMethod, + $parameter->getFileRef()->getLineNumberStart(), + (string)$method->getFQSEN() + ); + } + } + } else { + // Make sure the class exists + $type_fqsen = FullyQualifiedClassName::fromType($type); + if (!$code_base->hasClassWithFQSEN($type_fqsen)) { + Issue::maybeEmitWithParameters( + $code_base, + $method->getContext(), + Issue::UndeclaredTypeParameter, + $parameter->getFileRef()->getLineNumberStart(), + [$parameter->getName(), (string)$outer_type], + IssueFixSuggester::suggestSimilarClass( + $code_base, + $method->getContext(), + $type_fqsen, + null, + IssueFixSuggester::DEFAULT_CLASS_SUGGESTION_PREFIX, + IssueFixSuggester::CLASS_SUGGEST_CLASSES_AND_TYPES + ) + ); + } elseif ($code_base->hasClassWithFQSEN($type_fqsen->withAlternateId(1))) { + UnionType::emitRedefinedClassReferenceWarning( + $code_base, + (clone($method->getContext()))->withLineNumberStart($parameter->getFileRef()->getLineNumberStart()), + $type_fqsen + ); + } + } + } + } + foreach ($method->getRealParameterList() as $parameter) { + if ($parameter->hasDefaultValue()) { + $default_node = $parameter->getDefaultValue(); + if ($default_node instanceof Node && + !$parameter->getUnionType()->containsNullableOrIsEmpty() && + $parameter->getDefaultValueType()->isNull()) { + // @phan-suppress-next-next-line PhanPartialTypeMismatchArgumentInternal + if (!($default_node->kind === ast\AST_CONST && + \strtolower($default_node->children['name']->children['name'] ?? '') === 'null')) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleDefaultEqualsNull, + $default_node->lineno, + ASTReverter::toShortString($default_node), + $parameter->getUnionType() . ' $' . $parameter->getName() + ); + } + } + } + $union_type = $parameter->getUnionType(); + + foreach ($union_type->getTypeSet() as $type) { + if (!$type->isObjectWithKnownFQSEN()) { + continue; + } + $type_fqsen = FullyQualifiedClassName::fromType($type); + if (!$code_base->hasClassWithFQSEN($type_fqsen)) { + // We should have already warned + continue; + } + $class = $code_base->getClassByFQSEN($type_fqsen); + if ($class->isTrait()) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::TypeInvalidTraitParam, + $parameter->getFileRef()->getLineNumberStart(), + $method->getNameForIssue(), + $parameter->getName(), + $type_fqsen->__toString() + ); + } + } + } + + if ($method instanceof Method) { + if ($method->getName() === '__construct') { + $class = $method->getClass($code_base); + if ($class->isGeneric()) { + $class->hydrate($code_base); + // Call this to emit any warnings about missing template params + $class->getGenericConstructorBuilder($code_base); + } + } + self::analyzeOverrideSignature($code_base, $method); + } + } + + /** + * Precondition: $minimum_target_php_version < 70200 + */ + private static function analyzeRealSignatureCompatibility(CodeBase $code_base, FunctionInterface $method, int $minimum_target_php_version): void + { + $php70_checks = $minimum_target_php_version < 70100; + + foreach ($method->getRealParameterList() as $real_parameter) { + foreach ($real_parameter->getUnionType()->getTypeSet() as $type) { + $type_class = \get_class($type); + if ($php70_checks) { + if ($type->isNullableLabeled()) { + if ($real_parameter->isUsingNullableSyntax()) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleNullableTypePHP70, + $real_parameter->getFileRef()->getLineNumberStart(), + (string)$type + ); + } + } + if ($type_class === IterableType::class) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleIterableTypePHP70, + $real_parameter->getFileRef()->getLineNumberStart(), + (string)$type + ); + continue; + } + if ($minimum_target_php_version < 70000 && $type instanceof ScalarType) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleScalarTypePHP56, + $real_parameter->getFileRef()->getLineNumberStart(), + (string)$type + ); + } + } + if ($type_class === ObjectType::class) { + if ($minimum_target_php_version < 70200) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleObjectTypePHP71, + $real_parameter->getFileRef()->getLineNumberStart(), + (string)$type + ); + } + } elseif ($type_class === MixedType::class) { + if ($minimum_target_php_version < 80000) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleMixedType, + $real_parameter->getFileRef()->getLineNumberStart(), + (string)$type + ); + } + } + } + } + foreach ($method->getRealReturnType()->getTypeSet() as $type) { + $type_class = \get_class($type); + if ($php70_checks) { + if ($minimum_target_php_version < 70000) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleAnyReturnTypePHP56, + $method->getFileRef()->getLineNumberStart(), + (string)$method->getRealReturnType() + ); + } + // Could check for use statements, but `php7.1 -l path/to/file.php` would do that already. + if ($minimum_target_php_version < 70100) { + if ($type_class === VoidType::class) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleVoidTypePHP70, + $method->getFileRef()->getLineNumberStart(), + (string)$type + ); + } else { + if ($type->isNullableLabeled()) { + // Don't emit CompatibleNullableTypePHP70 for `void`. + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleNullableTypePHP70, + $method->getFileRef()->getLineNumberStart(), + (string)$type + ); + } + if ($type_class === IterableType::class) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleIterableTypePHP70, + $method->getFileRef()->getLineNumberStart(), + (string)$type + ); + continue; + } + if ($minimum_target_php_version < 70000 && $type instanceof ScalarType) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleScalarTypePHP56, + $method->getFileRef()->getLineNumberStart(), + (string)$type + ); + } + } + } + } + if ($type_class === ObjectType::class) { + if ($minimum_target_php_version < 70200) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleObjectTypePHP71, + $method->getFileRef()->getLineNumberStart(), + (string)$type + ); + } + } elseif ($type_class === MixedType::class) { + if ($minimum_target_php_version < 80000) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CompatibleMixedType, + $method->getFileRef()->getLineNumberStart(), + (string)$type + ); + } + } + } + } + + private static function checkCommentParametersAreInOrder(CodeBase $code_base, FunctionInterface $method): void + { + $comment = $method->getComment(); + if ($comment === null) { + return; + } + $parameter_map = $comment->getParameterMap(); + if (\count($parameter_map) < 2) { + // There have to be at least two comment parameters for the parameters to be out of order + return; + } + $prev_index = -1; + $prev_name = -1; + $comment_parameter_map = $comment->getParameterMap(); + $expected_parameter_order = \array_flip(\array_keys($comment_parameter_map)); + foreach ($method->getParameterList() as $parameter) { + $parameter_name = $parameter->getName(); + $parameter_index_in_comment = $expected_parameter_order[$parameter_name] ?? null; + if ($parameter_index_in_comment === null) { + continue; + } + if ($parameter_index_in_comment < $prev_index) { + $comment_param = $comment_parameter_map[$parameter_name] ?? null; + $line = $comment_param ? $comment_param->getLineno() : $method->getFileRef()->getLineNumberStart(); + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CommentParamOutOfOrder, + $line, + $prev_name, + $parameter_name + ); + return; + } + $prev_name = $parameter_name; + $prev_index = $parameter_index_in_comment; + } + } + + /** + * Make sure signatures line up between methods and the + * methods they override + * + * @see https://en.wikipedia.org/wiki/Liskov_substitution_principle + */ + private static function analyzeOverrideSignature( + CodeBase $code_base, + Method $method + ): void { + if (!Config::getValue('analyze_signature_compatibility')) { + return; + } + + // Hydrate the class this method is coming from in + // order to understand if it's an override or not + $class = $method->getClass($code_base); + $class->hydrate($code_base); + + // Check to see if the method is an override + // $method->analyzeOverride($code_base); + + // Make sure we're actually overriding something + // TODO(in another PR): check that signatures of magic methods are valid, if not done already (e.g. __get expects one param, most can't define return types, etc.)? + $is_actually_override = $method->isOverride(); + + if (!$is_actually_override && $method->isOverrideIntended()) { + self::analyzeOverrideComment($code_base, $method); + } + + if (!$is_actually_override) { + return; + } + + // Get the method(s) that are being overridden + // E.g. if the subclass, the parent class, and an interface the subclass implements implement a method, + // then this has to check two different overrides (Subclass overriding parent class, and subclass overriding abstract method in interface) + try { + $o_method_list = $method->getOverriddenMethods($code_base); + } catch (CodeBaseException $_) { + if (strcasecmp($method->getDefiningFQSEN()->getName(), $method->getFQSEN()->getName()) !== 0) { + // Give up, this is probably a renamed trait method that overrides another trait method. + return; + } + // TODO: Remove if no edge cases are seen. + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::UnanalyzableInheritance, + $method->getFileRef()->getLineNumberStart(), + $method->getFQSEN() + ); + return; + } + foreach ($o_method_list as $o_method) { + self::analyzeOverrideSignatureForOverriddenMethod($code_base, $method, $class, $o_method); + } + } + + private static function analyzeOverrideComment(CodeBase $code_base, Method $method): void + { + if ($method->isMagic()) { + return; + } + // Only emit this issue on the base class, not for the subclass which inherited it + if ($method->getDefiningFQSEN() !== $method->getFQSEN()) { + return; + } + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::CommentOverrideOnNonOverrideMethod, + $method->getFileRef()->getLineNumberStart(), + $method->getFQSEN() + ); + } + + /** + * Make sure signatures line up between methods and a method it overrides. + * + * @see https://en.wikipedia.org/wiki/Liskov_substitution_principle + * + * @param CodeBase $code_base + * @param Method $method the overriding method. + * @param Clazz $class the subclass where the overrides take place. + * @param Method $o_method the overridden method. + */ + private static function analyzeOverrideSignatureForOverriddenMethod( + CodeBase $code_base, + Method $method, + Clazz $class, + Method $o_method + ): void { + if ($o_method->isFinal()) { + // Even if it is a constructor, verify that a method doesn't override a final method. + // TODO: different warning for trait (#1126) + self::warnOverridingFinalMethod($code_base, $method, $class, $o_method); + } + + $construct_access_signature_mismatch_thrown = false; + if ($method->getName() === '__construct') { + // flip the switch on so we don't throw both ConstructAccessSignatureMismatch now and AccessSignatureMismatch later + $construct_access_signature_mismatch_thrown = Config::get_closest_minimum_target_php_version_id() < 70200 && !$o_method->getPhanFlagsHasState(Flags::IS_FAKE_CONSTRUCTOR) && $o_method->isStrictlyMoreVisibleThan($method); + + if ($construct_access_signature_mismatch_thrown) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::ConstructAccessSignatureMismatch, + $method->getFileRef()->getLineNumberStart(), + $method, + $o_method, + $o_method->getFileRef()->getFile(), + $o_method->getFileRef()->getLineNumberStart() + ); + } + + if (!$o_method->isAbstract()) { + return; + } + } + + // Don't bother warning about incompatible signatures for private methods. + // (But it is an error to override a private final method) + if ($o_method->isPrivate()) { + return; + } + // Inherit (at)phan-pure annotations by default. + if ($o_method->isPure()) { + $method->setIsPure(); + } + + // Get the class that the overridden method lives on + $o_class = $o_method->getClass($code_base); + + // A lot of analyzeOverrideRealSignature is redundant. + // However, phan should consistently emit both issue types if one of them is suppressed. + self::analyzeOverrideRealSignature($code_base, $method, $class, $o_method, $o_class); + + // Phan needs to complain in some cases, such as a trait existing for an abstract method defined in the class. + // PHP also checks if a trait redefines a method in the class. + if ($o_class->isTrait() && $method->getDefiningFQSEN()->getFullyQualifiedClassName() === $class->getFQSEN()) { + // Give up on analyzing if the class **directly** overrides any (abstract OR non-abstract) method defined by the trait + // TODO: Fix edge cases caused by hack changing FQSEN of private methods + return; + } + $mismatch_details = ''; + + // Get the parameters for that method + $o_parameter_list = $o_method->getParameterList(); + + // If we have a parent type defined, map the method's + // return type and parameter types through it + $type_option = $class->getParentTypeOption(); + + // Map overridden method parameter types through any + // template type parameters we may have + if ($type_option->isDefined()) { + $o_parameter_list = + \array_map(static function (Parameter $parameter) use ($type_option, $code_base): Parameter { + + if (!$parameter->getUnionType()->hasTemplateTypeRecursive()) { + return $parameter; + } + + $mapped_parameter = clone($parameter); + + $mapped_parameter->setUnionType( + $mapped_parameter->getUnionType()->withTemplateParameterTypeMap( + $type_option->get()->getTemplateParameterTypeMap( + $code_base + ) + ) + ); + + return $mapped_parameter; + }, $o_parameter_list); + } + + // Map overridden method return type through any template + // type parameters we may have + $o_return_union_type = $o_method->getUnionType(); + if ($type_option->isDefined() + && $o_return_union_type->hasTemplateTypeRecursive() + ) { + $o_return_union_type = + $o_return_union_type->withTemplateParameterTypeMap( + $type_option->get()->getTemplateParameterTypeMap( + $code_base + ) + ); + } + + // Determine if the signatures match up + $signatures_match = true; + + // Make sure the count of parameters matches + if ($method->getNumberOfRequiredParameters() + > $o_method->getNumberOfRequiredParameters() + ) { + $signatures_match = false; + $mismatch_details = 'Saw more required parameters in the override'; + } elseif ($method->getNumberOfParameters() + < $o_method->getNumberOfParameters() + ) { + $signatures_match = false; + $mismatch_details = 'Saw fewer optional parameters in the override'; + + // If parameter counts match, check their types + } else { + $real_parameter_list = $method->getRealParameterList(); + $o_real_parameter_list = $o_method->getRealParameterList(); + + foreach ($method->getParameterList() as $i => $parameter) { + if (!isset($o_parameter_list[$i])) { + continue; + } + + $o_parameter = $o_parameter_list[$i]; + + // Changing pass by reference is not ok + // @see https://3v4l.org/Utuo8 + if ($parameter->isPassByReference() != $o_parameter->isPassByReference()) { + $signatures_match = false; + $mismatch_details = "Difference in passing by reference in override $parameter of parameter $o_parameter"; + break; + } + + // Variadic parameters must match up. + if ($o_parameter->isVariadic() !== $parameter->isVariadic()) { + $signatures_match = false; + $mismatch_details = "Difference in being variadic in override $parameter of parameter $o_parameter"; + break; + } + + // Check for the presence of real types first, warn if the override has a type but the original doesn't. + $o_real_parameter = $o_real_parameter_list[$i] ?? null; + $real_parameter = $real_parameter_list[$i] ?? null; + if ($o_real_parameter !== null && $real_parameter !== null && !$real_parameter->getUnionType()->isEmptyOrMixed() && $o_real_parameter->getUnionType()->isEmptyOrMixed() + && (!$method->isFromPHPDoc() || $parameter->getUnionType()->isEmptyOrMixed())) { + $signatures_match = false; + $mismatch_details = "Cannot use $parameter with a real type to override parameter $o_parameter without a real type"; + break; + } + + if ($parameter->getUnionType()->isEmptyOrMixed()) { + // parameter type widening is allowed + continue; + } + + if ($o_parameter->getUnionType()->isEmptyOrMixed()) { + // XXX Not sure why this check was here but there are better checks elsewhere for real mismatches + continue; + } + + // In php 7.2, it's ok to have a more relaxed type on an overriding method. + // In earlier versions it isn't. + // Because this check is analyzing phpdoc types, so it's fine for php < 7.2 as well. Use `PhanParamSignatureRealMismatch*` for detecting **real** mismatches. + // + // https://3v4l.org/XTm3P + + // If we have types, make sure they line up + // + // TODO: should we be expanding the types on $o_parameter + // via ->asExpandedTypes($code_base)? + // + // @see https://3v4l.org/ke3kp + if (!self::canWeakCast($code_base, $o_parameter->getUnionType(), $parameter->getUnionType())) { + $signatures_match = false; + $mismatch_details = "Expected $parameter to have the same type as $o_parameter or a supertype"; + break; + } + } + } + + // Return types should be mappable for LSP + // Note: PHP requires return types to be identical + // The return type should be stricter than or identical to the overridden union type. + // E.g. there is no issue if the overridden return type is empty. + // See https://github.com/phan/phan/issues/1397 + if (!$o_return_union_type->isEmptyOrMixed()) { + if (!$method->getUnionType()->asExpandedTypes($code_base)->canCastToUnionType( + $o_return_union_type + )) { + $signatures_match = false; + } + } + + // Static or non-static should match + if ($method->isStatic() != $o_method->isStatic()) { + if ($o_method->isStatic()) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::AccessStaticToNonStatic, + $method->getFileRef()->getLineNumberStart(), + $o_method->getFQSEN() + ); + } else { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::AccessNonStaticToStatic, + $method->getFileRef()->getLineNumberStart(), + $o_method->getFQSEN() + ); + } + } + + + if ($o_method->returnsRef() && !$method->returnsRef()) { + $signatures_match = false; + } + + if (!$signatures_match) { + if ($o_method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::ParamSignatureMismatchInternal, + $method->getFileRef()->getLineNumberStart(), + $method, + $o_method, + $mismatch_details !== '' ? " ($mismatch_details)" : '' + ); + } else { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::ParamSignatureMismatch, + $method->getFileRef()->getLineNumberStart(), + $method, + $o_method, + $o_method->getFileRef()->getFile(), + $o_method->getFileRef()->getLineNumberStart(), + $mismatch_details !== '' ? " ($mismatch_details)" : '' + ); + } + } + + // Access must be compatible + if (!$construct_access_signature_mismatch_thrown && $o_method->isStrictlyMoreVisibleThan($method)) { + if ($o_method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::AccessSignatureMismatchInternal, + $method->getFileRef()->getLineNumberStart(), + $method, + $o_method + ); + } else { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::AccessSignatureMismatch, + $method->getFileRef()->getLineNumberStart(), + $method, + $o_method, + $o_method->getFileRef()->getFile(), + $o_method->getFileRef()->getLineNumberStart() + ); + } + } + } + + private static function canWeakCast(CodeBase $code_base, UnionType $overridden_type, UnionType $type): bool + { + $expanded_overridden_type = $overridden_type->asExpandedTypes($code_base); + return $expanded_overridden_type->canCastToUnionType($type) && + $expanded_overridden_type->hasAnyTypeOverlap($code_base, $type); + } + /** + * Previously, Phan bases the analysis off of phpdoc. + * Keeping that around(e.g. to check that string[] is compatible with string[]) + * and also checking the **real**(non-phpdoc) types. + * + * @param $code_base + * @param $method - The overriding method + * @param $o_method - The overridden method. E.g. if a subclass overrid a base class implementation, then $o_method would be from the base class. + * @param $o_class the overridden class + */ + private static function analyzeOverrideRealSignature( + CodeBase $code_base, + Method $method, + Clazz $class, + Method $o_method, + Clazz $o_class + ): void { + if ($o_class->isTrait() && $method->getDefiningFQSEN()->getFullyQualifiedClassName() === $class->getFQSEN()) { + // Give up on analyzing if the class **directly** overrides any (abstract OR non-abstract) method defined by the trait + // TODO: Fix edge cases caused by hack changing FQSEN of private methods + return; + } + + // Get the parameters for that method + // NOTE: If the overriding method is from an (at)method tag, then compare the phpdoc types instead here to emit FromPHPDoc issue equivalents. + // TODO: Track magic and real methods separately so that subclasses of subclasses get properly analyzed + $o_parameter_list = $method->isFromPHPDoc() ? $o_method->getParameterList() : $o_method->getRealParameterList(); + + // Make sure the count of parameters matches + if ($method->getNumberOfRequiredRealParameters() + > $o_method->getNumberOfRequiredRealParameters() + ) { + self::emitSignatureRealMismatchIssue( + $code_base, + $method, + $o_method, + Issue::ParamSignatureRealMismatchTooManyRequiredParameters, + Issue::ParamSignatureRealMismatchTooManyRequiredParametersInternal, + Issue::ParamSignaturePHPDocMismatchTooManyRequiredParameters, + null, + $method->getNumberOfRequiredRealParameters(), + $o_method->getNumberOfRequiredRealParameters() + ); + return; + } elseif ($method->getNumberOfRealParameters() + < $o_method->getNumberOfRealParameters() + ) { + self::emitSignatureRealMismatchIssue( + $code_base, + $method, + $o_method, + Issue::ParamSignatureRealMismatchTooFewParameters, + Issue::ParamSignatureRealMismatchTooFewParametersInternal, + Issue::ParamSignaturePHPDocMismatchTooFewParameters, + null, + $method->getNumberOfRealParameters(), + $o_method->getNumberOfRealParameters() + ); + return; + // If parameter counts match, check their types + } + $is_possibly_compatible = true; + + // TODO: Stricter checks for parameter types when this is a magic method? + // - If the overriding method is magic, then compare the magic method phpdoc types against the phpdoc+real types of the parent + foreach ($method->isFromPHPDoc() ? $method->getParameterList() : $method->getRealParameterList() as $i => $parameter) { + $offset = $i + 1; + // TODO: check if variadic + if (!isset($o_parameter_list[$i])) { + continue; + } + + // TODO: check that the variadic types match up? + $o_parameter = $o_parameter_list[$i]; + + // Changing pass by reference is not ok + // @see https://3v4l.org/Utuo8 + if ($parameter->isPassByReference() != $o_parameter->isPassByReference()) { + $is_reference = $parameter->isPassByReference(); + self::emitSignatureRealMismatchIssue( + $code_base, + $method, + $o_method, + ($is_reference ? Issue::ParamSignatureRealMismatchParamIsReference : Issue::ParamSignatureRealMismatchParamIsNotReference), + ($is_reference ? Issue::ParamSignatureRealMismatchParamIsReferenceInternal : Issue::ParamSignatureRealMismatchParamIsNotReferenceInternal), + ($is_reference ? Issue::ParamSignaturePHPDocMismatchParamIsReference : Issue::ParamSignaturePHPDocMismatchParamIsNotReference), + self::guessCommentParamLineNumber($method, $parameter), + $offset + ); + return; + } + + // Changing variadic to/from non-variadic is not ok? + // (Not absolutely sure about that) + if ($parameter->isVariadic() != $o_parameter->isVariadic()) { + $is_variadic = $parameter->isVariadic(); + self::emitSignatureRealMismatchIssue( + $code_base, + $method, + $o_method, + ($is_variadic ? Issue::ParamSignatureRealMismatchParamVariadic : Issue::ParamSignatureRealMismatchParamNotVariadic), + ($is_variadic ? Issue::ParamSignatureRealMismatchParamVariadicInternal : Issue::ParamSignatureRealMismatchParamNotVariadicInternal), + ($is_variadic ? Issue::ParamSignaturePHPDocMismatchParamVariadic : Issue::ParamSignaturePHPDocMismatchParamNotVariadic), + self::guessCommentParamLineNumber($method, $parameter), + $offset + ); + return; + } + + // Either 0 or both of the params must have types for the signatures to be compatible. + $o_parameter_union_type = $o_parameter->getUnionType(); + $parameter_union_type = $parameter->getUnionType(); + // Mixed and empty parameter types are interchangeable in php 8 + if ($parameter_union_type->isEmptyOrMixed() != $o_parameter_union_type->isEmptyOrMixed()) { + if ($parameter_union_type->isEmptyOrMixed()) { + // Don't warn about mixed + if (Config::getValue('allow_method_param_type_widening') === false) { + $is_possibly_compatible = false; + self::emitSignatureRealMismatchIssue( + $code_base, + $method, + $o_method, + Issue::ParamSignatureRealMismatchHasNoParamType, + Issue::ParamSignatureRealMismatchHasNoParamTypeInternal, + Issue::ParamSignaturePHPDocMismatchHasNoParamType, + self::guessCommentParamLineNumber($method, $parameter), + $offset, + (string)$o_parameter_union_type + ); + } + continue; + } else { + $is_possibly_compatible = false; + self::emitSignatureRealMismatchIssue( + $code_base, + $method, + $o_method, + Issue::ParamSignatureRealMismatchHasParamType, + Issue::ParamSignatureRealMismatchHasParamTypeInternal, + Issue::ParamSignaturePHPDocMismatchHasParamType, + self::guessCommentParamLineNumber($method, $parameter), + $offset, + (string)$parameter_union_type + ); + continue; + } + } + + // If both have types, make sure they are identical. + // Non-nullable param types can be substituted with the nullable equivalents. + // E.g. A::foo(?int $x) can override BaseClass::foo(int $x) + if (!$parameter_union_type->isEmptyOrMixed()) { + if (!$o_parameter_union_type->isEqualTo($parameter_union_type) && + !($parameter_union_type->containsNullable() && $o_parameter_union_type->isEqualTo($parameter_union_type->nonNullableClone())) + ) { + // There is one exception to this in php 7.1 - the pseudo-type "iterable" can replace ArrayAccess/array in a subclass + // TODO: Traversable and array work, but Iterator doesn't. Check for those specific cases? + $is_exception_to_rule = $parameter_union_type->hasIterable() && + $o_parameter_union_type->hasIterable() && + ($parameter_union_type->hasType(IterableType::instance(true)) || + $parameter_union_type->hasType(IterableType::instance(false)) && !$o_parameter_union_type->containsNullable()); + + if (!$is_exception_to_rule) { + $is_possibly_compatible = false; + self::emitSignatureRealMismatchIssue( + $code_base, + $method, + $o_method, + Issue::ParamSignatureRealMismatchParamType, + Issue::ParamSignatureRealMismatchParamTypeInternal, + Issue::ParamSignaturePHPDocMismatchParamType, + self::guessCommentParamLineNumber($method, $parameter), + $offset, + (string)$parameter_union_type, + (string)$o_parameter_union_type + ); + continue; + } + } + } + } + + $o_return_union_type = $o_method->getRealReturnType(); + + $return_union_type = $method->isFromPHPDoc() ? $method->getUnionType() : $method->getRealReturnType(); + // If the parent has a return type, then return types should be equal. + // A non-nullable return type can override a nullable return type of the same type. + // Be sure to handle `void`, which contains nullable types + if (!$o_return_union_type->isEmpty()) { + if (!($o_return_union_type->isEqualTo($return_union_type) || ( + ($o_return_union_type->containsNullable() && !$o_return_union_type->isNull()) && ($o_return_union_type->nonNullableClone()->isEqualTo($return_union_type))) + )) { + // There is one exception to this in php 7.1 - the pseudo-type "iterable" can replace ArrayAccess/array in a subclass + // TODO: Traversable and array work, but Iterator doesn't. Check for those specific cases? + $is_exception_to_rule = $return_union_type->hasIterable() && + $o_return_union_type->hasIterable() && + ($o_return_union_type->hasType(IterableType::instance(true)) || + $o_return_union_type->hasType(IterableType::instance(false)) && !$return_union_type->containsNullable()); + if (!$is_exception_to_rule) { + $is_possibly_compatible = false; + + self::emitSignatureRealMismatchIssue( + $code_base, + $method, + $o_method, + Issue::ParamSignatureRealMismatchReturnType, + Issue::ParamSignatureRealMismatchReturnTypeInternal, + Issue::ParamSignaturePHPDocMismatchReturnType, + null, + (string)$return_union_type, + (string)$o_return_union_type + ); + } + } + } + if ($is_possibly_compatible) { + if (Config::getValue('inherit_phpdoc_types')) { + self::inheritPHPDoc($code_base, $method, $o_method); + } + } + } + + /** + * @return array + */ + private static function extractCommentParameterMap(Method $method): array + { + $comment = $method->getComment(); + return $comment ? $comment->getParameterMap() : []; + } + /** + * Inherit any missing phpdoc types for (at)return and (at)param of $method from $o_method. + * This is the default behavior, see https://www.phpdoc.org/docs/latest/guides/inheritance.html + */ + private static function inheritPHPDoc( + CodeBase $code_base, + Method $method, + Method $o_method + ): void { + // The method was already from phpdoc. + if ($method->isFromPHPDoc()) { + return; + } + // Get the parameters for that method + $phpdoc_parameter_list = $method->getParameterList(); + $o_phpdoc_parameter_list = $o_method->getParameterList(); + $comment_parameter_map = null; + foreach ($phpdoc_parameter_list as $i => $parameter) { + $parameter_type = $parameter->getNonVariadicUnionType(); + // If there is already a phpdoc parameter type, then don't bother inheriting the parameter type from $o_method + if (!$parameter_type->isEmpty()) { + $comment_parameter_map = $comment_parameter_map ?? self::extractCommentParameterMap($method); + $comment_parameter = $comment_parameter_map[$parameter->getName()] ?? null; + if ($comment_parameter) { + $comment_parameter_type = $comment_parameter->getUnionType(); + if (!$comment_parameter_type->isEmpty()) { + continue; + } + } + } + $parent_parameter = $o_phpdoc_parameter_list[$i] ?? null; + if ($parent_parameter) { + $parent_parameter_type = $parent_parameter->getNonVariadicUnionType(); + if ($parent_parameter_type->isEmpty()) { + continue; + } + if ($parameter_type->isEmpty() || $parent_parameter_type->isExclusivelyNarrowedFormOf($code_base, $parameter_type)) { + $parameter->setUnionType($parent_parameter_type->eraseRealTypeSetRecursively()); + } + } + } + + $parent_phpdoc_return_type = $o_method->getUnionType(); + if (!$parent_phpdoc_return_type->isEmpty()) { + $phpdoc_return_type = $method->getUnionType(); + if ($phpdoc_return_type->isEmpty()) { + $method->setUnionType($parent_phpdoc_return_type); + } else { + self::maybeInheritCommentReturnType($code_base, $method, $parent_phpdoc_return_type); + } + } + } + + /** + * @param Method $method a method which has a union type, but is permitted to inherit a more specific type. + * @param UnionType $inherited_union_type a non-empty union type + */ + private static function maybeInheritCommentReturnType(CodeBase $code_base, Method $method, UnionType $inherited_union_type): void + { + $comment = $method->getComment(); + if ($comment && $comment->hasReturnUnionType()) { + if (!$comment->getReturnType()->isEmpty()) { + // This comment explicitly specified the desired return type. + // Give up on inheriting + return; + } + } + if ($inherited_union_type->isExclusivelyNarrowedFormOf($code_base, $method->getUnionType())) { + $method->setUnionType($inherited_union_type); + } + } + + /** + * Emit an $issue_type instance corresponding to a potential runtime inheritance warning/error + * + * @param CodeBase $code_base + * @param Method $method + * @param Method $o_method the overridden method + * @param string $issue_type the ParamSignatureRealMismatch* (issue type if overriding user-defined method) + * @param string $internal_issue_type the ParamSignatureRealMismatch* (issue type if overriding internal method) + * @param string $phpdoc_issue_type the ParamSignaturePHPDocMismatch* (issue type if overriding internal method) + * @param ?int $lineno + * @param int|string ...$args + */ + private static function emitSignatureRealMismatchIssue( + CodeBase $code_base, + Method $method, + Method $o_method, + string $issue_type, + string $internal_issue_type, + string $phpdoc_issue_type, + ?int $lineno, + ...$args + ): void { + if ($method->isFromPHPDoc() || $o_method->isFromPHPDoc()) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + $phpdoc_issue_type, + $lineno ?? $method->getFileRef()->getLineNumberStart(), + $method->toRealSignatureString(), + $o_method->toRealSignatureString(), + ...array_merge($args, [ + $o_method->getFileRef()->getFile(), + $o_method->getFileRef()->getLineNumberStart(), + ]) + ); + } elseif ($o_method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + $internal_issue_type, + $lineno ?? $method->getFileRef()->getLineNumberStart(), + $method->toRealSignatureString(), + $o_method->toRealSignatureString(), + ...$args + ); + } else { + Issue::maybeEmit( + $code_base, + $method->getContext(), + $issue_type, + $lineno ?? $method->getFileRef()->getLineNumberStart(), + $method->toRealSignatureString(), + $o_method->toRealSignatureString(), + ...array_merge($args, [ + $o_method->getFileRef()->getFile(), + $o_method->getFileRef()->getLineNumberStart(), + ]) + ); + } + } + + private static function analyzeParameterTypesDocblockSignaturesMatch( + CodeBase $code_base, + FunctionInterface $method + ): void { + $phpdoc_parameter_map = $method->getPHPDocParameterTypeMap(); + if (\count($phpdoc_parameter_map) === 0) { + // nothing to check. + return; + } + $real_parameter_list = $method->getRealParameterList(); + foreach ($real_parameter_list as $i => $parameter) { + $real_param_type = $parameter->getNonVariadicUnionType(); + if ($real_param_type->isEmpty()) { + continue; + } + $phpdoc_param_union_type = $phpdoc_parameter_map[$parameter->getName()] ?? null; + if ($phpdoc_param_union_type && !$phpdoc_param_union_type->isEmpty()) { + self::tryToAssignPHPDocTypeToParameter($code_base, $method, $i, $parameter, $real_param_type, $phpdoc_param_union_type); + } + } + self::recordOutputReferences($method); + } + + private static function tryToAssignPHPDocTypeToParameter( + CodeBase $code_base, + FunctionInterface $method, + int $i, + Parameter $parameter, + UnionType $real_param_type, + UnionType $phpdoc_param_union_type + ): void { + $context = $method->getContext(); + $resolved_real_param_type = $real_param_type->withStaticResolvedInContext($context); + $is_exclusively_narrowed = true; + foreach ($phpdoc_param_union_type->getTypeSet() as $phpdoc_type) { + // Make sure that the commented type is a narrowed + // or equivalent form of the syntax-level declared + // return type. + if (!$phpdoc_type->isExclusivelyNarrowedFormOrEquivalentTo( + $resolved_real_param_type, + $context, + $code_base + ) + ) { + $is_exclusively_narrowed = false; + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeMismatchDeclaredParam, + self::guessCommentParamLineNumber($method, $parameter) ?: $context->getLineNumberStart(), + $parameter->getName(), + $method->getName(), + $phpdoc_type->__toString(), + $real_param_type->__toString() + ); + } + } + // TODO: test edge cases of variadic signatures + if ($is_exclusively_narrowed && Config::getValue('prefer_narrowed_phpdoc_param_type')) { + $normalized_phpdoc_param_union_type = self::normalizeNarrowedParamType($phpdoc_param_union_type, $real_param_type); + if ($normalized_phpdoc_param_union_type) { + $param_to_modify = $method->getParameterList()[$i] ?? null; + if ($param_to_modify) { + // TODO: Maybe have two different sets of methods for setUnionType and setCallerUnionType, this is easy to mix up for variadics. + $param_to_modify->setUnionType($normalized_phpdoc_param_union_type->withRealTypeSet($real_param_type->getRealTypeSet())); + } + } else { + $comment = $method->getComment(); + if ($comment === null) { + return; + } + // This check isn't urgent to fix, and is specific to nullable casting rules, + // so use a different issue type. + $param_name = $parameter->getName(); + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeMismatchDeclaredParamNullable, + self::guessCommentParamLineNumber($method, $parameter) ?: $context->getLineNumberStart(), + $param_name, + $method->getName(), + $phpdoc_param_union_type->__toString(), + $real_param_type->__toString() + ); + } + } + } + + private static function guessCommentParamLineNumber(FunctionInterface $method, Parameter $param): ?int + { + $comment = $method->getComment(); + if ($comment === null) { + return null; + } + $parameter_map = $comment->getParameterMap(); + $comment_param = $parameter_map[$param->getName()] ?? null; + if (!$comment_param) { + return null; + } + return $comment_param->getLineno(); + } + + /** + * Guesses the return number of a method's PHPDoc's (at)return statement. + * Returns null if that could not be found. + * @internal + */ + public static function guessCommentReturnLineNumber(FunctionInterface $method): ?int + { + $comment = $method->getComment(); + if ($comment === null) { + return null; + } + if (!$comment->hasReturnUnionType()) { + return null; + } + return $comment->getReturnLineno(); + } + + private static function recordOutputReferences(FunctionInterface $method): void + { + foreach ($method->getOutputReferenceParamNames() as $output_param_name) { + foreach ($method->getRealParameterList() as $parameter) { + // TODO: Emit an issue if the (at)phan-output-reference is on a non-reference (at)param? + if ($parameter->getName() === $output_param_name && $parameter->isPassByReference()) { + $parameter->setIsOutputReference(); + } + } + } + } + + /** + * Forbid these two types of narrowing: + * 1. Forbid inferring a type of null from "(at)param null $x" for foo(?int $x = null) + * The phpdoc is probably nonsense. + * 2. Forbid inferring a type of `T` from "(at)param T $x" for foo(?T $x = null) + * The phpdoc is probably shorthand. + * + * Annotations may be added in the future to support this, e.g. "(at)param T $x (at)phan-not-null" + * + * @return ?UnionType + * - normalized version of $phpdoc_param_union_type (possibly same object) + * if Phan should proceed using phpdoc type instead of real types. (Converting T|null to ?T) + * - null if the type is an invalid narrowing, and Phan should warn. + */ + public static function normalizeNarrowedParamType(UnionType $phpdoc_param_union_type, UnionType $real_param_type): ?UnionType + { + // "@param null $x" is almost always a mistake. Forbid it for now. + // But allow "@param T|null $x" + $has_null = $phpdoc_param_union_type->hasType(NullType::instance(false)); + if ($has_null && $phpdoc_param_union_type->typeCount() === 1) { + // "@param null" + return null; + } + if (!$real_param_type->containsNullable() || $phpdoc_param_union_type->containsNullable()) { + // We already validated that the other casts were supported. + return $phpdoc_param_union_type; + } + if (!$has_null) { + // Attempting to narrow nullable to non-nullable is usually a mistake, currently not supported. + return null; + } + // Create a clone, converting "T|S|null" to "?T|?S" + return $phpdoc_param_union_type->nullableClone()->withoutType(NullType::instance(false)); + } + + /** + * Warns if a method is overriding a final method + */ + private static function warnOverridingFinalMethod(CodeBase $code_base, Method $method, Clazz $class, Method $o_method): void + { + if ($method->isFromPHPDoc()) { + // TODO: Track phpdoc methods separately from real methods + if ($class->checkHasSuppressIssueAndIncrementCount(Issue::AccessOverridesFinalMethodPHPDoc)) { + return; + } + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::AccessOverridesFinalMethodPHPDoc, + $method->getFileRef()->getLineNumberStart(), + $method->getFQSEN(), + $o_method->getFQSEN(), + $o_method->getFileRef()->getFile(), + $o_method->getFileRef()->getLineNumberStart() + ); + } elseif ($o_method->isPHPInternal()) { + Issue::maybeEmit( + $code_base, + $method->getContext(), + Issue::AccessOverridesFinalMethodInternal, + $method->getFileRef()->getLineNumberStart(), + $method->getFQSEN(), + $o_method->getFQSEN() + ); + } else { + $issue_type = Issue::AccessOverridesFinalMethod; + + try { + $o_clazz = $o_method->getDefiningClass($code_base); + if ($o_clazz->isTrait()) { + $issue_type = Issue::AccessOverridesFinalMethodInTrait; + } + } catch (CodeBaseException $_) { + } + + Issue::maybeEmit( + $code_base, + $method->getContext(), + $issue_type, + $method->getFileRef()->getLineNumberStart(), + $method->getFQSEN(), + $o_method->getFQSEN(), + $o_method->getFileRef()->getFile(), + $o_method->getFileRef()->getLineNumberStart() + ); + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ParentConstructorCalledAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/ParentConstructorCalledAnalyzer.php new file mode 100644 index 000000000..112993625 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ParentConstructorCalledAnalyzer.php @@ -0,0 +1,70 @@ +getName(), + Config::getValue('parent_constructor_required'), + true + )) { + return; + } + + // Don't worry about internal classes + if ($clazz->isPHPInternal()) { + return; + } + + // Don't worry if there's no parent class + if (!$clazz->hasParentType()) { + return; + } + + if (!$code_base->hasClassWithFQSEN( + $clazz->getParentClassFQSEN() + )) { + // This is an error, but its caught elsewhere. We'll + // just roll through looking for other errors + return; + } + + $parent_clazz = $code_base->getClassByFQSEN( + $clazz->getParentClassFQSEN() + ); + + if (!$parent_clazz->isAbstract() + && !$clazz->isParentConstructorCalled() + ) { + Issue::maybeEmit( + $code_base, + $clazz->getContext(), + Issue::TypeParentConstructorCalled, + $clazz->getFileRef()->getLineNumberStart(), + (string)$clazz->getFQSEN(), + (string)$parent_clazz->getFQSEN() + ); + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/PostOrderAnalysisVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/PostOrderAnalysisVisitor.php new file mode 100644 index 000000000..4ef2ba8ca --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/PostOrderAnalysisVisitor.php @@ -0,0 +1,4929 @@ + a list of parent nodes of the currently analyzed node, + * within the current global or function-like scope + */ + private $parent_node_list; + + /** + * @param CodeBase $code_base + * A code base needs to be passed in because we require + * it to be initialized before any classes or files are + * loaded. + * + * @param Context $context + * The context of the parser at the node for which we'd + * like to determine a type + * + * @param list $parent_node_list + * The parent node list of the node being analyzed + */ + public function __construct( + CodeBase $code_base, + Context $context, + array $parent_node_list + ) { + parent::__construct($code_base, $context); + $this->parent_node_list = $parent_node_list; + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node (@phan-unused-param) + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visit(Node $node): Context + { + // Many nodes don't change the context and we + // don't need to read them. + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitAssign(Node $node): Context + { + // Get the type of the right side of the + // assignment + $right_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'], + true + ); + + $var_node = $node->children['var']; + if (!($var_node instanceof Node)) { + // Give up, this should be impossible except with the fallback + $this->emitIssue( + Issue::InvalidNode, + $node->lineno, + "Expected left side of assignment to be a variable" + ); + return $this->context; + } + + if ($right_type->isVoidType()) { + $this->emitIssue( + Issue::TypeVoidAssignment, + $node->lineno + ); + } + + // Handle the assignment based on the type of the + // right side of the equation and the kind of item + // on the left. + // (AssignmentVisitor converts possibly undefined types to nullable) + $context = (new AssignmentVisitor( + $this->code_base, + $this->context, + $node, + $right_type + ))->__invoke($var_node); + + $expr_node = $node->children['expr']; + if ($expr_node instanceof Node + && $expr_node->kind === ast\AST_CLOSURE + ) { + $method = (new ContextNode( + $this->code_base, + $this->context->withLineNumberStart( + $expr_node->lineno + ), + $expr_node + ))->getClosure(); + + $method->addReference($this->context); + } + + return $context; + } + + /** + * @param Node $node (@phan-unused-param) + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitAssignRef(Node $node): Context + { + return $this->visitAssign($node); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + * @override + */ + public function visitAssignOp(Node $node): Context + { + return (new AssignOperatorAnalysisVisitor($this->code_base, $this->context))->__invoke($node); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitUnset(Node $node): Context + { + $context = $this->context; + // Get the type of the thing being unset + $var_node = $node->children['var']; + if (!($var_node instanceof Node)) { + return $context; + } + + $kind = $var_node->kind; + if ($kind === ast\AST_VAR) { + $var_name = $var_node->children['name']; + if (\is_string($var_name)) { + // TODO: Make this work in branches + $context->unsetScopeVariable($var_name); + } + // I think DollarDollarPlugin already warns, so don't warn here. + } elseif ($kind === ast\AST_DIM) { + $this->analyzeUnsetDim($var_node); + } elseif ($kind === ast\AST_PROP) { + return $this->analyzeUnsetProp($var_node); + } + return $context; + } + + /** + * @param Node $node a node of type AST_DIM in unset() + * @see UnionTypeVisitor::resolveArrayShapeElementTypes() + * @see UnionTypeVisitor::visitDim() + */ + private function analyzeUnsetDim(Node $node): void + { + $expr_node = $node->children['expr']; + if (!($expr_node instanceof Node)) { + // php -l would warn + return; + } + + // For now, just handle a single level of dimensions for unset($x['field']); + if ($expr_node->kind === ast\AST_VAR) { + $var_name = $expr_node->children['name']; + if (!\is_string($var_name)) { + return; + } + + $context = $this->context; + $scope = $context->getScope(); + if (!$scope->hasVariableWithName($var_name)) { + // TODO: Warn about potentially pointless unset in function scopes? + return; + } + // TODO: Could warn about invalid offsets for isset + $variable = $scope->getVariableByName($var_name); + $union_type = $variable->getUnionType(); + if ($union_type->isEmpty()) { + return; + } + $resolved_union_type = $union_type->withStaticResolvedInContext($this->context); + if (!$resolved_union_type->asExpandedTypes($this->code_base)->hasArrayLike() && !$resolved_union_type->hasMixedType()) { + $this->emitIssue( + Issue::TypeArrayUnsetSuspicious, + $node->lineno, + ASTReverter::toShortString($expr_node), + (string)$resolved_union_type + ); + } + $dim_node = $node->children['dim']; + $dim_value = $dim_node instanceof Node ? (new ContextNode($this->code_base, $this->context, $dim_node))->getEquivalentPHPScalarValue() : $dim_node; + // unset($x[$i]) should convert a list or non-empty-list to an array + $union_type = $union_type->withAssociativeArrays(true)->asMappedUnionType(static function (Type $type): Type { + if ($type instanceof NonEmptyMixedType) { + // convert non-empty-mixed to non-null-mixed because `unset($x[$i])` could have removed the last element of an array, + // but that would still not be null. + return $type->isNullableLabeled() ? MixedType::instance(true) : NonNullMixedType::instance(false); + } + return $type; + }); + $variable = clone($variable); + $context->addScopeVariable($variable); + $variable->setUnionType($union_type); + /* + if (!is_scalar($dim_value) || (!is_numeric($dim_value) || $dim_value >= 0)) { + foreach ($union_type->getTypeSet() as $type) { + if ($type instanceof ListType) { + $union_type = $union_type->withoutType($type)->withType( + GenericArrayType::fromElementType($type->genericArrayElementType(), false, $type->getKeyType()) + ); + $variable = clone($variable); + $context->addScopeVariable($variable); + $variable->setUnionType($union_type); + } + } + } + */ + + if (!$union_type->hasTopLevelArrayShapeTypeInstances()) { + return; + } + // TODO: detect and warn about null + if (!\is_scalar($dim_value)) { + return; + } + $variable->setUnionType($union_type->withoutArrayShapeField($dim_value)); + } + } + + /** + * @param Node $node a node of type AST_PROP in unset() + * @see UnionTypeVisitor::resolveArrayShapeElementTypes() + * @see UnionTypeVisitor::visitDim() + */ + private function analyzeUnsetProp(Node $node): Context + { + $expr_node = $node->children['expr']; + $context = $this->context; + if (!($expr_node instanceof Node)) { + // php -l would warn + return $context; + } + $prop_name = $node->children['prop']; + if (!\is_string($prop_name)) { + $prop_name = (new ContextNode($this->code_base, $this->context, $prop_name))->getEquivalentPHPScalarValue(); + if (!\is_string($prop_name)) { + return $context; + } + } + if ($expr_node->kind === \ast\AST_VAR && $expr_node->children['name'] === 'this' && $context === $this->context) { + $context = $context->withThisPropertySetToTypeByName($prop_name, NullType::instance(false)->asPHPDocUnionType()->withIsDefinitelyUndefined()); + } + + $union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr_node)->withStaticResolvedInContext($this->context); + $type_fqsens = $union_type->objectTypesWithKnownFQSENs(); + foreach ($type_fqsens->getTypeSet() as $type) { + $fqsen = FullyQualifiedClassName::fromType($type); + if (!$this->code_base->hasClassWithFQSEN($fqsen)) { + continue; + } + $class = $this->code_base->getClassByFQSEN($fqsen); + if ($class->hasPropertyWithName($this->code_base, $prop_name)) { + // NOTE: We deliberately emit this issue whether or not the access is to a public or private variable, + // because unsetting a private variable at runtime is also a (failed) attempt to unset a declared property. + $prop = $class->getPropertyByName($this->code_base, $prop_name); + if ($prop->isFromPHPDoc()) { + // TODO: Warn if __get is defined but __unset isn't defined? + continue; + } + if ($prop->isDynamicProperty()) { + continue; + } + $this->emitIssue( + Issue::TypeObjectUnsetDeclaredProperty, + $node->lineno, + (string)$type, + $prop_name, + $prop->getFileRef()->getFile(), + $prop->getFileRef()->getLineNumberStart() + ); + } + } + return $context; + } + + /** + * @param Node $node (@phan-unused-param) + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitIfElem(Node $node): Context + { + return $this->context; + } + + /** + * @param Node $node @phan-unused-param + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitWhile(Node $node): Context + { + return $this->context; + } + + /** + * @param Node $node @phan-unused-param + * A node of kind ast\AST_SWITCH to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + * + * @suppress PhanUndeclaredProperty + */ + public function visitSwitch(Node $node): Context + { + if (isset($node->phan_loop_contexts)) { + // Combine contexts from continue/break statements within this do-while loop + $context = (new ContextMergeVisitor($this->context, \array_merge([$this->context], $node->phan_loop_contexts)))->combineChildContextList(); + unset($node->phan_loop_contexts); + return $context; + } + return $this->context; + } + + /** + * @param Node $node @phan-unused-param + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitSwitchCase(Node $node): Context + { + return $this->context; + } + + /** + * @param Node $node @phan-unused-param + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitExprList(Node $node): Context + { + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitEncapsList(Node $node): Context + { + $this->analyzeNoOp($node, Issue::NoopEncapsulatedStringLiteral); + + foreach ($node->children as $child_node) { + // Confirm that variables exists + if (!($child_node instanceof Node)) { + continue; + } + $this->checkExpressionInDynamicString($child_node); + } + + return $this->context; + } + + private function checkExpressionInDynamicString(Node $expr_node): void + { + $code_base = $this->code_base; + $context = $this->context; + $type = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $expr_node, + true + ); + + if (!$type->hasPrintableScalar()) { + if ($type->isType(ArrayType::instance(false)) + || $type->isType(ArrayType::instance(true)) + || $type->isGenericArray() + ) { + $this->emitIssue( + Issue::TypeConversionFromArray, + $expr_node->lineno, + 'string' + ); + return; + } + // Check for __toString(), stringable variables/expressions in encapsulated strings work whether or not strict_types is set + try { + foreach ($type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->asClassList($code_base, $context) as $clazz) { + if ($clazz->hasMethodWithName($code_base, "__toString", true)) { + return; + } + } + } catch (CodeBaseException | RecursionDepthException $_) { + // Swallow "Cannot find class" or recursion exceptions, go on to emit issue + } + $this->emitIssue( + Issue::TypeSuspiciousStringExpression, + $expr_node->lineno, + (string)$type, + ASTReverter::toShortString($expr_node) + ); + } + } + + /** + * Check if a given variable is undeclared. + * @param Node $node Node with kind AST_VAR + */ + private function checkForUndeclaredVariable(Node $node): void + { + $variable_name = $node->children['name']; + + // Ignore $$var type things + if (!\is_string($variable_name)) { + return; + } + + // Don't worry about non-existent undeclared variables + // in the global scope if configured to do so + if (Config::getValue('ignore_undeclared_variables_in_global_scope') + && $this->context->isInGlobalScope() + ) { + return; + } + + if (!$this->context->getScope()->hasVariableWithName($variable_name) + && !Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope()) + ) { + $this->emitIssueWithSuggestion( + Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name), + $node->lineno, + [$variable_name], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name) + ); + } + } + + /** + * @param Node $node (@phan-unused-param) + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitDoWhile(Node $node): Context + { + return $this->context; + } + + /** + * Visit a node with kind `ast\AST_GLOBAL` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitGlobal(Node $node): Context + { + $variable_name = $node->children['var']->children['name'] ?? null; + if (!\is_string($variable_name)) { + // Shouldn't happen? + return $this->context; + } + $variable = new Variable( + $this->context->withLineNumberStart($node->lineno), + $variable_name, + UnionType::empty(), + 0 + ); + $optional_global_variable_type = Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name); + if ($optional_global_variable_type) { + $variable->setUnionType($optional_global_variable_type); + } else { + $scope = $this->context->getScope(); + if ($scope->hasGlobalVariableWithName($variable_name)) { + // TODO: Support @global, add a clone to the method context? + $actual_global_variable = clone($scope->getGlobalVariableByName($variable_name)); + $actual_global_variable->setUnionType($actual_global_variable->getUnionType()->eraseRealTypeSetRecursively()); + $this->context->addScopeVariable($actual_global_variable); + return $this->context; + } + } + + // Note that we're not creating a new scope, just + // adding variables to the existing scope + $this->context->addScopeVariable($variable); + + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitStatic(Node $node): Context + { + $variable = Variable::fromNodeInContext( + $node->children['var'], + $this->context, + $this->code_base, + false + ); + + // If the element has a default, set its type + // on the variable + if (isset($node->children['default'])) { + $default_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['default'] + ); + } else { + $default_type = NullType::instance(false)->asRealUnionType(); + } + + // NOTE: Phan can't be sure that the type the static type starts with is the same as what it has later. Avoid false positive PhanRedundantCondition. + $variable->setUnionType($default_type->eraseRealTypeSetRecursively()); + // TODO: Probably not true in a loop? + // TODO: Expand this to assigning to variables? (would need to make references invalidate that, and skip this in the global scope) + $variable->enablePhanFlagBits(\Phan\Language\Element\Flags::IS_CONSTANT_DEFINITION); + + // Note that we're not creating a new scope, just + // adding variables to the existing scope + $this->context->addScopeVariable($variable); + + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitEcho(Node $node): Context + { + return $this->visitPrint($node); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitPrint(Node $node): Context + { + $code_base = $this->code_base; + $context = $this->context; + $expr_node = $node->children['expr']; + $type = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $expr_node, + true + ); + + if (!$type->hasPrintableScalar()) { + if ($type->isType(ArrayType::instance(false)) + || $type->isType(ArrayType::instance(true)) + || $type->isGenericArray() + ) { + $this->emitIssue( + Issue::TypeConversionFromArray, + $expr_node->lineno ?? $node->lineno, + 'string' + ); + return $context; + } + if (!$context->isStrictTypes()) { + try { + foreach ($type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->asClassList($code_base, $context) as $clazz) { + if ($clazz->hasMethodWithName($code_base, "__toString", true)) { + return $context; + } + } + } catch (CodeBaseException $_) { + // Swallow "Cannot find class", go on to emit issue + } + } + $this->emitIssue( + Issue::TypeSuspiciousEcho, + $expr_node->lineno ?? $node->lineno, + ASTReverter::toShortString($expr_node), + (string)$type + ); + } + + return $context; + } + + /** + * These types are either types which create variables, + * or types which will be checked in other parts of Phan + */ + private const SKIP_VAR_CHECK_TYPES = [ + ast\AST_ARG_LIST => true, // may be a reference + ast\AST_ARRAY_ELEM => true, // [$x, $y] = expr() is an AST_ARRAY_ELEM. visitArray() checks the right-hand side. + ast\AST_ASSIGN_OP => true, // checked in visitAssignOp + ast\AST_ASSIGN_REF => true, // Creates by reference? + ast\AST_ASSIGN => true, // checked in visitAssign + ast\AST_DIM => true, // should be checked elsewhere, as part of check for array access to non-array/string + ast\AST_EMPTY => true, // TODO: Enable this in the future? + ast\AST_GLOBAL => true, // global $var; + ast\AST_ISSET => true, // TODO: Enable this in the future? + ast\AST_PARAM_LIST => true, // this creates the variable + ast\AST_STATIC => true, // static $var; + ast\AST_STMT_LIST => true, // ;$var; (Implicitly creates the variable. Already checked to emit PhanNoopVariable) + ast\AST_USE_ELEM => true, // may be a reference, checked elsewhere + ]; + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitVar(Node $node): Context + { + $this->analyzeNoOp($node, Issue::NoopVariable); + $parent_node = \end($this->parent_node_list); + if ($parent_node instanceof Node && !($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF)) { + $parent_kind = $parent_node->kind; + if (!\array_key_exists($parent_kind, self::SKIP_VAR_CHECK_TYPES)) { + $this->checkForUndeclaredVariable($node); + } + } + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitArray(Node $node): Context + { + $this->analyzeNoOp($node, Issue::NoopArray); + return $this->context; + } + + /** @internal */ + public const NAME_FOR_BINARY_OP = [ + flags\BINARY_BOOL_AND => '&&', + flags\BINARY_BOOL_OR => '||', + flags\BINARY_BOOL_XOR => 'xor', + flags\BINARY_BITWISE_OR => '|', + flags\BINARY_BITWISE_AND => '&', + flags\BINARY_BITWISE_XOR => '^', + flags\BINARY_CONCAT => '.', + flags\BINARY_ADD => '+', + flags\BINARY_SUB => '-', + flags\BINARY_MUL => '*', + flags\BINARY_DIV => '/', + flags\BINARY_MOD => '%', + flags\BINARY_POW => '**', + flags\BINARY_SHIFT_LEFT => '<<', + flags\BINARY_SHIFT_RIGHT => '>>', + flags\BINARY_IS_IDENTICAL => '===', + flags\BINARY_IS_NOT_IDENTICAL => '!==', + flags\BINARY_IS_EQUAL => '==', + flags\BINARY_IS_NOT_EQUAL => '!=', + flags\BINARY_IS_SMALLER => '<', + flags\BINARY_IS_SMALLER_OR_EQUAL => '<=', + flags\BINARY_IS_GREATER => '>', + flags\BINARY_IS_GREATER_OR_EQUAL => '>=', + flags\BINARY_SPACESHIP => '<=>', + flags\BINARY_COALESCE => '??', + ]; + + /** + * @param Node $node + * A node of type AST_BINARY_OP to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitBinaryOp(Node $node): Context + { + $flags = $node->flags; + if ($this->isInNoOpPosition($node)) { + if (\in_array($flags, [flags\BINARY_BOOL_AND, flags\BINARY_BOOL_OR, flags\BINARY_COALESCE], true)) { + // @phan-suppress-next-line PhanAccessMethodInternal + if (!ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $node->children['right'])) { + $this->emitIssue( + Issue::NoopBinaryOperator, + $node->lineno, + self::NAME_FOR_BINARY_OP[$flags] ?? '' + ); + } + } else { + $this->emitIssue( + Issue::NoopBinaryOperator, + $node->lineno, + self::NAME_FOR_BINARY_OP[$flags] ?? '' + ); + } + } + switch ($flags) { + case flags\BINARY_CONCAT: + $this->analyzeBinaryConcat($node); + break; + case flags\BINARY_DIV: + case flags\BINARY_POW: + case flags\BINARY_MOD: + $this->analyzeBinaryNumericOp($node); + break; + case flags\BINARY_SHIFT_LEFT: + case flags\BINARY_SHIFT_RIGHT: + $this->analyzeBinaryShift($node); + break; + case flags\BINARY_BITWISE_OR: + case flags\BINARY_BITWISE_AND: + case flags\BINARY_BITWISE_XOR: + $this->analyzeBinaryBitwiseOp($node); + break; + } + return $this->context; + } + + private function analyzeBinaryShift(Node $node): void + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['left'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['right'] + ); + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + if ($type->isNullableLabeled()) { + return false; + } + if ($type instanceof IntType || $type instanceof MixedType) { + return true; + } + if ($type instanceof LiteralFloatType) { + return $type->isValidBitwiseOperand(); + } + return false; + }, + $left, + $right, + Issue::TypeInvalidLeftOperandOfIntegerOp, + Issue::TypeInvalidRightOperandOfIntegerOp + ); + } + + private function analyzeBinaryBitwiseOp(Node $node): void + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['left'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['right'] + ); + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + if ($type->isNullableLabeled()) { + return false; + } + if ($type instanceof IntType || $type instanceof StringType || $type instanceof MixedType) { + return true; + } + if ($type instanceof LiteralFloatType) { + return $type->isValidBitwiseOperand(); + } + return false; + }, + $left, + $right, + Issue::TypeInvalidLeftOperandOfBitwiseOp, + Issue::TypeInvalidRightOperandOfBitwiseOp + ); + } + + /** @internal used by AssignOperatorAnalysisVisitor */ + public const ISSUE_TYPES_RIGHT_SIDE_ZERO = [ + flags\BINARY_POW => Issue::PowerOfZero, + flags\BINARY_DIV => Issue::DivisionByZero, + flags\BINARY_MOD => Issue::ModuloByZero, + ]; + + private function analyzeBinaryNumericOp(Node $node): void + { + $left = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['left'] + ); + + $right = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['right'] + ); + if (!$right->isEmpty() && !$right->containsTruthy()) { + $this->emitIssue( + self::ISSUE_TYPES_RIGHT_SIDE_ZERO[$node->flags], + $node->children['right']->lineno ?? $node->lineno, + ASTReverter::toShortString($node), + $right + ); + } + $this->warnAboutInvalidUnionType( + $node, + static function (Type $type): bool { + return $type->isValidNumericOperand(); + }, + $left, + $right, + Issue::TypeInvalidLeftOperandOfNumericOp, + Issue::TypeInvalidRightOperandOfNumericOp + ); + } + + /** + * @param Node $node with type AST_BINARY_OP + * @param Closure(Type):bool $is_valid_type + */ + private function warnAboutInvalidUnionType( + Node $node, + Closure $is_valid_type, + UnionType $left, + UnionType $right, + string $left_issue_type, + string $right_issue_type + ): void { + if (!$left->isEmpty()) { + if (!$left->hasTypeMatchingCallback($is_valid_type)) { + $this->emitIssue( + $left_issue_type, + $node->children['left']->lineno ?? $node->lineno, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + $left + ); + } + } + if (!$right->isEmpty()) { + if (!$right->hasTypeMatchingCallback($is_valid_type)) { + $this->emitIssue( + $right_issue_type, + $node->children['right']->lineno ?? $node->lineno, + PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags], + $right + ); + } + } + } + + private function analyzeBinaryConcat(Node $node): void + { + $left = $node->children['left']; + if ($left instanceof Node) { + $this->checkExpressionInDynamicString($left); + } + $right = $node->children['right']; + if ($right instanceof Node) { + $this->checkExpressionInDynamicString($right); + } + } + + public const NAME_FOR_UNARY_OP = [ + flags\UNARY_BOOL_NOT => '!', + flags\UNARY_BITWISE_NOT => '~', + flags\UNARY_SILENCE => '@', + flags\UNARY_PLUS => '+', + flags\UNARY_MINUS => '-', + ]; + + /** + * @param Node $node + * A node of type AST_EMPTY to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitEmpty(Node $node): Context + { + if ($this->isInNoOpPosition($node)) { + $this->emitIssue( + Issue::NoopEmpty, + $node->lineno, + ASTReverter::toShortString($node->children['expr']) + ); + } + return $this->context; + } + + /** + * @internal + * Maps the flags of nodes with kind AST_CAST to their types + */ + public const AST_CAST_FLAGS_LOOKUP = [ + flags\TYPE_NULL => 'unset', + flags\TYPE_BOOL => 'bool', + flags\TYPE_LONG => 'int', + flags\TYPE_DOUBLE => 'float', + flags\TYPE_STRING => 'string', + flags\TYPE_ARRAY => 'array', + flags\TYPE_OBJECT => 'object', + // These aren't casts, but they are used in various places + flags\TYPE_CALLABLE => 'callable', + flags\TYPE_VOID => 'void', + flags\TYPE_ITERABLE => 'iterable', + flags\TYPE_FALSE => 'false', + flags\TYPE_STATIC => 'static', + ]; + + /** + * @suppress PhanUselessBinaryAddRight this replaces 'unset' with 'null' + */ + public const AST_TYPE_FLAGS_LOOKUP = [ + ast\flags\TYPE_NULL => 'null', + ] + self::AST_CAST_FLAGS_LOOKUP; + + + /** + * @param Node $node + * A node of type ast\AST_CAST to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitCast(Node $node): Context + { + if ($this->isInNoOpPosition($node)) { + $this->emitIssue( + Issue::NoopCast, + $node->lineno, + self::AST_CAST_FLAGS_LOOKUP[$node->flags] ?? 'unknown', + ASTReverter::toShortString($node->children['expr']) + ); + } + if ($node->flags === flags\TYPE_NULL) { + $this->emitIssue( + Issue::CompatibleUnsetCast, + $node->lineno, + ASTReverter::toShortString($node) + ); + } + return $this->context; + } + + /** + * @param Node $node + * A node of kind ast\AST_ISSET to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitIsset(Node $node): Context + { + if ($this->isInNoOpPosition($node)) { + $this->emitIssue( + Issue::NoopIsset, + $node->lineno, + ASTReverter::toShortString($node->children['var']) + ); + } + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitUnaryOp(Node $node): Context + { + if ($node->flags === flags\UNARY_SILENCE) { + $expr = $node->children['expr']; + if ($expr instanceof Node) { + if ($expr->kind === ast\AST_UNARY_OP && $expr->flags === flags\UNARY_SILENCE) { + $this->emitIssue( + Issue::NoopRepeatedSilenceOperator, + $node->lineno, + ASTReverter::toShortString($node) + ); + } + } else { + // TODO: Other node kinds + $this->emitIssue( + Issue::NoopUnaryOperator, + $node->lineno, + self::NAME_FOR_UNARY_OP[$node->flags] ?? '' + ); + } + } else { + if ($this->isInNoOpPosition($node)) { + $this->emitIssue( + Issue::NoopUnaryOperator, + $node->lineno, + self::NAME_FOR_UNARY_OP[$node->flags] ?? '' + ); + } + } + return $this->context; + } + + /** + * @override + */ + public function visitPreInc(Node $node): Context + { + return $this->analyzeIncOrDec($node); + } + + /** + * @override + */ + public function visitPostInc(Node $node): Context + { + return $this->analyzeIncOrDec($node); + } + + /** + * @override + */ + public function visitPreDec(Node $node): Context + { + return $this->analyzeIncOrDec($node); + } + + /** + * @override + */ + public function visitPostDec(Node $node): Context + { + return $this->analyzeIncOrDec($node); + } + + public const NAME_FOR_INC_OR_DEC_KIND = [ + ast\AST_PRE_INC => '++(expr)', + ast\AST_PRE_DEC => '--(expr)', + ast\AST_POST_INC => '(expr)++', + ast\AST_POST_DEC => '(expr)--', + ]; + + private function analyzeIncOrDec(Node $node): Context + { + $var = $node->children['var']; + $old_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $var); + if (!$old_type->canCastToUnionType(UnionType::fromFullyQualifiedPHPDocString('int|string|float'))) { + $this->emitIssue( + Issue::TypeInvalidUnaryOperandIncOrDec, + $node->lineno, + self::NAME_FOR_INC_OR_DEC_KIND[$node->kind], + $old_type + ); + } + // The left can be a non-Node for an invalid AST + $kind = $var->kind ?? null; + if ($kind === \ast\AST_VAR) { + $new_type = $old_type->getTypeAfterIncOrDec(); + if ($old_type === $new_type) { + return $this->context; + } + if (!$this->context->isInLoop()) { + try { + $value = $old_type->asSingleScalarValueOrNull(); + if (\is_numeric($value)) { + if ($node->kind === ast\AST_POST_DEC || $node->kind === ast\AST_PRE_DEC) { + @--$value; + } else { + @++$value; + } + // TODO: Compute the real type set. + $new_type = Type::fromObject($value)->asPHPDocUnionType(); + } + } catch (\Throwable $_) { + // ignore + } + } + try { + $variable = (new ContextNode($this->code_base, $this->context, $var))->getVariableStrict(); + } catch (IssueException | NodeException $_) { + return $this->context; + } + $variable = clone($variable); + $variable->setUnionType($new_type); + $this->context->addScopeVariable($variable); + return $this->context; + } + // Treat expr++ like expr -= -1 and expr-- like expr -= 1. + // Use `-` to avoid false positives about array operations. + // (This isn't 100% accurate for invalid types) + $new_node = new Node( + ast\AST_ASSIGN_OP, + ast\flags\BINARY_SUB, + [ + 'var' => $var, + 'expr' => ($node->kind === ast\AST_POST_DEC || $node->kind === ast\AST_PRE_DEC) ? 1 : -1, + ], + $node->lineno + ); + return (new AssignOperatorAnalysisVisitor($this->code_base, $this->context))->visitBinarySub($new_node); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitConst(Node $node): Context + { + $context = $this->context; + try { + // Based on UnionTypeVisitor::visitConst + $constant = (new ContextNode( + $this->code_base, + $context, + $node + ))->getConst(); + + // Mark that this constant has been referenced from + // this context + $constant->addReference($context); + } catch (IssueException $exception) { + // We need to do this in order to check keys and (after the first 5) values in AST arrays. + // Other parts of the AST may also not be covered. + // (This issue may be a duplicate) + Issue::maybeEmitInstance( + $this->code_base, + $context, + $exception->getIssueInstance() + ); + } catch (Exception $_) { + // Swallow any other types of exceptions. We'll log the errors + // elsewhere. + } + + // Check to make sure we're doing something with the + // constant + $this->analyzeNoOp($node, Issue::NoopConstant); + + return $context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitClassConst(Node $node): Context + { + try { + $constant = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getClassConst(); + + // Mark that this class constant has been referenced + // from this context + $constant->addReference($this->context); + } catch (IssueException $exception) { + // We need to do this in order to check keys and (after the first 5) values in AST arrays, possibly other types. + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (Exception $_) { + // Swallow any other types of exceptions. We'll log the errors + // elsewhere. + } + + // Check to make sure we're doing something with the + // class constant + $this->analyzeNoOp($node, Issue::NoopConstant); + + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitClassConstDecl(Node $node): Context + { + $class = $this->context->getClassInScope($this->code_base); + + foreach ($node->children as $child_node) { + if (!$child_node instanceof Node) { + throw new AssertionError('expected class const element to be a Node'); + } + $name = $child_node->children['name']; + if (!\is_string($name)) { + throw new AssertionError('expected class const name to be a string'); + } + try { + $const_decl = $class->getConstantByNameInContext($this->code_base, $name, $this->context); + $const_decl->getUnionType(); + } catch (IssueException $exception) { + // We need to do this in order to check keys and (after the first 5) values in AST arrays, possibly other types. + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (Exception $_) { + // Swallow any other types of exceptions. We'll log the errors + // elsewhere. + } + } + + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitConstDecl(Node $node): Context + { + foreach ($node->children as $child_node) { + if (!$child_node instanceof Node) { + throw new AssertionError('expected const element to be a Node'); + } + $name = $child_node->children['name']; + if (!\is_string($name)) { + throw new AssertionError('expected const name to be a string'); + } + + try { + $fqsen = FullyQualifiedGlobalConstantName::fromStringInContext( + $name, + $this->context + ); + $const_decl = $this->code_base->getGlobalConstantByFQSEN($fqsen); + $const_decl->getUnionType(); + } catch (IssueException $exception) { + // We need to do this in order to check keys and (after the first 5) values in AST arrays, possibly other types. + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (Exception $_) { + // Swallow any other types of exceptions. We'll log the errors + // elsewhere. + } + } + + return $this->context; + } + + /** + * @param Node $node + * A node of kind `ast\AST_CLASS_NAME` to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitClassName(Node $node): Context + { + try { + foreach ((new ContextNode( + $this->code_base, + $this->context, + $node->children['class'] + ))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME) as $class) { + $class->addReference($this->context); + } + } catch (CodeBaseException $exception) { + $exception_fqsen = $exception->getFQSEN(); + $this->emitIssueWithSuggestion( + Issue::UndeclaredClassReference, + $node->lineno, + [(string)$exception_fqsen], + IssueFixSuggester::suggestSimilarClassForGenericFQSEN($this->code_base, $this->context, $exception_fqsen) + ); + } catch (IssueException $exception) { + Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); + } + + // Check to make sure we're doing something with the + // ::class class constant + $this->analyzeNoOp($node, Issue::NoopConstant); + + return $this->context; + } + + /** + * @param Node $node + * A node of kind ast\AST_CLOSURE or ast\AST_ARROW_FUNC to analyze + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitClosure(Node $node): Context + { + $func = $this->context->getFunctionLikeInScope($this->code_base); + + $return_type = $func->getUnionType(); + + if (!$return_type->isEmpty() + && !$func->hasReturn() + && !self::declOnlyThrows($node) + && !$return_type->hasType(VoidType::instance(false)) + && !$return_type->hasType(NullType::instance(false)) + ) { + $this->warnTypeMissingReturn($func, $node); + } + $uses = $node->children['uses'] ?? null; + // @phan-suppress-next-line PhanUndeclaredProperty + if (isset($uses->polyfill_has_trailing_comma) && Config::get_closest_minimum_target_php_version_id() < 80000) { + $this->emitIssue( + Issue::CompatibleTrailingCommaParameterList, + end($uses->children)->lineno ?? $uses->lineno, + ASTReverter::toShortString($node) + ); + } + $this->analyzeNoOp($node, Issue::NoopClosure); + $this->checkForFunctionInterfaceIssues($node, $func); + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitArrowFunc(Node $node): Context + { + if (Config::get_closest_minimum_target_php_version_id() < 70400) { + $this->emitIssue( + Issue::CompatibleArrowFunction, + $node->lineno, + ASTReverter::toShortString($node) + ); + } + return $this->visitClosure($node); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitReturn(Node $node): Context + { + $context = $this->context; + // Make sure we're actually returning from a method. + if (!$context->isInFunctionLikeScope()) { + return $context; + } + $code_base = $this->code_base; + + // Check real return types instead of phpdoc return types in traits for #800 + // TODO: Why did Phan originally not analyze return types of traits at all in 4c6956c05222e093b29393ceaa389ffb91041bdc + $is_trait = false; + if ($context->isInClassScope()) { + $clazz = $context->getClassInScope($code_base); + $is_trait = $clazz->isTrait(); + } + + // Get the method/function/closure we're in + $method = $context->getFunctionLikeInScope($code_base); + + // Mark the method as returning something (even if void) + if (null !== $node->children['expr']) { + $method->setHasReturn(true); + } + + if ($method->returnsRef()) { + $this->analyzeReturnsReference($method, $node); + } + if ($method->hasYield()) { // Function that is syntactically a Generator. + $this->analyzeReturnInGenerator($method, $node); + // TODO: Compare against TReturn of Generator + return $context; // Analysis was completed in PreOrderAnalysisVisitor + } + + // Figure out what we intend to return + // (For traits, lower the false positive rate by comparing against the real return type instead of the phpdoc type (#800)) + $method_return_type = $is_trait ? $method->getRealReturnType()->withAddedClassForResolvedSelf($method->getContext()) : $method->getUnionType(); + $expr = $node->children['expr']; + + // Check for failing to return a value, or returning a value in a void method. + if ($expr !== null) { + if ($method_return_type->hasRealTypeSet() && $method_return_type->asRealUnionType()->isVoidType()) { + $this->emitIssue( + Issue::SyntaxReturnValueInVoid, + $expr->lineno ?? $node->lineno, + 'void', + $method->getNameForIssue(), + 'return;', + 'return ' . ASTReverter::toShortString($expr) . ';' + ); + return $context; + } + } else { + // `function test() : ?string { return; }` is a fatal error. (We already checked for generators) + if ($method_return_type->hasRealTypeSet() && !$method_return_type->asRealUnionType()->isVoidType()) { + $this->emitIssue( + Issue::SyntaxReturnExpectedValue, + $node->lineno, + $method->getNameForIssue(), + $method_return_type, + 'return null', + 'return' + ); + return $context; + } + } + + + // This leaves functions which aren't syntactically generators. + + // Figure out what is actually being returned + // TODO: Properly check return values of array shapes + foreach ($this->getReturnTypes($context, $expr, $node->lineno) as $lineno => [$expression_type, $inner_node]) { + // If there is no declared type, see if we can deduce + // what it should be based on the return type + if ($method_return_type->isEmpty() + || $method->isReturnTypeUndefined() + ) { + if (!$is_trait) { + $method->setIsReturnTypeUndefined(true); + + // Set the inferred type of the method based + // on what we're returning + $method->setUnionType($method->getUnionType()->withUnionType($expression_type)); + } + + // No point in comparing this type to the + // type we just set + continue; + } + + // Check if the return type is compatible with the declared return type. + $is_mismatch = false; + if (!$method->isReturnTypeUndefined()) { + $resolved_expression_type = $expression_type->withStaticResolvedInContext($context); + // We allow base classes to cast to subclasses, and subclasses to cast to base classes, + // but don't allow subclasses to cast to subclasses on a separate branch of the inheritance tree + if (!$this->checkCanCastToReturnType($resolved_expression_type, $method_return_type)) { + $this->emitTypeMismatchReturnIssue($resolved_expression_type, $method, $method_return_type, $lineno, $inner_node); + $is_mismatch = true; + } elseif (Config::get_strict_return_checking() && $resolved_expression_type->typeCount() > 1) { + $is_mismatch = self::analyzeReturnStrict($code_base, $method, $resolved_expression_type, $method_return_type, $lineno, $inner_node); + } + } + // For functions that aren't syntactically Generators, + // update the set/existence of return values. + + if ($method->isReturnTypeModifiable() && !$is_mismatch) { + // Add the new type to the set of values returned by the + // method + $method->setUnionType($method->getUnionType()->withUnionType($expression_type)); + } + } + + return $context; + } + + /** + * @param Node $node a node of kind ast\AST_RETURN + */ + private function analyzeReturnsReference(FunctionInterface $method, Node $node): void + { + $expr = $node->children['expr']; + if ((!$expr instanceof Node) || !\in_array($expr->kind, ArgumentType::REFERENCE_NODE_KINDS, true)) { + $is_possible_reference = ArgumentType::isExpressionReturningReference($this->code_base, $this->context, $expr); + + if (!$is_possible_reference) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeNonVarReturnByRef, + $expr->lineno ?? $node->lineno, + $method->getRepresentationForIssue() + ); + } + } + } + + /** + * Emits Issue::TypeMismatchReturnNullable or TypeMismatchReturn, unless suppressed + * @param Node|string|int|float|null $inner_node + */ + private function emitTypeMismatchReturnIssue(UnionType $expression_type, FunctionInterface $method, UnionType $method_return_type, int $lineno, $inner_node): void + { + if ($this->shouldSuppressIssue(Issue::TypeMismatchReturnReal, $lineno)) { + // Suppressing TypeMismatchReturnReal also suppresses less severe return type mismatches + return; + } + if (!$expression_type->isNull() && $this->checkCanCastToReturnTypeIfWasNonNullInstead($expression_type, $method_return_type)) { + if ($this->shouldSuppressIssue(Issue::TypeMismatchReturn, $lineno)) { + // Suppressing TypeMismatchReturn also suppresses TypeMismatchReturnNullable + return; + } + $issue_type = Issue::TypeMismatchReturnNullable; + } else { + $issue_type = Issue::TypeMismatchReturn; + // TODO: Don't warn for callable <-> string + if ($method_return_type->hasRealTypeSet()) { + // Always emit a real type warning about returning a value in a void method + $real_method_return_type = $method_return_type->getRealUnionType(); + $real_expression_type = $expression_type->getRealUnionType(); + if ($real_method_return_type->isVoidType() || + ($expression_type->hasRealTypeSet() && !$real_expression_type->canCastToDeclaredType($this->code_base, $this->context, $real_method_return_type))) { + $this->emitIssue( + Issue::TypeMismatchReturnReal, + $lineno, + self::returnExpressionToShortString($inner_node), + (string)$expression_type, + self::toDetailsForRealTypeMismatch($expression_type), + $method->getNameForIssue(), + (string)$method_return_type, + self::toDetailsForRealTypeMismatch($method_return_type) + ); + return; + } + } + } + if ($this->context->hasSuppressIssue($this->code_base, Issue::TypeMismatchArgumentProbablyReal)) { + // Suppressing ProbablyReal also suppresses the less severe version. + return; + } + if ($issue_type === Issue::TypeMismatchReturn) { + if ($expression_type->hasRealTypeSet() && + !$expression_type->getRealUnionType()->canCastToDeclaredType($this->code_base, $this->context, $method_return_type)) { + // The argument's real type is completely incompatible with the documented phpdoc type. + // + // Either the phpdoc type is wrong or the argument is likely wrong. + $this->emitIssue( + Issue::TypeMismatchReturnProbablyReal, + $lineno, + self::returnExpressionToShortString($inner_node), + $expression_type, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($expression_type), + $method->getNameForIssue(), + $method_return_type, + PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($method_return_type) + ); + return; + } + } + $this->emitIssue( + $issue_type, + $lineno, + self::returnExpressionToShortString($inner_node), + (string)$expression_type, + $method->getNameForIssue(), + (string)$method_return_type + ); + } + + /** + * Converts the type to a description of the real type (if different from phpdoc type) for Phan's issue messages + * @internal + */ + public static function toDetailsForRealTypeMismatch(UnionType $type): string + { + $real_type = $type->getRealUnionType(); + if ($real_type->isEqualTo($type)) { + return ''; + } + if ($real_type->isEmpty()) { + return ' (no real type)'; + } + return " (real type $real_type)"; + } + + private function analyzeReturnInGenerator( + FunctionInterface $method, + Node $node + ): void { + $method_generator_type = $method->getReturnTypeAsGeneratorTemplateType(); + $type_list = $method_generator_type->getTemplateParameterTypeList(); + // Generator + if (\count($type_list) !== 4) { + return; + } + $expected_return_type = $type_list[3]; + if ($expected_return_type->isEmpty()) { + return; + } + + $context = $this->context; + $code_base = $this->code_base; + + foreach ($this->getReturnTypes($context, $node->children['expr'], $node->lineno) as $lineno => [$expression_type, $inner_node]) { + $expression_type = $expression_type->withStaticResolvedInContext($context); + // We allow base classes to cast to subclasses, and subclasses to cast to base classes, + // but don't allow subclasses to cast to subclasses on a separate branch of the inheritance tree + if (!self::checkCanCastToReturnType($expression_type, $expected_return_type)) { + $this->emitTypeMismatchReturnIssue($expression_type, $method, $expected_return_type, $lineno, $inner_node); + } elseif (Config::get_strict_return_checking() && $expression_type->typeCount() > 1) { + self::analyzeReturnStrict($code_base, $method, $expression_type, $expected_return_type, $lineno, $inner_node); + } + } + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitYield(Node $node): Context + { + $context = $this->context; + // Make sure we're actually returning from a method. + if (!$context->isInFunctionLikeScope()) { + return $context; + } + + // Get the method/function/closure we're in + $method = $context->getFunctionLikeInScope($this->code_base); + + // Figure out what we intend to return + $method_generator_type = $method->getReturnTypeAsGeneratorTemplateType(); + $type_list = $method_generator_type->getTemplateParameterTypeList(); + if (\count($type_list) === 0) { + return $context; + } + return $this->compareYieldAgainstDeclaredType($node, $method, $context, $type_list); + } + + /** + * @param list $template_type_list + */ + private function compareYieldAgainstDeclaredType(Node $node, FunctionInterface $method, Context $context, array $template_type_list): Context + { + $code_base = $this->code_base; + + $type_list_count = \count($template_type_list); + + $yield_value_node = $node->children['value']; + if ($yield_value_node === null) { + $yield_value_type = VoidType::instance(false)->asRealUnionType(); + } else { + $yield_value_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $yield_value_node); + } + $expected_value_type = $template_type_list[\min(1, $type_list_count - 1)]; + try { + if (!$yield_value_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->canCastToUnionType($expected_value_type->withStaticResolvedInContext($context))) { + $this->emitIssue( + Issue::TypeMismatchGeneratorYieldValue, + $node->lineno, + ASTReverter::toShortString($yield_value_node), + (string)$yield_value_type, + $method->getNameForIssue(), + (string)$expected_value_type, + '\Generator<' . implode(',', $template_type_list) . '>' + ); + } + } catch (RecursionDepthException $_) { + } + + if ($type_list_count > 1) { + $yield_key_node = $node->children['key']; + if ($yield_key_node === null) { + $yield_key_type = VoidType::instance(false)->asRealUnionType(); + } else { + $yield_key_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $yield_key_node); + } + // TODO: finalize syntax to indicate the absence of a key or value (e.g. use void instead?) + $expected_key_type = $template_type_list[0]; + if (!$yield_key_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->canCastToUnionType($expected_key_type->withStaticResolvedInContext($context))) { + $this->emitIssue( + Issue::TypeMismatchGeneratorYieldKey, + $node->lineno, + ASTReverter::toShortString($yield_key_node), + (string)$yield_key_type, + $method->getNameForIssue(), + (string)$expected_key_type, + '\Generator<' . implode(',', $template_type_list) . '>' + ); + } + } + return $context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitYieldFrom(Node $node): Context + { + $context = $this->context; + // Make sure we're actually returning from a method. + if (!$context->isInFunctionLikeScope()) { + return $context; + } + + // Get the method/function/closure we're in + $method = $context->getFunctionLikeInScope($this->code_base); + $code_base = $this->code_base; + + $yield_from_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node->children['expr']); + if ($yield_from_type->isEmpty()) { + return $context; + } + $yield_from_expanded_type = $yield_from_type->withStaticResolvedInContext($this->context)->asExpandedTypes($code_base); + if (!$yield_from_expanded_type->hasIterable() && !$yield_from_expanded_type->hasTraversable()) { + $this->emitIssue( + Issue::TypeInvalidYieldFrom, + $node->lineno, + ASTReverter::toShortString($node), + (string)$yield_from_type + ); + return $context; + } + + if (BlockAnalysisVisitor::isEmptyIterable($yield_from_type)) { + RedundantCondition::emitInstance( + $node->children['expr'], + $this->code_base, + (clone($this->context))->withLineNumberStart($node->children['expr']->lineno ?? $node->lineno), + Issue::EmptyYieldFrom, + [(string)$yield_from_type], + Closure::fromCallable([BlockAnalysisVisitor::class, 'isEmptyIterable']) + ); + } + + // Figure out what we intend to return + $method_generator_type = $method->getReturnTypeAsGeneratorTemplateType(); + $type_list = $method_generator_type->getTemplateParameterTypeList(); + if (\count($type_list) === 0) { + return $context; + } + return $this->compareYieldFromAgainstDeclaredType($node, $method, $context, $type_list, $yield_from_type); + } + + /** + * @param list $template_type_list + */ + private function compareYieldFromAgainstDeclaredType(Node $node, FunctionInterface $method, Context $context, array $template_type_list, UnionType $yield_from_type): Context + { + $code_base = $this->code_base; + + $type_list_count = \count($template_type_list); + + // TODO: Can do a better job of analyzing expressions that are just arrays or subclasses of Traversable. + // + // A solution would need to check for (at)return Generator|T[] + $yield_from_generator_type = $yield_from_type->asGeneratorTemplateType(); + + $actual_template_type_list = $yield_from_generator_type->getTemplateParameterTypeList(); + $actual_type_list_count = \count($actual_template_type_list); + if ($actual_type_list_count === 0) { + return $context; + } + + $yield_value_type = $actual_template_type_list[\min(1, $actual_type_list_count - 1)]; + $expected_value_type = $template_type_list[\min(1, $type_list_count - 1)]; + if (!$yield_value_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->canCastToUnionType($expected_value_type)) { + $this->emitIssue( + Issue::TypeMismatchGeneratorYieldValue, + $node->lineno, + sprintf('(values of %s)', ASTReverter::toShortString($node)), + (string)$yield_value_type, + $method->getNameForIssue(), + (string)$expected_value_type, + '\Generator<' . implode(',', $template_type_list) . '>' + ); + } + + if ($type_list_count > 1 && $actual_type_list_count > 1) { + // TODO: finalize syntax to indicate the absence of a key or value (e.g. use void instead?) + $yield_key_type = $actual_template_type_list[0]; + $expected_key_type = $template_type_list[0]; + if (!$yield_key_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->canCastToUnionType($expected_key_type)) { + $this->emitIssue( + Issue::TypeMismatchGeneratorYieldKey, + $node->lineno, + sprintf('(keys of %s)', ASTReverter::toShortString($node)), + (string)$yield_key_type, + $method->getNameForIssue(), + (string)$expected_key_type, + '\Generator<' . implode(',', $template_type_list) . '>' + ); + } + } + return $context; + } + + private function checkCanCastToReturnType(UnionType $expression_type, UnionType $method_return_type): bool + { + if ($expression_type->hasRealTypeSet() && $method_return_type->hasRealTypeSet()) { + $real_expression_type = $expression_type->getRealUnionType(); + $real_method_return_type = $method_return_type->getRealUnionType(); + if (!$real_method_return_type->isNull() && !$real_expression_type->canCastToDeclaredType($this->code_base, $this->context, $real_method_return_type)) { + return false; + } + } + if ($method_return_type->hasTemplateParameterTypes()) { + // Perform a check that does a better job understanding rules of templates. + // (E.g. should be able to cast None to Option, but not Some to Option + return $expression_type->asExpandedTypesPreservingTemplate($this->code_base)->canCastToUnionTypeHandlingTemplates($method_return_type, $this->code_base) || + $expression_type->canCastToUnionTypeHandlingTemplates($method_return_type->asExpandedTypesPreservingTemplate($this->code_base), $this->code_base); + } + // We allow base classes to cast to subclasses, and subclasses to cast to base classes, + // but don't allow subclasses to cast to subclasses on a separate branch of the inheritance tree + try { + return $expression_type->asExpandedTypes($this->code_base)->canCastToUnionType($method_return_type) || + $expression_type->canCastToUnionType($method_return_type->asExpandedTypes($this->code_base)); + } catch (RecursionDepthException $_) { + return false; + } + } + + /** + * Precondition: checkCanCastToReturnType is false + */ + private function checkCanCastToReturnTypeIfWasNonNullInstead(UnionType $expression_type, UnionType $method_return_type): bool + { + $nonnull_expression_type = $expression_type->nonNullableClone(); + if ($nonnull_expression_type === $expression_type || $nonnull_expression_type->isEmpty()) { + return false; + } + return $this->checkCanCastToReturnType($nonnull_expression_type, $method_return_type); + } + + /** + * @param Node|string|int|float|null $inner_node + */ + private function analyzeReturnStrict( + CodeBase $code_base, + FunctionInterface $method, + UnionType $expression_type, + UnionType $method_return_type, + int $lineno, + $inner_node + ): bool { + $type_set = $expression_type->getTypeSet(); + $context = $this->context; + if (\count($type_set) < 2) { + throw new AssertionError("Expected at least two types for strict return type checks"); + } + + $mismatch_type_set = UnionType::empty(); + $mismatch_expanded_types = null; + + // For the strict + foreach ($type_set as $type) { + // Expand it to include all parent types up the chain + try { + $individual_type_expanded = $type->asExpandedTypes($code_base); + } catch (RecursionDepthException $_) { + continue; + } + + // See if the argument can be cast to the + // parameter + if (!$individual_type_expanded->canCastToUnionType( + $method_return_type + )) { + if ($method->isPHPInternal()) { + // If we are not in strict mode and we accept a string parameter + // and the argument we are passing has a __toString method then it is ok + if (!$context->isStrictTypes() && $method_return_type->hasNonNullStringType()) { + if ($individual_type_expanded->hasClassWithToStringMethod($code_base, $context)) { + continue; + } + } + } + $mismatch_type_set = $mismatch_type_set->withType($type); + if ($mismatch_expanded_types === null) { + // Warn about the first type + $mismatch_expanded_types = $individual_type_expanded; + } + } + } + + + if ($mismatch_expanded_types === null) { + // No mismatches + return false; + } + + // If we have TypeMismatchReturn already, then also suppress the partial mismatch warnings (e.g. PartialTypeMismatchReturn) as well. + if ($this->context->hasSuppressIssue($code_base, Issue::TypeMismatchReturn)) { + return false; + } + $this->emitIssue( + self::getStrictIssueType($mismatch_type_set), + $lineno, + self::returnExpressionToShortString($inner_node), + (string)$expression_type, + $method->getNameForIssue(), + (string)$method_return_type, + $mismatch_expanded_types + ); + return true; + } + + /** + * @param Node|string|int|float|null $node + */ + private static function returnExpressionToShortString($node): string + { + return $node !== null ? ASTReverter::toShortString($node) : 'void'; + } + + private static function getStrictIssueType(UnionType $union_type): string + { + if ($union_type->typeCount() === 1) { + $type = $union_type->getTypeSet()[0]; + if ($type instanceof NullType) { + return Issue::PossiblyNullTypeReturn; + } + if ($type instanceof FalseType) { + return Issue::PossiblyFalseTypeReturn; + } + } + return Issue::PartialTypeMismatchReturn; + } + + /** + * @param ?Node|?string|?int|?float $node + * @return \Generator + */ + private function getReturnTypes(Context $context, $node, int $return_lineno): \Generator + { + if (!($node instanceof Node)) { + if (null === $node) { + yield $return_lineno => [VoidType::instance(false)->asRealUnionType(), null]; + return; + } + yield $return_lineno => [ + UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $context, + $node, + true + ), + $node + ]; + return; + } + $kind = $node->kind; + if ($kind === ast\AST_CONDITIONAL) { + yield from self::deduplicateUnionTypes($this->getReturnTypesOfConditional($context, $node)); + return; + } elseif ($kind === ast\AST_ARRAY) { + $expression_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $node, true); + if ($expression_type->hasTopLevelArrayShapeTypeInstances()) { + yield $return_lineno => [$expression_type, $node]; + return; + } + + // TODO: Infer list<> + $key_type_enum = GenericArrayType::getKeyTypeOfArrayNode($this->code_base, $context, $node); + foreach (self::deduplicateUnionTypes($this->getReturnTypesOfArray($context, $node)) as $return_lineno => [$elem_type, $elem_node]) { + yield $return_lineno => [ + $elem_type->asGenericArrayTypes($key_type_enum), // TODO: Infer corresponding key types + $elem_node, + ]; + } + return; + } + + $expression_type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $context, + $node, + true + ); + + yield $return_lineno => [$expression_type, $node]; + } + + /** + * @return \Generator|UnionType[] + * @phan-return \Generator + */ + private function getReturnTypesOfConditional(Context $context, Node $node): \Generator + { + $cond_node = $node->children['cond']; + $cond_truthiness = UnionTypeVisitor::checkCondUnconditionalTruthiness($cond_node); + // For the shorthand $a ?: $b, the cond node will be the truthy value. + // Note: an ast node will never be null(can be unset), it will be a const AST node with the name null. + $true_node = $node->children['true'] ?? $cond_node; + + // Rarely, a conditional will always be true or always be false. + if ($cond_truthiness !== null) { + // TODO: Add no-op checks in another PR, if they don't already exist for conditional. + if ($cond_truthiness) { + // The condition is unconditionally true + yield from $this->getReturnTypes($context, $true_node, $node->lineno); + return; + } else { + // The condition is unconditionally false + + // Add the type for the 'false' side + yield from $this->getReturnTypes($context, $node->children['false'], $node->lineno); + return; + } + } + + // TODO: false_context once there is a NegatedConditionVisitor + // TODO: emit no-op if $cond_node is a literal, such as `if (2)` + // - Also note that some things such as `true` and `false` are ast\AST_NAME nodes. + + if ($cond_node instanceof Node) { + // TODO: Use different contexts and merge those, in case there were assignments or assignments by reference in both sides of the conditional? + // Reuse the BranchScope (sort of unintuitive). The ConditionVisitor returns a clone and doesn't modify the original. + $base_context = $this->context; + // We don't bother analyzing visitReturn in PostOrderAnalysisVisitor, right now. + // This may eventually change, just to ensure the expression is checked for issues + $true_context = (new ConditionVisitor( + $this->code_base, + $base_context + ))->__invoke($cond_node); + $false_context = (new NegatedConditionVisitor( + $this->code_base, + $base_context + ))->__invoke($cond_node); + } else { + $true_context = $context; + $false_context = $this->context; + } + + // Allow nested ternary operators, or arrays within ternary operators + if (isset($node->children['true'])) { + yield from $this->getReturnTypes($true_context, $true_node, $true_node->lineno ?? $node->lineno); + } else { + // E.g. From the left-hand side of yield (int|false) ?: default, + // yielding false is impossible. + foreach ($this->getReturnTypes($true_context, $true_node, $true_node->lineno ?? $node->lineno) as $lineno => $details) { + $raw_union_type = $details[0]; + if ($raw_union_type->isEmpty() || !$raw_union_type->containsFalsey()) { + yield $lineno => $details; + } else { + $raw_union_type = $raw_union_type->nonFalseyClone(); + if (!$raw_union_type->isEmpty()) { + yield $lineno => [$raw_union_type, $details[1]]; + } + } + } + } + + $false_node = $node->children['false']; + yield from $this->getReturnTypes($false_context, $false_node, $false_node->lineno ?? $node->lineno); + } + + /** + * @param iterable $types + * @return \Generator + * @suppress PhanPluginCanUseParamType should probably suppress, iterable is php 7.2 + */ + private static function deduplicateUnionTypes($types): \Generator + { + $unique_types = []; + foreach ($types as $lineno => $details) { + $type = $details[0]; + foreach ($unique_types as $old_type) { + if ($type->isEqualTo($old_type)) { + continue 2; + } + } + yield $lineno => $details; + $unique_types[] = $type; + } + } + + /** + * @return \Generator|iterable + * @phan-return \Generator + */ + private function getReturnTypesOfArray(Context $context, Node $node): \Generator + { + if (\count($node->children) === 0) { + // Possibly unreachable (array shape would be returned instead) + yield $node->lineno => [MixedType::instance(false)->asPHPDocUnionType(), $node]; + return; + } + foreach ($node->children as $elem) { + if (!($elem instanceof Node)) { + // We already emit PhanSyntaxError + continue; + } + // Don't bother recursing more than one level to iterate over possible types. + if ($elem->kind === \ast\AST_UNPACK) { + // Could optionally recurse to better analyze `yield [...SOME_EXPRESSION_WITH_MIX_OF_VALUES]` + yield $elem->lineno => [ + UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $context, + $elem, + true + ), + $elem + ]; + continue; + } + $value_node = $elem->children['value']; + if ($value_node instanceof Node) { + yield $elem->lineno => [ + UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $context, + $value_node, + true + ), + $value_node + ]; + } else { + yield $elem->lineno => [ + Type::fromObject($value_node)->asRealUnionType(), + $value_node + ]; + } + } + } + + /** + * @param Node $node (@phan-unused-param) + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitPropDecl(Node $node): Context + { + return $this->context; + } + + /** + * @param Node $node (@phan-unused-param) + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitPropGroup(Node $node): Context + { + $this->checkUnionTypeCompatibility($node->children['type']); + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitCall(Node $node): Context + { + $expression = $node->children['expr']; + try { + // Get the function. + // If the function is undefined, always try to create a placeholder from Phan's type signatures for internal functions so they can still be type checked. + $function_list_generator = (new ContextNode( + $this->code_base, + $this->context, + $expression + ))->getFunctionFromNode(true); + + foreach ($function_list_generator as $function) { + // Check the call for parameter and argument types + $this->analyzeCallToFunctionLike( + $function, + $node + ); + if ($function instanceof Func && \strcasecmp($function->getName(), 'assert') === 0 && $function->getFQSEN()->getNamespace() === '\\') { + $this->context = $this->analyzeAssert($this->context, $node); + } + } + } catch (CodeBaseException $_) { + // ignore it. + } + + return $this->context; + } + + private function analyzeAssert(Context $context, Node $node): Context + { + $args_first_child = $node->children['args']->children[0] ?? null; + if (!($args_first_child instanceof Node)) { + return $this->context; + } + + // Look to see if the asserted expression says anything about + // the types of any variables. + return (new ConditionVisitor( + $this->code_base, + $context + ))->__invoke($args_first_child); + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitNew(Node $node): Context + { + $class_list = []; + try { + $context_node = new ContextNode( + $this->code_base, + $this->context, + $node + ); + + $method = $context_node->getMethod( + '__construct', + false, + false, + true + ); + + $class_list = $context_node->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME); + // Add a reference to each class this method + // could be called on + foreach ($class_list as $class) { + $class->addReference($this->context); + if ($class->isDeprecated()) { + $this->emitIssue( + Issue::DeprecatedClass, + $node->lineno, + (string)$class->getFQSEN(), + $class->getContext()->getFile(), + (string)$class->getContext()->getLineNumberStart(), + $class->getDeprecationReason() + ); + } + } + + $this->analyzeMethodVisibility( + $method, + $node + ); + + $this->analyzeCallToFunctionLike( + $method, + $node + ); + + foreach ($class_list as $class) { + if ($class->isAbstract() || $class->isInterface() || $class->isTrait()) { + // Check the full list of classes if any of the classes + // are abstract or interfaces. + $this->checkForInvalidNewType($node, $class_list); + break; + } + } + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (Exception $_) { + // If we can't figure out what kind of a call + // this is, don't worry about it + } + if ($this->isInNoOpPosition($node)) { + $this->warnNoopNew($node, $class_list); + } + + return $this->context; + } + + /** + * @param Node $node a node of type AST_NEW + * @param Clazz[] $class_list + */ + private function checkForInvalidNewType(Node $node, array $class_list): void + { + // This is either a string (new 'something'()) or a class name (new something()) + $class_node = $node->children['class']; + if (!$class_node instanceof Node) { + foreach ($class_list as $class) { + $this->warnIfInvalidClassForNew($class, $node); + } + return; + } + + if ($class_node->kind === ast\AST_NAME) { + $class_name = $class_node->children['name']; + if (\is_string($class_name) && \strcasecmp('static', $class_name) === 0) { + if ($this->isStaticGuaranteedToBeNonAbstract()) { + return; + } + } + foreach ($class_list as $class) { + $this->warnIfInvalidClassForNew($class, $class_node); + } + return; + } + foreach (UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $class_node)->getTypeSet() as $type) { + if ($type instanceof LiteralStringType) { + try { + $class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString($type->getValue()); + } catch (FQSENException $_) { + // Probably already emitted elsewhere, but emit anyway + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeExpectedObjectOrClassName, + $node->lineno, + ASTReverter::toShortString($node), + $type->getValue() + ); + continue; + } + if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) { + continue; + } + $class = $this->code_base->getClassByFQSEN($class_fqsen); + $this->warnIfInvalidClassForNew($class, $class_node); + } + } + } + + /** + * Given a call to `new static`, is the context likely to be guaranteed to be a non-abstract class? + */ + private function isStaticGuaranteedToBeNonAbstract(): bool + { + if (!$this->context->isInMethodScope()) { + return false; + } + // TODO: Could do a better job with closures inside of methods + $method = $this->context->getFunctionLikeInScope($this->code_base); + if (!($method instanceof Method)) { + if ($method instanceof Func && $method->isClosure()) { + // closures can be rebound + return true; + } + return false; + } + return !$method->isStatic(); + } + + private static function isStaticNameNode(Node $node, bool $allow_self): bool + { + if ($node->kind !== ast\AST_NAME) { + return false; + } + $name = $node->children['name']; + if (!\is_string($name)) { + return false; + } + return \strcasecmp($name, 'static') === 0 || ($allow_self && \strcasecmp($name, 'self') === 0); + } + + private function warnIfInvalidClassForNew(Clazz $class, Node $node): void + { + // Make sure we're not instantiating an abstract + // class + if ($class->isAbstract()) { + $this->emitIssue( + self::isStaticNameNode($node, false) ? Issue::TypeInstantiateAbstractStatic : Issue::TypeInstantiateAbstract, + $node->lineno, + (string)$class->getFQSEN() + ); + } elseif ($class->isInterface()) { + // Make sure we're not instantiating an interface + $this->emitIssue( + Issue::TypeInstantiateInterface, + $node->lineno, + (string)$class->getFQSEN() + ); + } elseif ($class->isTrait()) { + // Make sure we're not instantiating a trait + $this->emitIssue( + self::isStaticNameNode($node, true) ? Issue::TypeInstantiateTraitStaticOrSelf : Issue::TypeInstantiateTrait, + $node->lineno, + (string)$class->getFQSEN() + ); + } + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitInstanceof(Node $node): Context + { + try { + // Fetch the class list, and emit warnings as a side effect. + // TODO: Unify UnionTypeVisitor, AssignmentVisitor, and PostOrderAnalysisVisitor + (new ContextNode( + $this->code_base, + $this->context, + $node->children['class'] + ))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME, Issue::TypeInvalidInstanceof); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (CodeBaseException $exception) { + $this->emitIssueWithSuggestion( + Issue::UndeclaredClassInstanceof, + $node->lineno, + [(string)$exception->getFQSEN()], + IssueFixSuggester::suggestSimilarClassForGenericFQSEN( + $this->code_base, + $this->context, + $exception->getFQSEN(), + // Only suggest classes/interfaces for alternatives to instanceof checks. Don't suggest traits. + IssueFixSuggester::createFQSENFilterForClasslikeCategories($this->code_base, true, false, true) + ) + ); + } + + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitStaticCall(Node $node): Context + { + // Get the name of the method being called + $method_name = $node->children['method']; + + // Give up on things like Class::$var + if (!\is_string($method_name)) { + if ($method_name instanceof Node) { + $method_name = UnionTypeVisitor::anyStringLiteralForNode($this->code_base, $this->context, $method_name); + } + if (!\is_string($method_name)) { + $method_name_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['method']); + if (!$method_name_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeInvalidStaticMethodName, + $node->lineno, + $method_name_type + ); + } + return $this->context; + } + } + + // Get the name of the static class being referenced + $static_class = ''; + $class_node = $node->children['class']; + if (!($class_node instanceof Node)) { + $static_class = (string)$class_node; + } elseif ($class_node->kind === ast\AST_NAME) { + $static_class = (string)$class_node->children['name']; + } + + $method = $this->getStaticMethodOrEmitIssue($node, $method_name); + + if ($method === null) { + // Short circuit on a constructor being called statically + // on something other than 'parent' + if ($method_name === '__construct' && $static_class !== 'parent') { + $this->emitConstructorWarning($node, $static_class, $method_name); + } + return $this->context; + } + + try { + if ($method_name === '__construct') { + $this->checkNonAncestorConstructCall($node, $static_class, $method_name); + // Even if it exists, continue on and type check the arguments passed. + } + // If the method being called isn't actually static and it's + // not a call to parent::f from f, we may be in trouble. + if (!$method->isStatic() && !$this->canCallInstanceMethodFromContext($method, $static_class)) { + $class_list = (new ContextNode( + $this->code_base, + $this->context, + $node->children['class'] + ))->getClassList(); + + if (\count($class_list) > 0) { + $class = \array_values($class_list)[0]; + + $this->emitIssue( + Issue::StaticCallToNonStatic, + $node->lineno, + "{$class->getFQSEN()}::{$method_name}()", + $method->getFileRef()->getFile(), + (string)$method->getFileRef()->getLineNumberStart() + ); + } + } + + $this->analyzeMethodVisibility( + $method, + $node + ); + + // Make sure the parameters look good + $this->analyzeCallToFunctionLike( + $method, + $node + ); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (Exception $_) { + // If we can't figure out the class for this method + // call, cry YOLO and mark every method with that + // name with a reference. + if (Config::get_track_references() + && Config::getValue('dead_code_detection_prefer_false_negative') + ) { + foreach ($this->code_base->getMethodSetByName( + $method_name + ) as $method) { + $method->addReference($this->context); + } + } + + // If we can't figure out what kind of a call + // this is, don't worry about it + return $this->context; + } + return $this->context; + } + + private function canCallInstanceMethodFromContext(Method $method, string $static_class): bool + { + // Check if this is an instance method or closure of an instance method + if (!$this->context->getScope()->hasVariableWithName('this')) { + return false; + } + if (\in_array(\strtolower($static_class), ['parent', 'self', 'static'], true)) { + return true; + } + $calling_class_fqsen = $this->context->getClassFQSENOrNull(); + if ($calling_class_fqsen) { + $calling_class_type = $calling_class_fqsen->asType()->asExpandedTypes($this->code_base); + } else { + $calling_class_type = $this->context->getScope()->getVariableByName('this')->getUnionType()->asExpandedTypes($this->code_base); + } + // Allow calling its own methods and class's methods. + return $calling_class_type->hasType($method->getClassFQSEN()->asType()); + } + + /** + * Check calling A::__construct (where A is not parent) + */ + private function checkNonAncestorConstructCall( + Node $node, + string $static_class, + string $method_name + ): void { + // TODO: what about unanalyzable? + if ($node->children['class']->kind !== ast\AST_NAME) { + return; + } + // TODO: check for self/static/ and warn about recursion? + // TODO: Only allow calls to __construct from other constructors? + $found_ancestor_constructor = false; + if ($this->context->isInMethodScope()) { + try { + $possible_ancestor_type = UnionTypeVisitor::unionTypeFromClassNode( + $this->code_base, + $this->context, + $node->children['class'] + ); + } catch (FQSENException $e) { + $this->emitIssue( + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInCallable : Issue::InvalidFQSENInCallable, + $node->lineno, + $e->getFQSEN() + ); + return; + } + // If we can determine the ancestor type, and it's an parent/ancestor class, allow the call without warning. + // (other code should check visibility and existence and args of __construct) + + if (!$possible_ancestor_type->isEmpty()) { + // but forbid 'self::__construct', 'static::__construct' + $type = $this->context->getClassFQSEN()->asRealUnionType(); + if ($possible_ancestor_type->hasStaticType()) { + $this->emitIssue( + Issue::AccessOwnConstructor, + $node->lineno, + $static_class + ); + $found_ancestor_constructor = true; + } elseif ($type->asExpandedTypes($this->code_base)->canCastToUnionType($possible_ancestor_type)) { + if ($type->canCastToUnionType($possible_ancestor_type)) { + $this->emitIssue( + Issue::AccessOwnConstructor, + $node->lineno, + $static_class + ); + } + $found_ancestor_constructor = true; + } + } + } + + if (!$found_ancestor_constructor) { + // TODO: new issue type? + $this->emitConstructorWarning($node, $static_class, $method_name); + } + } + + /** + * TODO: change to a different issue type in a future phan release? + */ + private function emitConstructorWarning(Node $node, string $static_class, string $method_name): void + { + $this->emitIssue( + Issue::UndeclaredStaticMethod, + $node->lineno, + "{$static_class}::{$method_name}()" + ); + } + + /** + * gets the static method, or emits an issue. + * @param Node $node + * @param string $method_name - NOTE: The caller should convert constants/class constants/etc in $node->children['method'] to a string. + */ + private function getStaticMethodOrEmitIssue(Node $node, string $method_name): ?Method + { + try { + // Get a reference to the method being called + $result = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getMethod($method_name, true, true); + + // This didn't throw NonClassMethodCall + if (Config::get_strict_method_checking()) { + $this->checkForPossibleNonObjectAndNonClassInMethod($node, $method_name); + } + + return $result; + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (Exception $e) { + if ($e instanceof FQSENException) { + Issue::maybeEmit( + $this->code_base, + $this->context, + $e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike, + $node->lineno, + $e->getFQSEN() + ); + } + // We already checked for NonClassMethodCall + if (Config::get_strict_method_checking()) { + $this->checkForPossibleNonObjectAndNonClassInMethod($node, $method_name); + } + + // If we can't figure out the class for this method + // call, cry YOLO and mark every method with that + // name with a reference. + if (Config::get_track_references() + && Config::getValue('dead_code_detection_prefer_false_negative') + ) { + foreach ($this->code_base->getMethodSetByName( + $method_name + ) as $method) { + $method->addReference($this->context); + } + } + + // If we can't figure out what kind of a call + // this is, don't worry about it + } + return null; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitMethod(Node $node): Context + { + if (!$this->context->isInFunctionLikeScope()) { + throw new AssertionError("Must be in function-like scope to get method"); + } + + $method = $this->context->getFunctionLikeInScope($this->code_base); + + $return_type = $method->getUnionType(); + + if (!($method instanceof Method)) { + throw new AssertionError("Function found where method expected"); + } + + $has_interface_class = false; + try { + $class = $method->getClass($this->code_base); + $has_interface_class = $class->isInterface(); + + $this->checkForPHP4StyleConstructor($class, $method); + } catch (Exception $_) { + } + + if (!$method->isAbstract() + && !$method->isFromPHPDoc() + && !$has_interface_class + && !$return_type->isEmpty() + && !$method->hasReturn() + && !self::declOnlyThrows($node) + && !$return_type->hasType(VoidType::instance(false)) + && !$return_type->hasType(NullType::instance(false)) + ) { + $this->warnTypeMissingReturn($method, $node); + } + $this->checkForFunctionInterfaceIssues($node, $method); + + if ($method->hasReturn() && $method->isMagicAndVoid()) { + $this->emitIssue( + Issue::TypeMagicVoidWithReturn, + $node->lineno, + (string)$method->getFQSEN() + ); + } + + return $this->context; + } + + private function warnTypeMissingReturn(FunctionInterface $method, Node $node): void + { + $this->emitIssue( + $method->getRealReturnType()->isEmpty() ? Issue::TypeMissingReturn : Issue::TypeMissingReturnReal, + $node->lineno, + $method->getFQSEN(), + $method->getUnionType() + ); + } + /** + * Visit a node with kind `ast\AST_FUNC_DECL` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitFuncDecl(Node $node): Context + { + $method = + $this->context->getFunctionLikeInScope($this->code_base); + + if (\strcasecmp($method->getName(), '__autoload') === 0) { + $this->emitIssue( + Issue::CompatibleAutoload, + $node->lineno + ); + } + + $return_type = $method->getUnionType(); + + if (!$return_type->isEmpty() + && !$method->hasReturn() + && !self::declOnlyThrows($node) + && !$return_type->hasType(VoidType::instance(false)) + && !$return_type->hasType(NullType::instance(false)) + ) { + $this->warnTypeMissingReturn($method, $node); + } + + $this->checkForFunctionInterfaceIssues($node, $method); + + return $this->context; + } + + /** + * @suppress PhanPossiblyUndeclaredProperty + */ + private function checkForFunctionInterfaceIssues(Node $node, FunctionInterface $function): void + { + $parameters_seen = []; + foreach ($function->getParameterList() as $i => $parameter) { + if (isset($parameters_seen[$parameter->getName()])) { + $this->emitIssue( + Issue::ParamRedefined, + $node->lineno, + '$' . $parameter->getName() + ); + } else { + $parameters_seen[$parameter->getName()] = $i; + } + } + $params_node = $node->children['params']; + // @phan-suppress-next-line PhanUndeclaredProperty + if (isset($params_node->polyfill_has_trailing_comma)) { + $this->emitIssue( + Issue::CompatibleTrailingCommaParameterList, + end($params_node->children)->lineno ?? $params_node->lineno, + ASTReverter::toShortString($node) + ); + } + foreach ($params_node->children as $param) { + $this->checkUnionTypeCompatibility($param->children['type']); + } + $this->checkUnionTypeCompatibility($node->children['returnType']); + } + + private function checkUnionTypeCompatibility(?Node $type): void + { + if (!$type) { + return; + } + if (Config::get_closest_minimum_target_php_version_id() >= 80000) { + // Don't warn about using union types if the project dropped support for php versions older than 8.0 + return; + } + if ($type->kind === ast\AST_TYPE_UNION) { + // TODO: Warn about false|false, false|null, etc in php 8.0. + $this->emitIssue( + Issue::CompatibleUnionType, + $type->lineno, + ASTReverter::toShortString($type) + ); + return; + } + if ($type->kind === ast\AST_NULLABLE_TYPE) { + $inner_type = $type->children['type']; + if (!\is_object($inner_type)) { + // The polyfill will create param type nodes for function(? $x) + // Phan warns elsewhere. + return; + } + } else { + $inner_type = $type; + } + // echo \Phan\Debug::nodeToString($type) . "\n"; + if ($inner_type->kind === ast\AST_NAME) { + return; + } + if ($inner_type->kind !== ast\AST_TYPE) { + // e.g. ast\TYPE_UNION + $this->emitIssue( + Issue::InvalidNode, + $inner_type->lineno, + "Unsupported union type syntax " . ASTReverter::toShortString($inner_type) + ); + return; + } + if ($inner_type->flags === ast\flags\TYPE_STATIC) { + $this->emitIssue( + Issue::CompatibleStaticType, + $inner_type->lineno + ); + } elseif (\in_array($inner_type->flags, [ast\flags\TYPE_FALSE, ast\flags\TYPE_NULL], true)) { + $this->emitIssue( + Issue::InvalidNode, + $inner_type->lineno, + "Invalid union type " . ASTReverter::toShortTypeString($type) . " in element signature" + ); + } + } + + public function visitNullsafeMethodCall(Node $node): Context + { + $this->checkNullsafeOperatorCompatibility($node); + return $this->visitMethodCall($node); + } + + private function checkNullsafeOperatorCompatibility(Node $node): void + { + if (Config::get_closest_minimum_target_php_version_id() < 80000) { + $this->emitIssue( + Issue::CompatibleNullsafeOperator, + $node->lineno, + ASTReverter::toShortString($node) + ); + } + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitMethodCall(Node $node): Context + { + $method_name = $node->children['method']; + + if (!\is_string($method_name)) { + if ($method_name instanceof Node) { + $method_name = UnionTypeVisitor::anyStringLiteralForNode($this->code_base, $this->context, $method_name); + } + if (!\is_string($method_name)) { + $method_name_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['method']); + if (!$method_name_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::TypeInvalidMethodName, + $node->lineno, + $method_name_type + ); + } + return $this->context; + } + } + + try { + $method = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getMethod($method_name, false, true); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + return $this->context; + } catch (NodeException $_) { + // If we can't figure out the class for this method + // call, cry YOLO and mark every method with that + // name with a reference. + if (Config::get_track_references() + && Config::getValue('dead_code_detection_prefer_false_negative') + ) { + foreach ($this->code_base->getMethodSetByName( + $method_name + ) as $method) { + $method->addReference($this->context); + } + } + + // Swallow it + return $this->context; + } + + // We already checked for NonClassMethodCall + if (Config::get_strict_method_checking()) { + $this->checkForPossibleNonObjectInMethod($node, $method_name); + } + + $this->analyzeMethodVisibility( + $method, + $node + ); + + // Check the call for parameter and argument types + $this->analyzeCallToFunctionLike( + $method, + $node + ); + + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitArgList(Node $node): Context + { + $argument_name_set = []; + $has_unpack = false; + + foreach ($node->children as $i => $argument) { + if (!\is_int($i)) { + throw new AssertionError("Expected argument index to be an integer"); + } + if ($argument instanceof Node && $argument->kind === ast\AST_NAMED_ARG) { + if (Config::get_closest_minimum_target_php_version_id() < 80000) { + $this->emitIssue( + Issue::CompatibleNamedArgument, + $argument->lineno, + ASTReverter::toShortString($argument) + ); + } + ['name' => $argument_name, 'expr' => $argument_expression] = $argument->children; + if ($argument_expression === null) { + throw new AssertionError("Expected argument to have an expression"); + } + if (isset($argument_name_set[$argument_name])) { + $this->emitIssue( + Issue::DefinitelyDuplicateNamedArgument, + $argument->lineno, + ASTReverter::toShortString($argument), + ASTReverter::toShortString($argument_name_set[$argument_name]) + ); + } else { + $argument_name_set[$argument_name] = $argument; + } + } else { + $argument_expression = $argument; + } + if ($argument_name_set) { + if ($argument === $argument_expression) { + $this->emitIssue( + Issue::PositionalArgumentAfterNamedArgument, + $argument->lineno ?? $node->lineno, + ASTReverter::toShortString($argument), + ASTReverter::toShortString(\end($argument_name_set)) + ); + } + } + + + if (($argument->kind ?? 0) === ast\AST_UNPACK) { + $has_unpack = true; + } + } + // TODO: Make this a check that runs even without the $method object + if ($has_unpack && $argument_name_set) { + $this->emitIssue( + Issue::ArgumentUnpackingUsedWithNamedArgument, + $node->lineno, + ASTReverter::toShortString($node) + ); + } + // @phan-suppress-next-line PhanUndeclaredProperty + if (isset($node->polyfill_has_trailing_comma) && Config::get_closest_minimum_target_php_version_id() < 70300) { + $this->emitIssue( + Issue::CompatibleTrailingCommaArgumentList, + end($node->children)->lineno ?? $node->lineno, + ASTReverter::toShortString($node) + ); + } + return $this->context; + } + + private function checkForPossibleNonObjectInMethod(Node $node, string $method_name): void + { + $type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr'] ?? $node->children['class']); + if ($node->kind === ast\AST_NULLSAFE_METHOD_CALL && !$type->isNull() && !$type->isDefinitelyUndefined()) { + $type = $type->nonNullableClone(); + } + if ($type->containsDefiniteNonObjectType()) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::PossiblyNonClassMethodCall, + $node->lineno, + $method_name, + $type + ); + } + } + + private function checkForPossibleNonObjectAndNonClassInMethod(Node $node, string $method_name): void + { + $type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr'] ?? $node->children['class']); + if ($type->containsDefiniteNonObjectAndNonClassType()) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::PossiblyNonClassMethodCall, + $node->lineno, + $method_name, + $type + ); + } + } + + /** + * Visit a node with kind `ast\AST_DIM` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitDim(Node $node): Context + { + $code_base = $this->code_base; + $context = $this->context; + // Check the dimension type to trigger PhanUndeclaredVariable, etc. + /* $dim_type = */ + UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['dim'], + true + ); + $this->analyzeNoOp($node, Issue::NoopArrayAccess); + + $flags = $node->flags; + if ($flags & ast\flags\DIM_ALTERNATIVE_SYNTAX) { + $this->emitIssue( + Issue::CompatibleDimAlternativeSyntax, + $node->children['dim']->lineno ?? $node->lineno, + ASTReverter::toShortString($node) + ); + } + if ($flags & PhanAnnotationAdder::FLAG_IGNORE_NULLABLE_AND_UNDEF) { + return $context; + } + // Check the array type to trigger TypeArraySuspicious + try { + /* $array_type = */ + UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node, + false + ); + // TODO: check if array_type has array but not ArrayAccess. + // If that is true, then assert that $dim_type can cast to `int|string` + } catch (IssueException $_) { + // Detect this elsewhere, e.g. want to detect PhanUndeclaredVariableDim but not PhanUndeclaredVariable + } + return $context; + } + + /** + * Visit a node with kind `ast\AST_CONDITIONAL` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + * + * @suppress PhanAccessMethodInternal + */ + public function visitConditional(Node $node): Context + { + if ($this->isInNoOpPosition($node)) { + if (!ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $node->children['true']) && + !ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $node->children['false'])) { + $this->emitIssue( + Issue::NoopTernary, + $node->lineno + ); + } + } + $cond = $node->children['cond']; + if ($cond instanceof Node && $cond->kind === ast\AST_CONDITIONAL) { + $this->checkDeprecatedUnparenthesizedConditional($node, $cond); + } + return $this->context; + } + + /** + * Visit a node with kind `ast\AST_MATCH` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * analyzing the node + * + * @suppress PhanAccessMethodInternal + */ + public function visitMatch(Node $node): Context + { + if (Config::get_closest_minimum_target_php_version_id() < 80000) { + $this->emitIssue( + Issue::CompatibleMatchExpression, + $node->lineno, + ASTReverter::toShortString($node) + ); + } + if ($this->isInNoOpPosition($node)) { + if (!ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $node->children['stmts'])) { + $this->emitIssue( + Issue::NoopMatchExpression, + $node->lineno, + ASTReverter::toShortString($node) + ); + } + } + return $this->context; + } + + /** + * @param Node $node a node of kind AST_CONDITIONAL with a condition that is also of kind AST_CONDITIONAL + */ + private function checkDeprecatedUnparenthesizedConditional(Node $node, Node $cond): void + { + if ($cond->flags & flags\PARENTHESIZED_CONDITIONAL) { + // The condition is unambiguously parenthesized. + return; + } + // @phan-suppress-next-line PhanUndeclaredProperty + if (\PHP_VERSION_ID < 70400 && !isset($cond->is_not_parenthesized)) { + // This is from the native parser in php 7.3 or earlier. + // We don't know whether or not the AST is parenthesized. + return; + } + if (isset($cond->children['true'])) { + if (isset($node->children['true'])) { + $description = 'a ? b : c ? d : e'; + $first_suggestion = '(a ? b : c) ? d : e'; + $second_suggestion = 'a ? b : (c ? d : e)'; + } else { + $description = 'a ? b : c ?: d'; + $first_suggestion = '(a ? b : c) ?: d'; + $second_suggestion = 'a ? b : (c ?: d)'; + } + } else { + if (isset($node->children['true'])) { + $description = 'a ?: b ? c : d'; + $first_suggestion = '(a ?: b) ? c : d'; + $second_suggestion = 'a ?: (b ? c : d)'; + } else { + // This is harmless - (a ?: b) ?: c always produces the same result and side + // effects as a ?: (b ?: c). + // Don't warn. + return; + } + } + $this->emitIssue( + Issue::CompatibleUnparenthesizedTernary, + $node->lineno, + $description, + $first_suggestion, + $second_suggestion + ); + } + + /** + * @param list $parent_node_list + * @return bool true if the union type should skip analysis due to being the left-hand side expression of an assignment + * We skip checks for $x['key'] being valid in expressions such as `$x['key']['key2']['key3'] = 'value';` + * because those expressions will create $x['key'] as a side effect. + * + * Precondition: $parent_node->kind === ast\AST_DIM && $parent_node->children['expr'] is $node + */ + private static function shouldSkipNestedAssignDim(array $parent_node_list): bool + { + $cur_parent_node = \end($parent_node_list); + for (;; $cur_parent_node = $prev_parent_node) { + $prev_parent_node = \prev($parent_node_list); + if (!$prev_parent_node instanceof Node) { + throw new AssertionError('Unexpected end of parent nodes seen in ' . __METHOD__); + } + switch ($prev_parent_node->kind) { + case ast\AST_DIM: + if ($prev_parent_node->children['expr'] !== $cur_parent_node) { + return false; + } + break; + case ast\AST_ASSIGN: + case ast\AST_ASSIGN_REF: + return $prev_parent_node->children['var'] === $cur_parent_node; + case ast\AST_ARRAY_ELEM: + $prev_parent_node = \prev($parent_node_list); // this becomes AST_ARRAY + break; + case ast\AST_ARRAY: + break; + default: + return false; + } + } + } + + public function visitStaticProp(Node $node): Context + { + return $this->analyzeProp($node, true); + } + + public function visitProp(Node $node): Context + { + return $this->analyzeProp($node, false); + } + + public function visitNullsafeProp(Node $node): Context + { + $this->checkNullsafeOperatorCompatibility($node); + return $this->analyzeProp($node, false); + } + + /** + * Default visitor for node kinds that do not have + * an overriding method + * + * @param Node $node (@phan-unused-param) + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitClone(Node $node): Context + { + $type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node->children['expr'], + true + ); + if ($type->isEmpty()) { + return $this->context; + } + if (!$type->hasPossiblyObjectTypes()) { + $this->emitIssue( + Issue::TypeInvalidCloneNotObject, + $node->children['expr']->lineno ?? $node->lineno, + $type + ); + } elseif (Config::get_strict_param_checking()) { + if ($type->containsNullable() || !$type->canStrictCastToUnionType($this->code_base, ObjectType::instance(false)->asPHPDocUnionType())) { + $this->emitIssue( + Issue::TypePossiblyInvalidCloneNotObject, + $node->children['expr']->lineno ?? $node->lineno, + $type + ); + } + } + + return $this->context; + } + + /** + * Analyze a node with kind `ast\AST_PROP` or `ast\AST_STATIC_PROP` + * + * @param Node $node + * A node of the type indicated by the method name that we'd + * like to figure out the type that it produces. + * + * @param bool $is_static + * True if fetching a static property. + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function analyzeProp(Node $node, bool $is_static): Context + { + $exception_or_null = null; + + try { + $property = (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getProperty($is_static); + + // Mark that this property has been referenced from + // this context + if (Config::get_track_references()) { + $this->trackPropertyReference($property, $node); + } + } catch (IssueException $exception) { + // We'll check out some reasons it might not exist + // before logging the issue + $exception_or_null = $exception; + } catch (Exception $_) { + // Swallow any exceptions. We'll catch it later. + } + + if (isset($property)) { + // TODO could be more specific about checking if this is a magic property + // Right now it warns if it is magic but (at)property is used, etc. + $this->analyzeNoOp($node, Issue::NoopProperty); + } else { + $expr_or_class_node = $node->children['expr'] ?? $node->children['class']; + if ($expr_or_class_node === null) { + throw new AssertionError( + "Property nodes must either have an expression or class" + ); + } + + $class_list = []; + try { + // Get the set of classes that are being referenced + $class_list = (new ContextNode( + $this->code_base, + $this->context, + $expr_or_class_node + ))->getClassList( + true, + $is_static ? ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME : ContextNode::CLASS_LIST_ACCEPT_OBJECT, + null, + false + ); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } + + if (!$is_static) { + // Find out of any of them have a __get magic method + // (Only check if looking for instance properties) + $has_getter = $this->hasGetter($class_list); + + // If they don't, then analyze for No-ops. + if (!$has_getter) { + $this->analyzeNoOp($node, Issue::NoopProperty); + + if ($exception_or_null instanceof IssueException) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception_or_null->getIssueInstance() + ); + } + } + } else { + if ($exception_or_null instanceof IssueException) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception_or_null->getIssueInstance() + ); + } + } + } + + return $this->context; + } + + /** @param Clazz[] $class_list */ + private function hasGetter(array $class_list): bool + { + foreach ($class_list as $class) { + if ($class->hasGetMethod($this->code_base)) { + return true; + } + } + return false; + } + + private function trackPropertyReference(Property $property, Node $node): void + { + $property->addReference($this->context); + if (!$property->hasReadReference() && !$this->isAssignmentOrNestedAssignment($node)) { + $property->setHasReadReference(); + } + if (!$property->hasWriteReference() && $this->isAssignmentOrNestedAssignmentOrModification($node) !== false) { + $property->setHasWriteReference(); + } + } + + /** + * @return ?bool + * - false if this is a read reference + * - false for modifications such as $x++ + * - true if this is a write reference + * - null if this is both, e.g. $a =& $b for $a and $b + */ + private function isAssignmentOrNestedAssignment(Node $node): ?bool + { + $parent_node_list = $this->parent_node_list; + $parent_node = \end($parent_node_list); + if (!$parent_node instanceof Node) { + // impossible + return false; + } + $parent_kind = $parent_node->kind; + // E.g. analyzing [$x] in [$x] = expr() + while ($parent_kind === ast\AST_ARRAY_ELEM) { + if ($parent_node->children['value'] !== $node) { + // e.g. analyzing `$v = [$x => $y];` for $x + return false; + } + \array_pop($parent_node_list); // pop AST_ARRAY_ELEM + $node = \array_pop($parent_node_list); // AST_ARRAY + $parent_node = \array_pop($parent_node_list); + if (!$parent_node instanceof Node) { + // impossible + return false; + } + $parent_kind = $parent_node->kind; + } + if ($parent_kind === ast\AST_DIM) { + return $parent_node->children['expr'] === $node && $this->shouldSkipNestedAssignDim($parent_node_list); + } elseif ($parent_kind === ast\AST_ASSIGN || $parent_kind === ast\AST_ASSIGN_OP) { + return $parent_node->children['var'] === $node; + } elseif ($parent_kind === ast\AST_ASSIGN_REF) { + return null; + } + return false; + } + + // An incomplete list of known parent node kinds that simultaneously read and write the given expression + // TODO: ASSIGN_OP? + private const READ_AND_WRITE_KINDS = [ + ast\AST_PRE_INC, + ast\AST_PRE_DEC, + ast\AST_POST_INC, + ast\AST_POST_DEC, + ]; + + /** + * @return ?bool + * - false if this is a read reference + * - true if this is a write reference + * - true if this is a modification such as $x++ + * - null if this is both, e.g. $a =& $b for $a and $b + */ + private function isAssignmentOrNestedAssignmentOrModification(Node $node): ?bool + { + $parent_node_list = $this->parent_node_list; + $parent_node = \end($parent_node_list); + if (!$parent_node instanceof Node) { + // impossible + return false; + } + $parent_kind = $parent_node->kind; + // E.g. analyzing [$x] in [$x] = expr() + while ($parent_kind === ast\AST_ARRAY_ELEM) { + if ($parent_node->children['value'] !== $node) { + // e.g. analyzing `$v = [$x => $y];` for $x + return false; + } + \array_pop($parent_node_list); // pop AST_ARRAY_ELEM + $node = \array_pop($parent_node_list); // AST_ARRAY + $parent_node = \array_pop($parent_node_list); + if (!$parent_node instanceof Node) { + // impossible + return false; + } + $parent_kind = $parent_node->kind; + } + if ($parent_kind === ast\AST_DIM) { + return $parent_node->children['expr'] === $node && self::shouldSkipNestedAssignDim($parent_node_list); + } elseif ($parent_kind === ast\AST_ASSIGN || $parent_kind === ast\AST_ASSIGN_OP) { + return $parent_node->children['var'] === $node; + } elseif ($parent_kind === ast\AST_ASSIGN_REF) { + return null; + } else { + return \in_array($parent_kind, self::READ_AND_WRITE_KINDS, true); + } + } + + /** + * Analyze whether a method is callable + * + * @param Method $method + * @param Node $node + */ + private function analyzeMethodVisibility( + Method $method, + Node $node + ): void { + if ($method->isPublic()) { + return; + } + if ($method->isAccessibleFromClass($this->code_base, $this->context->getClassFQSENOrNull())) { + return; + } + if ($method->isPrivate()) { + $has_call_magic_method = !$method->isStatic() + && $method->getDefiningClass($this->code_base)->hasMethodWithName($this->code_base, '__call', true); + + $this->emitIssue( + $has_call_magic_method ? + Issue::AccessMethodPrivateWithCallMagicMethod : Issue::AccessMethodPrivate, + $node->lineno, + (string)$method->getFQSEN(), + $method->getFileRef()->getFile(), + (string)$method->getFileRef()->getLineNumberStart() + ); + } else { + if (Clazz::isAccessToElementOfThis($node)) { + return; + } + $has_call_magic_method = !$method->isStatic() + && $method->getDefiningClass($this->code_base)->hasMethodWithName($this->code_base, '__call', true); + + $this->emitIssue( + $has_call_magic_method ? + Issue::AccessMethodProtectedWithCallMagicMethod : Issue::AccessMethodProtected, + $node->lineno, + (string)$method->getFQSEN(), + $method->getFileRef()->getFile(), + (string)$method->getFileRef()->getLineNumberStart() + ); + } + } + + /** + * Analyze the parameters and arguments for a call + * to the given method or function + * + * @param FunctionInterface $method + * @param Node $node + */ + private function analyzeCallToFunctionLike( + FunctionInterface $method, + Node $node + ): void { + $code_base = $this->code_base; + $context = $this->context; + + $method->addReference($context); + + // Create variables for any pass-by-reference + // parameters + $argument_list = $node->children['args']->children; + foreach ($argument_list as $i => $argument) { + if (!$argument instanceof Node) { + continue; + } + + $parameter = $method->getParameterForCaller($i); + if (!$parameter) { + continue; + } + + // If pass-by-reference, make sure the variable exists + // or create it if it doesn't. + if ($parameter->isPassByReference()) { + $this->createPassByReferenceArgumentInCall($method, $argument, $parameter, $method->getRealParameterForCaller($i)); + } + } + + // Confirm the argument types are clean + ArgumentType::analyze( + $method, + $node, + $context, + $code_base + ); + + // Take another pass over pass-by-reference parameters + // and assign types to passed in variables + foreach ($argument_list as $i => $argument) { + if (!$argument instanceof Node) { + continue; + } + $parameter = $method->getParameterForCaller($i); + + if (!$parameter) { + continue; + } + + $kind = $argument->kind; + if ($kind === ast\AST_CLOSURE) { + if (Config::get_track_references()) { + $this->trackReferenceToClosure($argument); + } + } + + // If the parameter is pass-by-reference and we're + // passing a variable in, see if we should pass + // the parameter and variable types to each other + if ($parameter->isPassByReference()) { + self::analyzePassByReferenceArgument( + $code_base, + $context, + $argument, + $argument_list, + $method, + $parameter, + $method->getRealParameterForCaller($i), + $i + ); + } + } + + // If we're in quick mode, don't retest methods based on + // parameter types passed in + if (Config::get_quick_mode()) { + return; + } + + // Don't re-analyze recursive methods. That doesn't go + // well. + if ($context->isInFunctionLikeScope() + && $method->getFQSEN() === $context->getFunctionLikeFQSEN() + ) { + $this->checkForInfiniteRecursion($node, $method); + return; + } + + if (!$method->needsRecursiveAnalysis()) { + return; + } + + // Re-analyze the method with the types of the arguments + // being passed in. + $this->analyzeMethodWithArgumentTypes( + $node->children['args'], + $method + ); + } + + /** + * @param Parameter $parameter the parameter types inferred from combination of real and union type + * + * @param ?Parameter $real_parameter the real parameter type from the type signature + */ + private function createPassByReferenceArgumentInCall(FunctionInterface $method, Node $argument, Parameter $parameter, ?Parameter $real_parameter): void + { + if ($argument->kind === ast\AST_VAR) { + // We don't do anything with the new variable; just create it + // if it doesn't exist + try { + $variable = (new ContextNode( + $this->code_base, + $this->context, + $argument + ))->getOrCreateVariableForReferenceParameter($parameter, $real_parameter); + $variable_union_type = $variable->getUnionType(); + if ($variable_union_type->hasRealTypeSet()) { + // TODO: Do a better job handling the large number of edge cases + // - e.g. infer that stream_select will convert non-empty arrays to possibly empty arrays, while the result continues to have a real type of array. + if ($method->getContext()->isPHPInternal() && \in_array($parameter->getReferenceType(), [Parameter::REFERENCE_IGNORED, Parameter::REFERENCE_READ_WRITE], true)) { + if (\preg_match('/shuffle|sort|array_(unshift|shift|push|pop|splice)/i', $method->getName())) { + // This use case is probably handled by MiscParamPlugin + return; + } + } + $variable->setUnionType($variable->getUnionType()->eraseRealTypeSetRecursively()); + } + } catch (NodeException $_) { + return; + } + } elseif ($argument->kind === ast\AST_STATIC_PROP + || $argument->kind === ast\AST_PROP + ) { + $property_name = $argument->children['prop']; + if ($property_name instanceof Node) { + $property_name = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $property_name)->asSingleScalarValueOrNullOrSelf(); + } + + // Only try to handle known literals or strings, ignore properties with names that couldn't be inferred. + if (\is_string($property_name)) { + // We don't do anything with it; just create it + // if it doesn't exist + try { + $property = (new ContextNode( + $this->code_base, + $this->context, + $argument + ))->getOrCreateProperty($property_name, $argument->kind === ast\AST_STATIC_PROP); + $property->setHasWriteReference(); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $this->code_base, + $this->context, + $exception->getIssueInstance() + ); + } catch (Exception $_) { + // If we can't figure out what kind of a call + // this is, don't worry about it + } + } + } + } + + /** + * @param list $argument_list the arguments of the invocation, containing the pass by reference argument + * + * @param Parameter $parameter the parameter types inferred from combination of real and union type + * + * @param ?Parameter $real_parameter the real parameter type from the type signature + */ + private static function analyzePassByReferenceArgument( + CodeBase $code_base, + Context $context, + Node $argument, + array $argument_list, + FunctionInterface $method, + Parameter $parameter, + ?Parameter $real_parameter, + int $parameter_offset + ): void { + $variable = null; + $kind = $argument->kind; + if ($kind === ast\AST_VAR) { + try { + $variable = (new ContextNode( + $code_base, + $context, + $argument + ))->getOrCreateVariableForReferenceParameter($parameter, $real_parameter); + } catch (NodeException $_) { + // E.g. `function_accepting_reference(${$varName})` - Phan can't analyze outer type of ${$varName} + return; + } + } elseif ($kind === ast\AST_STATIC_PROP + || $kind === ast\AST_PROP + ) { + $property_name = $argument->children['prop']; + if ($property_name instanceof Node) { + $property_name = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $property_name)->asSingleScalarValueOrNullOrSelf(); + } + + // Only try to handle property names that could be inferred. + if (\is_string($property_name)) { + // We don't do anything with it; just create it + // if it doesn't exist + try { + $variable = (new ContextNode( + $code_base, + $context, + $argument + ))->getOrCreateProperty($property_name, $argument->kind === ast\AST_STATIC_PROP); + $variable->addReference($context); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $code_base, + $context, + $exception->getIssueInstance() + ); + } catch (Exception $_) { + // If we can't figure out what kind of a call + // this is, don't worry about it + } + } + } + + if ($variable) { + $set_variable_type = static function (UnionType $new_type) use ($code_base, $context, $variable, $argument): void { + if ($variable instanceof Variable) { + $variable = clone($variable); + AssignmentVisitor::analyzeSetUnionTypeInContext($code_base, $context, $variable, $new_type, $argument); + $context->addScopeVariable($variable); + } else { + // This is a Property. Add any compatible new types to the type of the property. + AssignmentVisitor::addTypesToPropertyStandalone($code_base, $context, $variable, $new_type); + } + }; + if ($variable instanceof Property) { + // TODO: If @param-out is ever supported, then use that type to check + self::checkPassingPropertyByReference($code_base, $context, $method, $parameter, $argument, $variable, $parameter_offset); + } + switch ($parameter->getReferenceType()) { + case Parameter::REFERENCE_WRITE_ONLY: + self::analyzeWriteOnlyReference($code_base, $context, $method, $set_variable_type, $argument_list, $parameter); + break; + case Parameter::REFERENCE_READ_WRITE: + $reference_parameter_type = $parameter->getNonVariadicUnionType(); + $variable_type = $variable->getUnionType(); + if ($variable_type->isEmpty()) { + // if Phan doesn't know the variable type, + // then guess that the variable is the type of the reference + // when analyzing the following statements. + $set_variable_type($reference_parameter_type); + } elseif (!$variable_type->canCastToUnionType($reference_parameter_type)) { + // Phan already warned about incompatible types. + // But analyze the following statements as if it could have been the type expected, + // to reduce false positives. + $set_variable_type($variable->getUnionType()->withUnionType( + $reference_parameter_type + )); + } + // don't modify - assume the function takes the same type in that it returns, + // and we want to preserve generic array types for sorting functions (May change later on) + // TODO: Check type compatibility earlier, and don't modify? + break; + case Parameter::REFERENCE_IGNORED: + // Pretend this reference doesn't modify the passed in argument. + break; + case Parameter::REFERENCE_DEFAULT: + default: + $reference_parameter_type = $parameter->getNonVariadicUnionType(); + // We have no idea what type of reference this is. + // Probably user defined code. + $set_variable_type($variable->getUnionType()->withUnionType( + $reference_parameter_type + )); + break; + } + } + } + + /** + * @param Closure(UnionType):void $set_variable_type + * @param list $argument_list + */ + private static function analyzeWriteOnlyReference( + CodeBase $code_base, + Context $context, + FunctionInterface $method, + Closure $set_variable_type, + array $argument_list, + Parameter $parameter + ): void { + switch ($method->getFQSEN()->__toString()) { + case '\preg_match': + $set_variable_type( + RegexAnalyzer::getPregMatchUnionType($code_base, $context, $argument_list) + ); + return; + case '\preg_match_all': + $set_variable_type( + RegexAnalyzer::getPregMatchAllUnionType($code_base, $context, $argument_list) + ); + return; + default: + $reference_parameter_type = $parameter->getNonVariadicUnionType(); + + // The previous value is being ignored, and being replaced. + // FIXME: Do something different for properties, e.g. limit it to a scope, combine with old property, etc. + $set_variable_type( + $reference_parameter_type + ); + } + } + + private function trackReferenceToClosure(Node $argument): void + { + try { + $inner_context = $this->context->withLineNumberStart($argument->lineno); + $method = (new ContextNode( + $this->code_base, + $inner_context, + $argument + ))->getClosure(); + + $method->addReference($inner_context); + } catch (Exception $_) { + // Swallow it + } + } + + /** + * Replace the method's parameter types with the argument + * types and re-analyze the method. + * + * This is used when analyzing callbacks and closures, e.g. in array_map. + * + * @param list $argument_types + * An AST node listing the arguments + * + * @param FunctionInterface $method + * The method or function being called + * @see analyzeMethodWithArgumentTypes (Which takes AST nodes) + * + * @param list $arguments + * An array of arguments to the callable, to analyze references. + * + * @param bool $erase_old_return_type + * Whether $method's old return type should be erased + * to use the newly inferred type based on $argument_types. + * (useful for array_map, etc) + */ + public function analyzeCallableWithArgumentTypes( + array $argument_types, + FunctionInterface $method, + array $arguments = [], + bool $erase_old_return_type = false + ): void { + $method = $this->findDefiningMethod($method); + if (!$method->needsRecursiveAnalysis()) { + return; + } + + // Don't re-analyze recursive methods. That doesn't go well. + if ($this->context->isInFunctionLikeScope() + && $method->getFQSEN() === $this->context->getFunctionLikeFQSEN() + ) { + return; + } + foreach ($argument_types as $i => $type) { + $argument_types[$i] = $type->withStaticResolvedInContext($this->context); + } + + $original_method_scope = $method->getInternalScope(); + $method->setInternalScope(clone($original_method_scope)); + try { + // Even though we don't modify the parameter list, we still need to know the types + // -- as an optimization, we don't run quick mode again if the types didn't change? + $parameter_list = \array_map(static function (Parameter $parameter): Parameter { + return clone($parameter); + }, $method->getParameterList()); + + foreach ($parameter_list as $i => $parameter_clone) { + if (!isset($argument_types[$i]) && $parameter_clone->hasDefaultValue()) { + $parameter_type = $parameter_clone->getDefaultValueType()->withRealTypeSet($parameter_clone->getNonVariadicUnionType()->getRealTypeSet()); + if ($parameter_type->isType(NullType::instance(false))) { + // Treat a parameter default of null the same way as passing null to that parameter + // (Add null to the list of possibilities) + $parameter_clone->addUnionType($parameter_type); + } else { + // For other types (E.g. string), just replace the union type. + $parameter_clone->setUnionType($parameter_type); + } + } + + // Add the parameter to the scope + $method->getInternalScope()->addVariable( + $parameter_clone->asNonVariadic() + ); + + // If there's no parameter at that offset, we may be in + // a ParamTooMany situation. That is caught elsewhere. + if (!isset($argument_types[$i]) + || !$parameter_clone->hasEmptyNonVariadicType() + ) { + continue; + } + + $this->updateParameterTypeByArgument( + $method, + $parameter_clone, + $arguments[$i] ?? null, + $argument_types, + $parameter_list, + $i + ); + } + foreach ($parameter_list as $parameter_clone) { + if ($parameter_clone->isVariadic()) { + // We're using this parameter clone to analyze the **inside** of the method, it's never seen on the outside. + // Convert it immediately. + // TODO: Add tests of variadic references, fix those if necessary. + $method->getInternalScope()->addVariable( + $parameter_clone->cloneAsNonVariadic() + ); + } + } + + // Now that we know something about the parameters used + // to call the method, we can reanalyze the method with + // the types of the parameter + if ($erase_old_return_type) { + $method->setUnionType($method->getOriginalReturnType()); + } + $method->analyzeWithNewParams($method->getContext(), $this->code_base, $parameter_list); + } finally { + $method->setInternalScope($original_method_scope); + } + } + + /** + * Replace the method's parameter types with the argument + * types and re-analyze the method. + * + * @param Node $argument_list_node + * An AST node listing the arguments + * + * @param FunctionInterface $method + * The method or function being called + * Precondition: $method->needsRecursiveAnalysis() === false + * + * @return void + * + * TODO: deduplicate code. + */ + private function analyzeMethodWithArgumentTypes( + Node $argument_list_node, + FunctionInterface $method + ): void { + $method = $this->findDefiningMethod($method); + $original_method_scope = $method->getInternalScope(); + $method->setInternalScope(clone($original_method_scope)); + $method_context = $method->getContext(); + + try { + // Even though we don't modify the parameter list, we still need to know the types + // -- as an optimization, we don't run quick mode again if the types didn't change? + $parameter_list = \array_map(static function (Parameter $parameter): Parameter { + return $parameter->cloneAsNonVariadic(); + }, $method->getParameterList()); + + // always resolve all arguments outside of quick mode to detect undefined variables, other problems in call arguments. + // Fixes https://github.com/phan/phan/issues/583 + $argument_types = []; + foreach ($argument_list_node->children as $i => $argument) { + // Determine the type of the argument at position $i + $argument_types[$i] = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $argument, + true + )->withStaticResolvedInContext($this->context)->eraseRealTypeSetRecursively(); + } + + foreach ($parameter_list as $i => $parameter_clone) { + $argument = $argument_list_node->children[$i] ?? null; + + if ($argument === null + && $parameter_clone->hasDefaultValue() + ) { + $parameter_type = $parameter_clone->getDefaultValueType()->withRealTypeSet($parameter_clone->getNonVariadicUnionType()->getRealTypeSet()); + if ($parameter_type->isType(NullType::instance(false))) { + // Treat a parameter default of null the same way as passing null to that parameter + // (Add null to the list of possibilities) + $parameter_clone->addUnionType($parameter_type); + } else { + // For other types (E.g. string), just replace the union type. + $parameter_clone->setUnionType($parameter_type); + } + } + + // Add the parameter to the scope + // TODO: asNonVariadic()? + $method->getInternalScope()->addVariable( + $parameter_clone + ); + + // If there's no parameter at that offset, we may be in + // a ParamTooMany situation. That is caught elsewhere. + if ($argument === null) { + continue; + } + + // If there's a declared type for the parameter, + // then don't bother overriding the type to analyze the function/method body (unless the parameter is pass-by-reference) + // Note that $parameter_clone was converted to a non-variadic clone, so the getNonVariadicUnionType returns an array. + if (!$parameter_clone->hasEmptyNonVariadicType() && !$parameter_clone->isPassByReference()) { + continue; + } + + $this->updateParameterTypeByArgument( + $method, + $parameter_clone, + $argument, + $argument_types, + $parameter_list, + $i + ); + } + foreach ($parameter_list as $parameter_clone) { + if ($parameter_clone->isVariadic()) { + // We're using this parameter clone to analyze the **inside** of the method, it's never seen on the outside. + // Convert it immediately. + // TODO: Add tests of variadic references, fix those if necessary. + $method->getInternalScope()->addVariable( + $parameter_clone->cloneAsNonVariadic() + ); + } + } + + // Now that we know something about the parameters used + // to call the method, we can reanalyze the method with + // the types of the parameter + $method->analyzeWithNewParams($method_context, $this->code_base, $parameter_list); + } finally { + $method->setInternalScope($original_method_scope); + } + } + + private function findDefiningMethod(FunctionInterface $method): FunctionInterface + { + if ($method instanceof Method) { + $defining_fqsen = $method->getDefiningFQSEN(); + if ($method->getFQSEN() !== $defining_fqsen) { + // This should always happen, unless in the language server mode + if ($this->code_base->hasMethodWithFQSEN($defining_fqsen)) { + return $this->code_base->getMethodByFQSEN($defining_fqsen); + } + } + } + return $method; + } + + /** + * Check if $argument_list_node calling itself is likely to be a case of infinite recursion. + * This is based on heuristics, and will not catch all cases. + */ + private function checkForInfiniteRecursion(Node $node, FunctionInterface $method): void + { + $argument_list_node = $node->children['args']; + $kind = $node->kind; + if ($kind === ast\AST_METHOD_CALL || $kind === ast\AST_NULLSAFE_METHOD_CALL) { + $expr = $node->children['expr']; + if (!$expr instanceof Node || $expr->kind !== ast\AST_VAR || $expr->children['name'] !== 'this') { + return; + } + } + $nearest_function_like = null; + foreach ($this->parent_node_list as $c) { + if (\in_array($c->kind, [ast\AST_FUNC_DECL, ast\AST_METHOD, ast\AST_CLOSURE], true)) { + $nearest_function_like = $c; + } + } + if (!$nearest_function_like) { + return; + } + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null + if (ReachabilityChecker::willUnconditionallyBeReached($nearest_function_like->children['stmts'], $argument_list_node)) { + $this->emitIssue( + Issue::InfiniteRecursion, + $node->lineno, + $method->getNameForIssue() + ); + return; + } + $this->checkForInfiniteRecursionWithSameArgs($node, $method); + } + + private function checkForInfiniteRecursionWithSameArgs(Node $node, FunctionInterface $method): void + { + $argument_list_node = $node->children['args']; + $parameter_list = $method->getParameterList(); + if (\count($argument_list_node->children) !== \count($parameter_list)) { + return; + } + if (\count($argument_list_node->children) === 0) { + $this->emitIssue( + Issue::PossibleInfiniteRecursionSameParams, + $node->lineno, + $method->getNameForIssue() + ); + return; + } + // TODO also check AST_UNPACK against variadic + $arg_names = []; + foreach ($argument_list_node->children as $i => $arg) { + if (!$arg instanceof Node) { + return; + } + $is_unpack = false; + if ($arg->kind === ast\AST_UNPACK) { + $arg = $arg->children['expr']; + if (!$arg instanceof Node) { + return; + } + $is_unpack = true; + } + if ($arg->kind !== ast\AST_VAR) { + return; + } + $arg_name = $arg->children['name']; + if (!\is_string($arg_name)) { + return; + } + $param = $parameter_list[$i]; + if ($param->getName() !== $arg_name || $param->isVariadic() !== $is_unpack) { + return; + } + $arg_names[] = $arg_name; + } + $outer_scope = $method->getInternalScope(); + $current_scope = $this->context->getScope(); + foreach ($arg_names as $arg_name) { + if (!$current_scope->hasVariableWithName($arg_name) || !$outer_scope->hasVariableWithName($arg_name)) { + return; + } + } + // @phan-suppress-next-line PhanUndeclaredProperty + $node->check_infinite_recursion = [$arg_names, $method->getNameForIssue()]; + } + + /** + * @param FunctionInterface $method + * The method that we're updating parameter types for + * + * @param Parameter $parameter + * The parameter that we're updating + * + * @param Node|mixed $argument + * The argument whose type we'd like to replace the + * parameter type with. + * + * @param list $argument_types + * The type of arguments + * + * @param list &$parameter_list + * The parameter list - types are modified by reference + * + * @param int $parameter_offset + * The offset of the parameter on the method's + * signature. + */ + private function updateParameterTypeByArgument( + FunctionInterface $method, + Parameter $parameter, + $argument, + array $argument_types, + array &$parameter_list, + int $parameter_offset + ): void { + $argument_type = $argument_types[$parameter_offset]; + if ($parameter->isVariadic()) { + for ($i = $parameter_offset + 1; $i < \count($argument_types); $i++) { + $argument_type = $argument_type->withUnionType($argument_types[$i]); + } + } + // $argument_type = $this->filterValidArgumentTypes($argument_type, $non_variadic_parameter_type); + if (!$argument_type->isEmpty()) { + // Then set the new type on that parameter based + // on the argument's type. We'll use this to + // retest the method with the passed in types + // TODO: if $argument_type is non-empty and !isType(NullType), instead use setUnionType? + + if ($parameter->isCloneOfVariadic()) { + // For https://github.com/phan/phan/issues/1525 : Collapse array shapes into generic arrays before recursively analyzing a method. + if ($parameter->hasEmptyNonVariadicType()) { + $parameter->setUnionType( + $argument_type->withFlattenedArrayShapeOrLiteralTypeInstances()->asListTypes()->withRealTypeSet($parameter->getNonVariadicUnionType()->getRealTypeSet()) + ); + } else { + $parameter->addUnionType( + $argument_type->withFlattenedArrayShapeOrLiteralTypeInstances()->asListTypes()->withRealTypeSet($parameter->getNonVariadicUnionType()->getRealTypeSet()) + ); + } + } else { + $parameter->addUnionType( + ($method instanceof Func && $method->isClosure() ? $argument_type : $argument_type->withFlattenedArrayShapeOrLiteralTypeInstances())->withRealTypeSet($parameter->getNonVariadicUnionType()->getRealTypeSet()) + ); + } + if ($method instanceof Method && ($parameter->getFlags() & Parameter::PARAM_MODIFIER_VISIBILITY_FLAGS)) { + $this->analyzeArgumentWithConstructorPropertyPromotion($method, $parameter); + } + } + + // If we're passing by reference, get the variable + // we're dealing with wrapped up and shoved into + // the scope of the method + if (!$parameter->isPassByReference()) { + // Overwrite the method's variable representation + // of the parameter with the parameter with the + // new type + $method->getInternalScope()->addVariable( + $parameter + ); + + return; + } + + // At this point we're dealing with a pass-by-reference + // parameter. + + // For now, give up and work on it later. + // + // TODO (Issue #376): It's possible to have a + // parameter `&...$args`. Analyzing that is going to + // be a problem. Is it possible to create + // `PassByReferenceVariableCollection extends Variable` + // or something similar? + if ($parameter->isVariadic()) { + return; + } + + if (!$argument instanceof Node) { + return; + } + + $variable = null; + if ($argument->kind === ast\AST_VAR) { + try { + $variable = (new ContextNode( + $this->code_base, + $this->context, + $argument + ))->getOrCreateVariableForReferenceParameter($parameter, $method->getRealParameterForCaller($parameter_offset)); + } catch (NodeException $_) { + // Could not figure out the node name + return; + } + } elseif (\in_array($argument->kind, [ast\AST_STATIC_PROP, ast\AST_PROP], true)) { + try { + $variable = (new ContextNode( + $this->code_base, + $this->context, + $argument + ))->getProperty($argument->kind === ast\AST_STATIC_PROP); + } catch (IssueException | NodeException $_) { + // Hopefully caught elsewhere + } + } + + // If we couldn't find a variable, give up + if (!$variable) { + return; + } + // For @phan-ignore-reference, don't bother modifying the type + if ($parameter->getReferenceType() === Parameter::REFERENCE_IGNORED) { + return; + } + + $pass_by_reference_variable = + new PassByReferenceVariable( + $parameter, + $variable, + $this->code_base, + $this->context + ); + // Add it to the (cloned) scope of the function wrapped + // in a way that makes it addressable as the + // parameter its mimicking + $method->getInternalScope()->addVariable( + $pass_by_reference_variable + ); + $parameter_list[$parameter_offset] = $pass_by_reference_variable; + } + + private function analyzeArgumentWithConstructorPropertyPromotion(Method $method, Parameter $parameter): void + { + if (!$method->isNewConstructor()) { + return; + } + $code_base = $this->code_base; + $class_fqsen = $method->getClassFQSEN(); + $class = $code_base->getClassByFQSEN($class_fqsen); + $property = $class->getPropertyByName($code_base, $parameter->getName()); + AssignmentVisitor::addTypesToPropertyStandalone($code_base, $this->context, $property, $parameter->getUnionType()); + } + + + /** + * Emit warnings if the pass-by-reference call would set the property to an invalid type + * @param Node $argument a node of kind ast\AST_PROP or ast\AST_STATIC_PROP + */ + private static function checkPassingPropertyByReference(CodeBase $code_base, Context $context, FunctionInterface $method, Parameter $parameter, Node $argument, Property $property, int $parameter_offset): void + { + $parameter_type = $parameter->getNonVariadicUnionType(); + $expr_node = $argument->children['expr'] ?? null; + if ($expr_node instanceof Node && + $expr_node->kind === ast\AST_VAR && + $expr_node->children['name'] === 'this') { + // If the property is of the form $this->prop, check for local assignments and conditions on $this->prop + $property_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $argument); + } else { + $property_type = $property->getUnionType(); + } + if ($property_type->hasRealTypeSet()) { + // Barely any reference parameters will have real union types (and phan would already warn about passing them in if they did), + // so warn if the phpdoc type doesn't match the property's real type. + if (!$parameter_type->canCastToDeclaredType($code_base, $context, $property_type)) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeMismatchArgumentPropertyReferenceReal, + $argument->lineno, + $parameter_offset, + $property->getRepresentationForIssue(), + $property_type, + self::toDetailsForRealTypeMismatch($property_type), + $method->getRepresentationForIssue(), + $parameter_type, + self::toDetailsForRealTypeMismatch($parameter_type) + ); + return; + } + } + if ($parameter_type->canCastToDeclaredType($code_base, $context, $property_type)) { + return; + } + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeMismatchArgumentPropertyReference, + $argument->lineno, + $parameter_offset, + $property->getRepresentationForIssue(), + $property_type, + $method->getRepresentationForIssue(), + $parameter_type + ); + } + + private function isInNoOpPosition(Node $node): bool + { + $parent_node = \end($this->parent_node_list); + if (!($parent_node instanceof Node)) { + return false; + } + switch ($parent_node->kind) { + case ast\AST_STMT_LIST: + return true; + case ast\AST_EXPR_LIST: + $parent_parent_node = \prev($this->parent_node_list); + // @phan-suppress-next-line PhanPossiblyUndeclaredProperty + if ($parent_parent_node->kind === ast\AST_MATCH_ARM) { + return false; + } + if ($node !== \end($parent_node->children)) { + return true; + } + // This is an expression list, but it's in the condition + return $parent_node !== ($parent_parent_node->children['cond'] ?? null); + } + return false; + } + + /** + * @param Node $node + * A node to check to see if it's a no-op + * + * @param string $issue_type + * A message to emit if it's a no-op + */ + private function analyzeNoOp(Node $node, string $issue_type): void + { + if ($this->isInNoOpPosition($node)) { + $this->emitIssue( + $issue_type, + $node->lineno + ); + } + } + + private static function hasEmptyImplementation(Method $method): bool + { + if ($method->isAbstract() || $method->isPHPInternal()) { + return false; + } + $stmts = $method->getNode()->children['stmts'] ?? null; + if (!$stmts instanceof Node) { + // This is abstract or a stub or a magic method + return false; + } + return empty($stmts->children); + } + + /** + * @param list $class_list + */ + private function warnNoopNew( + Node $node, + array $class_list + ): void { + $has_constructor_or_destructor = \count($class_list) === 0; + foreach ($class_list as $class) { + if ($class->getPhanFlagsHasState(\Phan\Language\Element\Flags::IS_CONSTRUCTOR_USED_FOR_SIDE_EFFECTS)) { + return; + } + } + foreach ($class_list as $class) { + if ($class->hasMethodWithName($this->code_base, '__construct', true)) { + $constructor = $class->getMethodByName($this->code_base, '__construct'); + if (!$constructor->getPhanFlagsHasState(\Phan\Language\Element\Flags::IS_FAKE_CONSTRUCTOR)) { + if (!self::hasEmptyImplementation($constructor)) { + $has_constructor_or_destructor = true; + break; + } + } + } + if ($class->hasMethodWithName($this->code_base, '__destruct', true)) { + $destructor = $class->getMethodByName($this->code_base, '__destruct'); + if (!self::hasEmptyImplementation($destructor)) { + $has_constructor_or_destructor = true; + break; + } + } + if (!$class->isClass() || $class->isAbstract()) { + $has_constructor_or_destructor = true; + break; + } + } + $this->emitIssue( + $has_constructor_or_destructor ? Issue::NoopNew : Issue::NoopNewNoSideEffects, + $node->lineno, + ASTReverter::toShortString($node) + ); + } + + public const LOOP_SCOPE_KINDS = [ + ast\AST_FOR => true, + ast\AST_FOREACH => true, + ast\AST_WHILE => true, + ast\AST_DO_WHILE => true, + ast\AST_SWITCH => true, + ]; + + /** + * Analyzes a `break;` or `break N;` statement. + * Checks if there are enough loops to break out of. + */ + public function visitBreak(Node $node): Context + { + $depth = $node->children['depth'] ?? 1; + if (!\is_int($depth)) { + return $this->context; + } + foreach ($this->parent_node_list as $iter_node) { + if (\array_key_exists($iter_node->kind, self::LOOP_SCOPE_KINDS)) { + $depth--; + if ($depth <= 0) { + return $this->context; + } + } + } + $this->warnBreakOrContinueWithoutLoop($node); + return $this->context; + } + + /** + * Analyzes a `continue;` or `continue N;` statement. + * Checks for http://php.net/manual/en/migration73.incompatible.php#migration73.incompatible.core.continue-targeting-switch + * and similar issues. + */ + public function visitContinue(Node $node): Context + { + $nodes = $this->parent_node_list; + $depth = $node->children['depth'] ?? 1; + if (!\is_int($depth)) { + return $this->context; + } + for ($iter_node = \end($nodes); $iter_node instanceof Node; $iter_node = \prev($nodes)) { + switch ($iter_node->kind) { + case ast\AST_FOR: + case ast\AST_FOREACH: + case ast\AST_WHILE: + case ast\AST_DO_WHILE: + $depth--; + if ($depth <= 0) { + return $this->context; + } + break; + case ast\AST_SWITCH: + $depth--; + if ($depth <= 0) { + $this->emitIssue( + Issue::ContinueTargetingSwitch, + $node->lineno + ); + return $this->context; + } + break; + } + } + $this->warnBreakOrContinueWithoutLoop($node); + return $this->context; + } + + /** + * Visit a node of kind AST_LABEL to check for unused labels. + * @override + */ + public function visitLabel(Node $node): Context + { + $label = $node->children['name']; + $used_labels = GotoAnalyzer::getLabelSet($this->parent_node_list); + if (!isset($used_labels[$label])) { + $this->emitIssue( + Issue::UnusedGotoLabel, + $node->lineno, + $label + ); + } + return $this->context; + } + + private function warnBreakOrContinueWithoutLoop(Node $node): void + { + $depth = $node->children['depth'] ?? 1; + $name = $node->kind === ast\AST_BREAK ? 'break' : 'continue'; + if ($depth !== 1) { + $this->emitIssue( + Issue::ContinueOrBreakTooManyLevels, + $node->lineno, + $name, + $depth + ); + return; + } + $this->emitIssue( + Issue::ContinueOrBreakNotInLoop, + $node->lineno, + $name + ); + } + + /** + * @param Node $node + * A decl to check to see if its only effect + * is the throw an exception + * + * @return bool + * True when the decl can only throw an exception or return or exit() + */ + private static function declOnlyThrows(Node $node): bool + { + // Work around fallback parser generating methods without statements list. + // Otherwise, 'stmts' would always be a Node due to preconditions. + $stmts_node = $node->children['stmts']; + return $stmts_node instanceof Node && BlockExitStatusChecker::willUnconditionallyThrowOrReturn($stmts_node); + } + + /** + * Check if the class is using PHP4-style constructor (without having its own __construct method) + * + * @param Clazz $class + * @param Method $method + */ + private function checkForPHP4StyleConstructor(Clazz $class, Method $method): void + { + if ($class->isClass() + && ($class->getElementNamespace() ?: "\\") === "\\" + && \strcasecmp($class->getName(), $method->getName()) === 0 + && $class->hasMethodWithName($this->code_base, "__construct", false) // return true for the fake constructor + ) { + try { + $constructor = $class->getMethodByName($this->code_base, "__construct"); + + // Phan always makes up the __construct if it's not explicitly defined, so we need to check + // if there is no __construct method *actually* defined before we emit the issue + if ($constructor->getPhanFlagsHasState(\Phan\Language\Element\Flags::IS_FAKE_CONSTRUCTOR)) { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::CompatiblePHP8PHP4Constructor, + $this->context->getLineNumberStart(), + $method->getRepresentationForIssue() + ); + } + } catch (CodeBaseException $_) { + // actually __construct always exists as per Phan's current logic, so this exception won't be thrown. + // but just in case let's leave this here + } + } + } + + /** + * @param Node $node @unused-param + * A node to analyze + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitThrow(Node $node): Context + { + $parent_node = \end($this->parent_node_list); + if (!($parent_node instanceof Node)) { + return $this->context; + } + if ($parent_node->kind !== ast\AST_STMT_LIST) { + if (Config::get_closest_minimum_target_php_version_id() < 80000) { + $this->emitIssue( + Issue::CompatibleThrowExpression, + $parent_node->lineno, + ASTReverter::toShortString($parent_node) + ); + } + } + + return $this->context; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/PreOrderAnalysisVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/PreOrderAnalysisVisitor.php new file mode 100644 index 000000000..e6d44a706 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/PreOrderAnalysisVisitor.php @@ -0,0 +1,889 @@ +context; + } + + /** + * Visit a node with kind `ast\AST_CLASS` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + * + * @throws UnanalyzableException + * if the class name is unexpectedly empty + * + * @throws CodeBaseException + * if the class could not be located + */ + public function visitClass(Node $node): Context + { + if ($node->flags & ast\flags\CLASS_ANONYMOUS) { + $class_name = + (new ContextNode( + $this->code_base, + $this->context, + $node + ))->getUnqualifiedNameForAnonymousClass(); + } else { + $class_name = (string)$node->children['name']; + } + + if (!StringUtil::isNonZeroLengthString($class_name)) { + // Should only occur with --use-fallback-parser + throw new UnanalyzableException($node, "Class name cannot be empty"); + } + + $alternate_id = 0; + + // Hunt for the alternate of this class defined + // in this file + do { + // @phan-suppress-next-line PhanThrowTypeMismatchForCall + $class_fqsen = FullyQualifiedClassName::fromStringInContext( + $class_name, + $this->context + )->withAlternateId($alternate_id++); + + if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) { + throw new CodeBaseException( + $class_fqsen, + "Can't find class {$class_fqsen} - aborting" + ); + } + + $clazz = $this->code_base->getClassByFQSEN( + $class_fqsen + ); + } while ($this->context->getProjectRelativePath() + != $clazz->getFileRef()->getProjectRelativePath() + || $node->children['__declId'] != $clazz->getDeclId() + || $this->context->getLineNumberStart() != $clazz->getFileRef()->getLineNumberStart() + ); + + return $clazz->getContext()->withScope( + $clazz->getInternalScope() + )->withoutLoops(); + } + + /** + * Visit a node with kind `ast\AST_METHOD` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + * + * @throws CodeBaseException if the method could not be found + */ + public function visitMethod(Node $node): Context + { + $method_name = (string)$node->children['name']; + $code_base = $this->code_base; + $context = $this->context; + + if (!$context->isInClassScope()) { + throw new AssertionError("Must be in class context to see a method"); + } + + $clazz = $this->getContextClass(); + + if (!$clazz->hasMethodWithName( + $code_base, + $method_name, + true + )) { + throw new CodeBaseException( + null, + "Can't find method {$clazz->getFQSEN()}::$method_name() - aborting" + ); + } + + $method = $clazz->getMethodByName( + $code_base, + $method_name + ); + $method->ensureScopeInitialized($code_base); + // Fix #2504 - add flags to ensure that DimOffset warnings aren't emitted inside closures + Analyzable::ensureDidAnnotate($node); + + // Parse the comment above the method to get + // extra meta information about the method. + $comment = $method->getComment(); + + $context = $this->context->withScope( + clone($method->getInternalScope()) + ); + + // For any @var references in the method declaration, + // add them as variables to the method's scope + if ($comment !== null) { + foreach ($comment->getVariableList() as $parameter) { + $context->addScopeVariable( + $parameter->asVariable($this->context) + ); + } + } + + // Add $this to the scope of non-static methods + if (!($node->flags & ast\flags\MODIFIER_STATIC)) { + if (!$clazz->getInternalScope()->hasVariableWithName('this')) { + throw new AssertionError("Classes must have a \$this variable."); + } + + $context->addScopeVariable( + $clazz->getInternalScope()->getVariableByName('this') + ); + } + + // Add each method parameter to the scope. We clone it + // so that changes to the variable don't alter the + // parameter definition + if ($method->getRecursionDepth() === 0) { + // Add each method parameter to the scope. We clone it + // so that changes to the variable don't alter the + // parameter definition + foreach ($method->getParameterList() as $parameter) { + $context->addScopeVariable( + $parameter->cloneAsNonVariadic() + ); + } + } + if ($method->getName() === '__construct' && Config::getValue('infer_default_properties_in_construct') && $clazz->isClass() && !$method->isAbstract()) { + $this->addDefaultPropertiesOfThisToContext($clazz, $context); + } + + // TODO: Why is the check for yield in PreOrderAnalysisVisitor? + if ($method->hasYield()) { + $this->setReturnTypeOfGenerator($method, $node); + } + + return $context; + } + + /** + * Modifies the context of $class in place, adding types of default values for all declared properties + */ + private function addDefaultPropertiesOfThisToContext(Clazz $class, Context $context): void + { + $property_types = []; + foreach ($class->getPropertyMap($this->code_base) as $property) { + if ($property->isDynamicOrFromPHPDoc()) { + continue; + } + if ($property->isStatic()) { + continue; + } + $default_type = $property->getDefaultType(); + if (!$default_type) { + continue; + } + if ($property->getFQSEN() !== $property->getRealDefiningFQSEN()) { + // Here, we don't analyze the properties of parent classes to avoid false positives. + // Phan doesn't infer that the scope is cleared by parent::__construct(). + // + // TODO: It should be possible to inherit property types from parent::__construct() for simple constructors? + // TODO: Check if there's actually any calls to parent::__construct, infer types aggressively if there are no calls. + // TODO: Phan does not yet infer or apply implications of setPropName(), etc. + continue; + } + $property_types[$property->getName()] = $default_type; + } + if (!$property_types) { + return; + } + $override_type = ArrayShapeType::fromFieldTypes($property_types, false); + $variable = new Variable( + $context, + Context::VAR_NAME_THIS_PROPERTIES, + $override_type->asPHPDocUnionType(), + 0 + ); + $context->addScopeVariable($variable); + } + + + /** + * Visit a node with kind `ast\AST_FUNC_DECL` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + * @throws CodeBaseException + * if this function declaration could not be found + */ + public function visitFuncDecl(Node $node): Context + { + $function_name = (string)$node->children['name']; + $code_base = $this->code_base; + $original_context = $this->context; + + // This really ought not to throw given that + // we already successfully parsed the code + // base (the AST names should be valid) + // @phan-suppress-next-line PhanThrowTypeMismatchForCall + $canonical_function = (new ContextNode( + $code_base, + $original_context, + $node + ))->getFunction($function_name, true); + + // Hunt for the alternate associated with the file we're + // looking at currently in this context. + $function = null; + foreach ($canonical_function->alternateGenerator($code_base) as $alternate_function) { + if ($alternate_function->getFileRef()->getProjectRelativePath() + === $original_context->getProjectRelativePath() + ) { + $function = $alternate_function; + break; + } + } + + if (!($function instanceof Func)) { + // No alternate was found + throw new CodeBaseException( + null, + "Can't find function {$function_name} in context {$this->context} - aborting" + ); + } + + $function->ensureScopeInitialized($code_base); + // Fix #2504 - add flags to ensure that DimOffset warnings aren't emitted inside closures + Analyzable::ensureDidAnnotate($node); + + $context = $original_context->withScope( + clone($function->getInternalScope()) + )->withoutLoops(); + + // Parse the comment above the function to get + // extra meta information about the function. + // TODO: Investigate caching information from Comment::fromStringInContext? + $comment = $function->getComment(); + + // For any @var references in the function declaration, + // add them as variables to the function's scope + if ($comment !== null) { + foreach ($comment->getVariableList() as $parameter) { + $context->addScopeVariable( + $parameter->asVariable($this->context) + ); + } + } + + if ($function->getRecursionDepth() === 0) { + // Add each method parameter to the scope. We clone it + // so that changes to the variable don't alter the + // parameter definition + foreach ($function->getParameterList() as $parameter) { + $context->addScopeVariable( + $parameter->cloneAsNonVariadic() + ); + } + } + + if ($function->hasYield()) { + $this->setReturnTypeOfGenerator($function, $node); + } + if (!$function->hasReturn() && $function->getUnionType()->isEmpty()) { + // TODO: This is a global function - also guarantee that it's a real type elsewhere if phpdoc matches the implementation. + $function->setUnionType(VoidType::instance(false)->asRealUnionType()); + } + + return $context; + } + + private static function getOverrideClassFQSEN(CodeBase $code_base, Func $func): ?FullyQualifiedClassName + { + $closure_scope = $func->getInternalScope(); + if ($closure_scope instanceof ClosureScope) { + $class_fqsen = $closure_scope->getOverrideClassFQSEN(); + if (!$class_fqsen) { + return null; + } + + // Postponed the check for undeclared closure scopes to the analysis phase, + // because classes are still being parsed in the parse phase. + if (!$code_base->hasClassWithFQSEN($class_fqsen)) { + $func_context = $func->getContext(); + Issue::maybeEmit( + $code_base, + $func_context, + Issue::UndeclaredClosureScope, + $func_context->getLineNumberStart(), + (string)$class_fqsen + ); + // Avoid an uncaught CodeBaseException due to missing class for @phan-closure-scope + // Just pretend it's the containing class instead of the missing class. + $closure_scope->overrideClassFQSEN($func_context->getScope()->getParentScope()->getClassFQSENOrNull()); + return null; + } + return $class_fqsen; + } + return null; + } + + /** + * If a Closure overrides the scope(class) it will be executed in (via doc comment) + * then return a context with the new scope instead. + */ + private static function addThisVariableToInternalScope( + CodeBase $code_base, + Context $context, + Func $func + ): void { + // skip adding $this to internal scope if the closure is a static one + if ($func->getFlags() === ast\flags\MODIFIER_STATIC) { + return; + } + + $override_this_fqsen = self::getOverrideClassFQSEN($code_base, $func); + if ($override_this_fqsen !== null) { + if ($context->getScope()->hasVariableWithName('this') || !$context->isInClassScope()) { + // Handle @phan-closure-scope - Should set $this to the overridden class, as well as handling self:: and parent:: + $func->getInternalScope()->addVariable( + new Variable( + $context, + 'this', + $override_this_fqsen->asRealUnionType(), + 0 + ) + ); + } + return; + } + // If we have a 'this' variable in our current scope, + // pass it down into the closure + if ($context->getScope()->hasVariableWithName('this')) { + // Normal case: Closures inherit $this from parent scope. + $this_var_from_scope = $context->getScope()->getVariableByName('this'); + $func->getInternalScope()->addVariable($this_var_from_scope); + } + } + + /** + * Visit a node with kind `ast\AST_CLOSURE` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitClosure(Node $node): Context + { + $code_base = $this->code_base; + $context = $this->context->withoutLoops(); + $closure_fqsen = FullyQualifiedFunctionName::fromClosureInContext( + $context->withLineNumberStart($node->lineno), + $node + ); + $func = $code_base->getFunctionByFQSEN($closure_fqsen); + $func->ensureScopeInitialized($code_base); + // Fix #2504 - add flags to ensure that DimOffset warnings aren't emitted inside closures + Analyzable::ensureDidAnnotate($node); + + // If we have a 'this' variable in our current scope, + // pass it down into the closure + self::addThisVariableToInternalScope($code_base, $context, $func); + + // Make the closure reachable by FQSEN from anywhere + $code_base->addFunction($func); + + if (($node->children['uses']->kind ?? null) === ast\AST_CLOSURE_USES) { + foreach ($node->children['uses']->children ?? [] as $use) { + if (!($use instanceof Node) || $use->kind !== ast\AST_CLOSURE_VAR) { + $this->emitIssue( + Issue::VariableUseClause, + $node->lineno, + ASTReverter::toShortString($use) + ); + continue; + } + + $variable_name = (new ContextNode( + $code_base, + $context, + $use->children['name'] + ))->getVariableName(); + + // TODO: Distinguish between the empty string and the lack of a name + if ($variable_name === '') { + continue; + } + + // Check to see if the variable exists in this scope + if (!$context->getScope()->hasVariableWithName( + $variable_name + )) { + // If this is not pass-by-reference variable we + // have a problem + if (!($use->flags & ast\flags\CLOSURE_USE_REF)) { + Issue::maybeEmitWithParameters( + $this->code_base, + $context, + Issue::UndeclaredVariable, + $use->lineno, + [$variable_name], + IssueFixSuggester::suggestVariableTypoFix($this->code_base, $context, $variable_name) + ); + $variable = new Variable($context, $variable_name, NullType::instance(false)->asPHPDocUnionType(), 0); + } else { + // If the variable doesn't exist, but it's + // a pass-by-reference variable, we can + // just create it + $variable = Variable::fromNodeInContext( + $use, + $context, + $this->code_base, + false + ); + } + // And add it to the scope of the parent (For https://github.com/phan/phan/issues/367) + $context->addScopeVariable($variable); + } else { + $variable = $context->getScope()->getVariableByName( + $variable_name + ); + + // If this isn't a pass-by-reference variable, we + // clone the variable so state within this scope + // doesn't update the outer scope + if (!($use->flags & ast\flags\CLOSURE_USE_REF)) { + $variable = clone($variable); + } else { + $union_type = $variable->getUnionType(); + if ($union_type->hasRealTypeSet()) { + $variable->setUnionType($union_type->eraseRealTypeSetRecursively()); + } + } + } + + // Pass the variable into a new scope + $func->getInternalScope()->addVariable($variable); + } + } + if (!$func->hasReturn() && $func->getUnionType()->isEmpty()) { + $func->setUnionType(VoidType::instance(false)->asRealUnionType()); + } + + // Add parameters to the context. + $context = $context->withScope(clone($func->getInternalScope())); + + $comment = $func->getComment(); + + // For any @var references in the method declaration, + // add them as variables to the method's scope + if ($comment !== null) { + foreach ($comment->getVariableList() as $parameter) { + $context->addScopeVariable( + $parameter->asVariable($this->context) + ); + } + } + if ($func->getRecursionDepth() === 0) { + // Add each closure parameter to the scope. We clone it + // so that changes to the variable don't alter the + // parameter definition + foreach ($func->getParameterList() as $parameter) { + $context->addScopeVariable( + $parameter->cloneAsNonVariadic() + ); + } + } + + if ($func->hasYield()) { + $this->setReturnTypeOfGenerator($func, $node); + } + + return $context; + } + + /** + * Visit a node with kind `ast\AST_ARROW_FUNC` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + * @override + */ + public function visitArrowFunc(Node $node): Context + { + $code_base = $this->code_base; + $context = $this->context->withoutLoops(); + $closure_fqsen = FullyQualifiedFunctionName::fromClosureInContext( + $context->withLineNumberStart($node->lineno), + $node + ); + $func = $code_base->getFunctionByFQSEN($closure_fqsen); + $func->ensureScopeInitialized($code_base); + // Fix #2504 - add flags to ensure that DimOffset warnings aren't emitted inside closures + Analyzable::ensureDidAnnotate($node); + + // If we have a 'this' variable in our current scope, + // pass it down into the closure + self::addThisVariableToInternalScope($code_base, $context, $func); + + // Make the closure reachable by FQSEN from anywhere + $code_base->addFunction($func); + + foreach (ArrowFunc::getUses($node) as $variable_name => $use) { + $variable_name = (string)$variable_name; + // Check to see if the variable exists in this scope + // (If it doesn't, then don't add it - Phan will check later if it properly declares the variable in the scope.) + if ($context->getScope()->hasVariableWithName($variable_name)) { + ArrowFunc::recordVariableExistsInOuterScope($node, $variable_name); + $variable = $context->getScope()->getVariableByName( + $variable_name + ); + + // If this isn't a pass-by-reference variable, we + // clone the variable so state within this scope + // doesn't update the outer scope + if (!($use->flags & ast\flags\CLOSURE_USE_REF)) { + $variable = clone($variable); + } + // Pass the variable into a new scope + $func->getInternalScope()->addVariable($variable); + } + } + if (!$func->hasReturn() && $func->getUnionType()->isEmpty()) { + $func->setUnionType(VoidType::instance(false)->asRealUnionType()); + } + + // Add parameters to the context. + $context = $context->withScope(clone($func->getInternalScope())); + + $comment = $func->getComment(); + + // For any @var references in the method declaration, + // add them as variables to the method's scope + if ($comment !== null) { + foreach ($comment->getVariableList() as $parameter) { + $context->addScopeVariable( + $parameter->asVariable($this->context) + ); + } + } + if ($func->getRecursionDepth() === 0) { + // Add each closure parameter to the scope. We clone it + // so that changes to the variable don't alter the + // parameter definition + foreach ($func->getParameterList() as $parameter) { + $context->addScopeVariable( + $parameter->cloneAsNonVariadic() + ); + } + } + + if ($func->hasYield()) { + $this->setReturnTypeOfGenerator($func, $node); + } + + return $context; + } + + + /** + * The return type of the given FunctionInterface to a Generator. + * Emit an Issue if the documented return type is incompatible with that. + */ + private function setReturnTypeOfGenerator(FunctionInterface $func, Node $node): void + { + // Currently, there is no way to describe the types passed to + // a Generator in phpdoc. + // So, nothing bothers recording the types beyond \Generator. + $func->setHasReturn(true); // Returns \Generator, technically + $func->setHasYield(true); + if ($func->getUnionType()->isEmpty()) { + $func->setIsReturnTypeUndefined(true); + $func->setUnionType($func->getUnionType()->withType(Type::fromNamespaceAndName('\\', 'Generator', false))); + } + if (!$func->isReturnTypeUndefined()) { + $func_return_type = $func->getUnionType(); + try { + $func_return_type_can_cast = $func_return_type->canCastToExpandedUnionType( + Type::fromNamespaceAndName('\\', 'Generator', false)->asPHPDocUnionType(), + $this->code_base + ); + } catch (RecursionDepthException $_) { + return; + } + if (!$func_return_type_can_cast) { + // At least one of the documented return types must + // be Generator, Iterable, or Traversable. + // Check for the issue here instead of in visitReturn/visitYield so that + // the check is done exactly once. + $this->emitIssue( + Issue::TypeMismatchReturn, + $node->lineno, + '(a Generator due to existence of yield statements)', + '\\Generator', + $func->getNameForIssue(), + (string)$func_return_type + ); + } + } + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * An unchanged context resulting from parsing the node + */ + public function visitAssign(Node $node): Context + { + if (Config::get_closest_minimum_target_php_version_id() < 70100) { + $var_node = $node->children['var']; + if ($var_node instanceof Node && $var_node->kind === ast\AST_ARRAY) { + BlockAnalysisVisitor::analyzeArrayAssignBackwardsCompatibility($this->code_base, $this->context, $var_node); + } + } + return $this->context; + } + + /** + * No-op - all work is done in BlockAnalysisVisitor + * + * @param Node $node @unused-param + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitForeach(Node $node): Context + { + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitCatch(Node $node): Context + { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + $union_type = UnionTypeVisitor::unionTypeFromClassNode( + $this->code_base, + $this->context, + $node->children['class'] + ); + if (!isset($node->children['var'])) { + $this->emitIssue( + Issue::CompatibleNonCapturingCatch, + $node->lineno, + ASTReverter::toShortString($node->children['class']) + ); + } + + try { + $class_list = \iterator_to_array($union_type->asClassList($this->code_base, $this->context)); + + if (Config::get_closest_minimum_target_php_version_id() < 70100 && \count($class_list) > 1) { + $this->emitIssue( + Issue::CompatibleMultiExceptionCatchPHP70, + $node->lineno + ); + } + + foreach ($class_list as $class) { + $class->addReference($this->context); + } + } catch (CodeBaseException $exception) { + Issue::maybeEmitWithParameters( + $this->code_base, + $this->context, + Issue::UndeclaredClassCatch, + $node->lineno, + [(string)$exception->getFQSEN()], + IssueFixSuggester::suggestSimilarClassForGenericFQSEN($this->code_base, $this->context, $exception->getFQSEN()) + ); + } + + $throwable_type = Type::throwableInstance(); + if ($union_type->isEmpty() || !$union_type->asExpandedTypes($this->code_base)->hasType($throwable_type)) { + $union_type = $union_type->withType($throwable_type); + } + $var_node = $node->children['var']; + if (!$var_node instanceof Node) { + // Impossible + return $this->context; + } + + $variable_name = (new ContextNode( + $this->code_base, + $this->context, + $var_node + ))->getVariableName(); + + if ($variable_name !== '') { + $variable = Variable::fromNodeInContext( + $var_node, + $this->context, + $this->code_base, + false + ); + + if (!$union_type->isEmpty()) { + $variable->setUnionType($union_type); + } + + $this->context->addScopeVariable($variable); + } + + return $this->context; + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitIfElem(Node $node): Context + { + $cond = $node->children['cond'] ?? null; + if (!($cond instanceof Node)) { + return $this->context; + } + + // Look to see if any proofs we do within the condition + // can say anything about types within the statement + // list. + return (new ConditionVisitor( + $this->code_base, + $this->context + ))->__invoke($cond); + } + + // visitWhile is unnecessary, this has special logic in BlockAnalysisVisitor to handle conditions assigning variables to the loop + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitFor(Node $node): Context + { + $cond = $node->children['cond']; + if (!($cond instanceof Node)) { + return $this->context; + } + + // Look to see if any proofs we do within the condition of the while + // can say anything about types within the statement + // list. + return (new ConditionVisitor( + $this->code_base, + $this->context + ))->__invoke($cond); + } + + /** + * @param Node $node @unused-param + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitCall(Node $node): Context + { + return $this->context; + } + + /** + * @return Clazz + * Get the class on this scope or fail real hard + */ + private function getContextClass(): Clazz + { + return $this->context->getClassInScope($this->code_base); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/PropertyTypesAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/PropertyTypesAnalyzer.php new file mode 100644 index 000000000..d2c2e1f5c --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/PropertyTypesAnalyzer.php @@ -0,0 +1,94 @@ +getPropertyMap($code_base) as $property) { + // This phase is done before the analysis phase, so there aren't any dynamic properties to filter out. + + // Get the union type of this property. This may throw (e.g. it can refers to missing elements). + try { + $union_type = $property->getUnionType(); + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $code_base, + $property->getContext(), + $exception->getIssueInstance() + ); + continue; + } + + // Look at each type in the parameter's Union Type + foreach ($union_type->withFlattenedArrayShapeOrLiteralTypeInstances()->getTypeSet() as $outer_type) { + foreach ($outer_type->getReferencedClasses() as $type) { + // If it's a reference to self, its OK + if ($type->isSelfType()) { + continue; + } + + if ($type instanceof TemplateType) { + if ($property->isStatic()) { + Issue::maybeEmit( + $code_base, + $property->getContext(), + Issue::TemplateTypeStaticProperty, + $property->getFileRef()->getLineNumberStart(), + $property->asPropertyFQSENString() + ); + } + continue; + } + if (!($property->hasDefiningFQSEN() && $property->getDefiningFQSEN() === $property->getFQSEN())) { + continue; + } + if ($type instanceof TemplateType) { + continue; + } + + // Make sure the class exists + $type_fqsen = FullyQualifiedClassName::fromType($type); + + if ($code_base->hasClassWithFQSEN($type_fqsen)) { + if ($code_base->hasClassWithFQSEN($type_fqsen->withAlternateId(1))) { + UnionType::emitRedefinedClassReferenceWarning( + $code_base, + $property->getContext(), + $type_fqsen + ); + } + } else { + Issue::maybeEmitWithParameters( + $code_base, + $property->getContext(), + Issue::UndeclaredTypeProperty, + $property->getFileRef()->getLineNumberStart(), + [$property->asPropertyFQSENString(), (string)$outer_type], + IssueFixSuggester::suggestSimilarClass($code_base, $property->getContext(), $type_fqsen, null, 'Did you mean', IssueFixSuggester::CLASS_SUGGEST_CLASSES_AND_TYPES) + ); + } + } + } + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ReachabilityChecker.php b/bundled-libs/phan/phan/src/Phan/Analysis/ReachabilityChecker.php new file mode 100644 index 000000000..cafdc6288 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ReachabilityChecker.php @@ -0,0 +1,316 @@ +inner = $inner; + } + + public function visitArgList(Node $node): ?bool + { + if ($node === $this->inner) { + return true; + } + return $this->visit($node); + } + + /** + * If we don't know how to analyze a node type (or left it out), assume it always proceeds + * @return ?bool - The status bitmask corresponding to always proceeding + */ + public function visit(Node $node): ?bool + { + foreach ($node->children as $child) { + if (!($child instanceof Node)) { + continue; + } + $result = $this->__invoke($child); + if ($result !== null) { + return $result; + } + } + return null; + } + + /** + * @unused-param $node + * @return ?bool this gives up on analyzing catch lists + */ + public function visitCatchList(Node $node): ?bool + { + return null; + } + + /** + * @return ?bool this gives up on analyzing switches, except for the condition + */ + public function visitSwitch(Node $node): ?bool + { + $cond = $node->children['cond']; + if ($cond instanceof Node) { + return $this->__invoke($cond); + } + return null; + } + + /** + * @return ?bool this gives up on analyzing matches, except for the condition + */ + public function visitMatch(Node $node): ?bool + { + $cond = $node->children['cond']; + if ($cond instanceof Node) { + return $this->__invoke($cond); + } + return null; + } + + /** + * @return ?bool this gives up on analyzing for loops, except for the initializer and condition + */ + public function visitFor(Node $node): ?bool + { + $init = $node->children['init']; + if ($init instanceof Node) { + $result = $this->__invoke($init); + if ($result !== null) { + return $result; + } + } + $cond = $node->children['cond']; + if ($cond instanceof Node) { + return $this->__invoke($cond); + } + return null; + } + + /** + * @return ?bool this gives up on analyzing loops, except for the condition + */ + public function visitWhile(Node $node): ?bool + { + $cond = $node->children['cond']; + if ($cond instanceof Node) { + return $this->__invoke($cond); + } + return null; + } + + /** + * @return ?bool this gives up on analyzing loops, except for the condition + */ + public function visitForeach(Node $node): ?bool + { + $expr = $node->children['expr']; + if ($expr instanceof Node) { + return $this->__invoke($expr); + } + return null; + } + + /** + * @unused-param $node + * @override + */ + public function visitBreak(Node $node): ?bool + { + return false; + } + + /** + * @unused-param $node + * @override + */ + public function visitContinue(Node $node): ?bool + { + return false; + } + + public function visitReturn(Node $node): bool + { + $expr = $node->children['expr']; + if (!($expr instanceof Node)) { + return false; + } + return $this->__invoke($expr) ?? false; + } + + /** + * @unused-param $node + * @override + */ + public function visitClosure(Node $node): ?bool + { + return null; + } + + /** + * @unused-param $node + * @override + */ + public function visitArrowFunc(Node $node): ?bool + { + return null; + } + + /** + * @unused-param $node + * @override + */ + public function visitFuncDecl(Node $node): ?bool + { + return null; + } + + /** + * @override + */ + public function visitClass(Node $node): ?bool + { + $args = $node->children['args'] ?? null; + if (!$args instanceof Node) { + return null; + } + return $this->__invoke($args); + } + + public function visitThrow(Node $node): bool + { + $expr = $node->children['expr']; + if (!($expr instanceof Node)) { + return false; + } + return $this->__invoke($expr) ?? false; + } + + public function visitExit(Node $node): bool + { + $expr = $node->children['expr']; + if (!($expr instanceof Node)) { + return false; + } + return $this->__invoke($expr) ?? false; + } + + /** + * @return ?bool the first result seen for any statement, or null. + */ + public function visitStmtList(Node $node): ?bool + { + foreach ($node->children as $child) { + if (!($child instanceof Node)) { + continue; + } + $result = $this->__invoke($child); + if ($result !== null) { + return $result; + } + $status = (new BlockExitStatusChecker())->__invoke($child); + if ($status !== BlockExitStatusChecker::STATUS_PROCEED) { + if ($status & BlockExitStatusChecker::STATUS_THROW_OR_RETURN_BITMASK) { + return false; + } + continue; + } + } + return null; + } + + /** + * Analyzes a node with kind \ast\AST_IF + * @return ?bool the result seen for an if statement (if $node contains $this->inner or causes this to give up), or null + * @override + */ + public function visitIf(Node $node): ?bool + { + foreach ($node->children as $i => $child) { + // TODO could check first if element (not important) + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $result = $this->visitIfElem($child); + if ($result !== null) { + return $result && $i === 0; + } + } + return null; + } + + /** + * Analyzes a node with kind \ast\AST_IF_ELEM + * @return ?bool the result seen for an if statement element (if $node contains $this->inner or causes this to give up), or null + */ + public function visitIfElem(Node $node): ?bool + { + $cond = $node->children['cond'] ?? null; + if ($cond instanceof Node) { + $result = $this->__invoke($cond); + if ($result !== null) { + return $result; + } + } + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null + $result = $this->__invoke($node->children['stmts']); + if ($result !== null) { + // This is a conditional; it's not guaranteed to work + return false; + } + return null; + } + + /** + * Analyzes a node with kind \ast\AST_CONDITIONAL + * @return ?bool the result seen for a conditional + */ + public function visitConditional(Node $node): ?bool + { + $cond = $node->children['cond']; + if ($cond instanceof Node) { + $result = $this->__invoke($cond); + if ($result !== null) { + return $result; + } + } + foreach (['true', 'false'] as $sub_node_name) { + $value = $node->children[$sub_node_name]; + if (!($value instanceof Node)) { + continue; + } + $result = $this->__invoke($value); + if ($result !== null) { + // This is a conditional; it's not guaranteed to work + return false; + } + } + return null; + } + + /** + * Returns true if there are no break/return/throw/etc statements + * within the method that would prevent $inner (a descendant node of $node) + * to be reached from the start of evaluating the statements in $node. + * + * This does not attempt to check if any statements in $node might indirectly throw. + */ + public static function willUnconditionallyBeReached(Node $node, Node $inner): bool + { + return (new self($inner))->__invoke($node) ?? false; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/RedundantCondition.php b/bundled-libs/phan/phan/src/Phan/Analysis/RedundantCondition.php new file mode 100644 index 000000000..d05c9f216 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/RedundantCondition.php @@ -0,0 +1,206 @@ + Issue::RedundantConditionInLoop, + Issue::ImpossibleCondition => Issue::ImpossibleConditionInLoop, + Issue::ImpossibleTypeComparison => Issue::ImpossibleTypeComparisonInLoop, + Issue::SuspiciousValueComparison => Issue::SuspiciousValueComparisonInLoop, + Issue::SuspiciousWeakTypeComparison => Issue::SuspiciousWeakTypeComparisonInLoop, + Issue::CoalescingNeverNull => Issue::CoalescingNeverNullInLoop, + Issue::CoalescingAlwaysNull => Issue::CoalescingAlwaysNullInLoop, + ]; + + private const GLOBAL_ISSUE_NAMES = [ + Issue::RedundantCondition => Issue::RedundantConditionInGlobalScope, + Issue::ImpossibleCondition => Issue::ImpossibleConditionInGlobalScope, + Issue::ImpossibleTypeComparison => Issue::ImpossibleTypeComparisonInGlobalScope, + Issue::SuspiciousValueComparison => Issue::SuspiciousValueComparisonInGlobalScope, + Issue::SuspiciousWeakTypeComparison => Issue::SuspiciousWeakTypeComparisonInGlobalScope, + Issue::CoalescingNeverNull => Issue::CoalescingNeverNullInGlobalScope, + Issue::CoalescingAlwaysNull => Issue::CoalescingAlwaysNullInGlobalScope, + ]; + + /** + * Choose a more specific issue name based on where the issue was emitted from. + * In loops, Phan's checks have higher false positives. + * + * @param Node|int|float|string $node + * @param string $issue_name + */ + public static function chooseSpecificImpossibleOrRedundantIssueKind($node, Context $context, string $issue_name): string + { + if (ParseVisitor::isNonVariableExpr($node)) { + return $issue_name; + } + if ($context->isInGlobalScope()) { + return self::GLOBAL_ISSUE_NAMES[$issue_name] ?? $issue_name; + } + if ($context->isInLoop()) { + return self::LOOP_ISSUE_NAMES[$issue_name] ?? $issue_name; + } + + return $issue_name; + } + + /** + * Emit an issue. If this is in a loop, defer the check until more is known about possible types of the variable in the loop. + * + * @param Node|int|string|float $node + * @param list $issue_args + * @param Closure(UnionType):bool $is_still_issue + */ + public static function emitInstance( + $node, + CodeBase $code_base, + Context $context, + string $issue_name, + array $issue_args, + Closure $is_still_issue, + bool $specialize_issue = true + ): void { + if ($specialize_issue) { + if ($context->isInLoop() && $node instanceof Node) { + $type_fetcher = self::getLoopNodeTypeFetcher($code_base, $node); + if ($type_fetcher) { + $context->deferCheckToOutermostLoop(static function (Context $context_after_loop) use ($code_base, $node, $type_fetcher, $is_still_issue, $issue_name, $issue_args, $context): void { + $var_type = $type_fetcher($context_after_loop); + if ($var_type !== null && ($var_type->isEmpty() || !$is_still_issue($var_type))) { + return; + } + Issue::maybeEmit( + $code_base, + $context, + RedundantCondition::chooseSpecificImpossibleOrRedundantIssueKind($node, $context, $issue_name), + $node->lineno, + ...$issue_args + ); + }); + return; + } + } + $issue_name = RedundantCondition::chooseSpecificImpossibleOrRedundantIssueKind($node, $context, $issue_name); + } + Issue::maybeEmit( + $code_base, + $context, + $issue_name, + $node->lineno ?? $context->getLineNumberStart(), + ...$issue_args + ); + } + + /** + * Returns a closure to fetch the type of an expression that depends on the variables in this loop scope. + * Currently only supports regular variables + * + * @param Node|string|int|float|null $node + * @return ?Closure(Context):(?UnionType) A closure to fetch the type, or null if the inferred type isn't expected to vary. + * @internal + */ + public static function getLoopNodeTypeFetcher(CodeBase $code_base, $node): ?Closure + { + if (!($node instanceof Node)) { + // This scalar won't change. + return null; + } + if ($node->kind === ast\AST_VAR) { + $var_name = $node->children['name']; + if (\is_string($var_name)) { + return static function (Context $context_after_loop) use ($var_name): ?UnionType { + $scope = $context_after_loop->getScope(); + if ($scope->hasVariableWithName($var_name)) { + return $scope->getVariableByName($var_name)->getUnionType()->getRealUnionType(); + } + return null; + }; + } + } + $variable_set = self::getVariableSet($node); + if (!$variable_set) { + // We don't know any variables this uses + return null; + } + return static function (Context $context_after_loop) use ($code_base, $variable_set, $node): ?UnionType { + $scope = $context_after_loop->getScope(); + foreach ($variable_set as $var_name) { + if (!$scope->hasVariableWithName($var_name)) { + return null; + } + } + try { + return UnionTypeVisitor::unionTypeFromNode($code_base, $context_after_loop, $node, false)->getRealUnionType(); + } catch (Exception $_) { + return null; + } + }; + } + + /** + * @param Node|string|int|float $node + * @return associative-array the set of variable names. + * @internal + */ + public static function getVariableSet($node): array + { + if (!$node instanceof Node) { + return []; + } + if ($node->kind === ast\AST_VAR) { + $var_name = $node->children['name']; + if (\is_string($var_name)) { + return [$var_name => $var_name]; + } + } + // @phan-suppress-next-line PhanAccessClassConstantInternal + if (\in_array($node->kind, PhanAnnotationAdder::SCOPE_START_LIST, true)) { + return []; + } + $result = []; + foreach ($node->children as $c) { + if ($c instanceof Node) { + $result += self::getVariableSet($c); + } + } + return $result; + } + + /** + * Returns true for if $node is an expression that wouldn't be null, but for which isset($var_node) can return false. + * + * e.g. `isset($str[5])` + * @param Node|string|int|float $node + */ + public static function shouldNotWarnAboutIssetCheckForNonNullExpression(CodeBase $code_base, Context $context, $node): bool + { + if (!$node instanceof Node) { + return false; + } + if ($node->kind === ast\AST_DIM) { + // Surprisingly, $str[$invalidOffset] is the empty string instead of null, and isset($str[$invalid]) is false. + return UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node->children['expr'], false)->canCastToUnionType(StringType::instance(true)->asPHPDocUnionType()); + } + return false; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ReferenceCountsAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/ReferenceCountsAnalyzer.php new file mode 100644 index 000000000..4a2515933 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ReferenceCountsAnalyzer.php @@ -0,0 +1,524 @@ +totalElementCount(); + $i = 0; + + // Functions + self::analyzeGlobalElementListReferenceCounts( + $code_base, + $code_base->getFunctionMap(), + Issue::UnreferencedFunction, + $total_count, + $i + ); + + // Constants + self::analyzeGlobalElementListReferenceCounts( + $code_base, + $code_base->getGlobalConstantMap(), + Issue::UnreferencedConstant, + $total_count, + $i + ); + + // Classes + self::analyzeGlobalElementListReferenceCounts( + $code_base, + $code_base->getUserDefinedClassMap(), + Issue::UnreferencedClass, + $total_count, + $i + ); + + // Class Maps + $elements_to_analyze = []; + foreach ($code_base->getClassMapMap() as $class_map) { + foreach (self::getElementsFromClassMapForDeferredAnalysis( + $code_base, + $class_map, + $total_count, + $i + ) as $element) { + $elements_to_analyze[] = $element; + } + } + + static $issue_types = [ + ClassConstant::class => Issue::UnreferencedPublicClassConstant, // This is overridden + Method::class => Issue::UnreferencedPublicMethod, // This is overridden + Property::class => Issue::UnreferencedPublicProperty, // This is overridden + ]; + + foreach ($elements_to_analyze as $element) { + $issue_type = $issue_types[\get_class($element)]; + self::analyzeElementReferenceCounts($code_base, $element, $issue_type); + } + CLI::progress('dead code', 1.0); + } + + /** + * @param CodeBase $code_base + * @param ClassMap $class_map + * @param int $total_count + * @param int $i + * + * @return \Generator|ClassElement[] + * @phan-return \Generator + */ + private static function getElementsFromClassMapForDeferredAnalysis( + CodeBase $code_base, + ClassMap $class_map, + int $total_count, + int &$i + ): \Generator { + // Constants + yield from self::getElementsFromElementListForDeferredAnalysis( + $code_base, + $class_map->getClassConstantMap(), + $total_count, + $i + ); + + // Properties + yield from self::getElementsFromElementListForDeferredAnalysis( + $code_base, + $class_map->getPropertyMap(), + $total_count, + $i + ); + + // Methods + yield from self::getElementsFromElementListForDeferredAnalysis( + $code_base, + $class_map->getMethodMap(), + $total_count, + $i + ); + } + + /** + * @param CodeBase $code_base + * @param iterable $element_list + * @param string $issue_type + * @param int $total_count + * @param int $i + */ + private static function analyzeGlobalElementListReferenceCounts( + CodeBase $code_base, + iterable $element_list, + string $issue_type, + int $total_count, + int &$i + ): void { + foreach ($element_list as $element) { + CLI::progress('dead code', (++$i) / $total_count, $element); + // Don't worry about internal elements + if ($element->isPHPInternal() || $element->getContext()->isPHPInternal()) { + // The extra check of the context is necessary for code in internal_stubs + // which aren't exactly internal to PHP. + continue; + } + self::analyzeElementReferenceCounts($code_base, $element, $issue_type); + } + } + + /** + * @param CodeBase $code_base + * @param iterable $element_list + * @param int $total_count + * @param int $i + * + * @return \Generator|ClassElement[] + * @phan-return \Generator + */ + private static function getElementsFromElementListForDeferredAnalysis( + CodeBase $code_base, + iterable $element_list, + int $total_count, + int &$i + ): \Generator { + foreach ($element_list as $element) { + CLI::progress('dead code', (++$i) / $total_count, $element); + // Don't worry about internal elements + if ($element->isPHPInternal() || $element->getContext()->isPHPInternal()) { + // The extra check of the context is necessary for code in internal_stubs + // which aren't exactly internal to PHP. + continue; + } + // Currently, deferred analysis is only needed for class elements, which can be inherited + // (And we may track the references to the inherited version of the original) + if (!$element instanceof ClassElement) { + throw new TypeError("Expected an iterable of ClassElement values"); + } + // should not warn about self::class + if ($element instanceof ClassConstant && \strcasecmp($element->getName(), 'class') === 0) { + continue; + } + $fqsen = $element->getFQSEN(); + if ($element instanceof Method || $element instanceof Property) { + $defining_fqsen = $element->getRealDefiningFQSEN(); + } else { + $defining_fqsen = $element->getDefiningFQSEN(); + } + + // copy references to methods, properties, and constants into the defining trait or class. + if ($fqsen !== $defining_fqsen) { + $has_references = $element->getReferenceCount($code_base) > 0; + if ($has_references || ($element instanceof Method && ($element->isOverride() && !$element->isPrivate()))) { + $defining_element = null; + if ($defining_fqsen instanceof FullyQualifiedMethodName) { + if ($code_base->hasMethodWithFQSEN($defining_fqsen)) { + $defining_element = $code_base->getMethodByFQSEN($defining_fqsen); + } + } elseif ($defining_fqsen instanceof FullyQualifiedPropertyName) { + if ($code_base->hasPropertyWithFQSEN($defining_fqsen)) { + $defining_element = $code_base->getPropertyByFQSEN($defining_fqsen); + } + } elseif ($defining_fqsen instanceof FullyQualifiedClassConstantName) { + if ($code_base->hasClassConstantWithFQSEN($defining_fqsen)) { + $defining_element = $code_base->getClassConstantByFQSEN($defining_fqsen); + } + } + if ($defining_element !== null) { + if ($has_references) { + $defining_element->copyReferencesFrom($element); + } elseif ($element instanceof Method) { + foreach ($element->getOverriddenMethods($code_base) as $overridden_element) { + $defining_element->copyReferencesFrom($overridden_element); + } + } + } + } + continue; + } + + // Don't analyze elements defined in a parent class. + // We copy references to methods, properties, and constants into the defining trait or class before this. + if ($element->isOverride() && !$element->isPrivate()) { + continue; + } + + $defining_class = + $element->getClass($code_base); + + if ($element instanceof Method) { + // Ignore magic methods + if ($element->isMagic()) { + continue; + } + // Don't analyze abstract methods, as they're uncallable. + // (Every method on an interface is abstract) + if ($element->isAbstract() || $defining_class->isInterface()) { + continue; + } + } elseif ($element instanceof Property) { + // Skip properties on classes that were derived from (at)property annotations on classes + // or were automatically generated for classes with __get or __set methods + // (or undeclared properties that were automatically added depending on configs) + if ($element->isDynamicProperty()) { + continue; + } + // TODO: may want to continue to skip `if ($defining_class->hasGetOrSetMethod($code_base)) {` + // E.g. a __get() method that is implemented as `return $this->"_$name"`. + // (at)phan-file-suppress is an easy enough workaround, though + } + yield $element; + } + } + + /** + * Check to see if the given AddressableElement is a duplicate + */ + private static function analyzeElementReferenceCounts( + CodeBase $code_base, + AddressableElement $element, + string $issue_type + ): void { + /* + print "digraph G {\n"; + foreach ($element->getReferenceList() as $file_ref) { + print "\t\"{$file_ref->getFile()}\" -> \"{$element->getFileRef()->getFile()}\";\n"; + } + print "}\n"; + */ + + // Make issue types granular so that these can be fixed in smaller steps. + // E.g. composer libraries may have unreferenced but used public methods, properties, and class constants, + // and those would have higher false positives than private/protected elements. + // + // Make $issue_type specific **first**, so that issue suppressions are checked against the proper issue type + if ($element instanceof ClassElement) { + if ($element instanceof Method) { + if ($element->isPrivate()) { + $issue_type = Issue::UnreferencedPrivateMethod; + } elseif ($element->isProtected()) { + $issue_type = Issue::UnreferencedProtectedMethod; + } else { + $issue_type = Issue::UnreferencedPublicMethod; + } + } elseif ($element instanceof Property) { + if ($element->isFromPHPDoc()) { + $issue_type = Issue::UnreferencedPHPDocProperty; + } elseif ($element->isPrivate()) { + $issue_type = Issue::UnreferencedPrivateProperty; + } elseif ($element->isProtected()) { + $issue_type = Issue::UnreferencedProtectedProperty; + } else { + $issue_type = Issue::UnreferencedPublicProperty; + } + } elseif ($element instanceof ClassConstant) { + if ($element->isPrivate()) { + $issue_type = Issue::UnreferencedPrivateClassConstant; + } elseif ($element->isProtected()) { + $issue_type = Issue::UnreferencedProtectedClassConstant; + } else { + $issue_type = Issue::UnreferencedPublicClassConstant; + } + } + } elseif ($element instanceof Func) { + if (\strcasecmp($element->getName(), "__autoload") === 0) { + return; + } + if ($element->getFQSEN()->isClosure()) { + if (self::hasSuppressionForUnreferencedClosure($code_base, $element)) { + // $element->getContext() is the context within the closure - We also want to check the context of the function-like outside of the closure(s). + return; + } + $issue_type = Issue::UnreferencedClosure; + } + } elseif ($element instanceof Clazz) { + if ($element->isAnonymous()) { + // This can't be referenced by name in type signatures, etc. + return; + } + } + + + // If we're suppressing this element type being unreferenced, then exit early. + if ($element->checkHasSuppressIssueAndIncrementCount($issue_type)) { + return; + } + + if ($element->getReferenceCount($code_base) >= 1) { + if ($element instanceof Property) { + if (!$element->hasReadReference()) { + self::maybeWarnWriteOnlyProperty($code_base, $element); + } elseif (!$element->hasWriteReference()) { + self::maybeWarnReadOnlyProperty($code_base, $element); + } + } + return; + } + // getReferenceCount === 0 + + $element_alt = self::findAlternateReferencedElementDeclaration($code_base, $element); + if (!\is_null($element_alt)) { + if ($element_alt->getReferenceCount($code_base) >= 1) { + if ($element_alt instanceof Property) { + if (!$element_alt->hasReadReference()) { + self::maybeWarnWriteOnlyProperty($code_base, $element_alt); + } elseif (!($element_alt->hasWriteReference())) { + self::maybeWarnReadOnlyProperty($code_base, $element_alt); + } + } + // If there is a reference to the "canonical" declaration (the one which was parsed first), + // then also treat it as a reference to the duplicate. + return; + } + if ($element_alt->isPHPInternal()) { + // For efficiency, Phan doesn't track references to internal classes. + // Phan already emitted a warning about duplicating an internal class. + return; + } + } + // If there are duplicate declarations, display issues for unreferenced elements on each declaration. + Issue::maybeEmit( + $code_base, + $element->getContext(), + $issue_type, + $element->getFileRef()->getLineNumberStart(), + $element->getRepresentationForIssue() + ); + } + + private static function hasSuppressionForUnreferencedClosure(CodeBase $code_base, Func $func): bool + { + $context = $func->getContext(); + return $context->withScope($context->getScope()->getParentScope())->hasSuppressIssue($code_base, Issue::UnreferencedClosure); + } + + private static function maybeWarnWriteOnlyProperty(CodeBase $code_base, Property $property): void + { + if ($property->isWriteOnly()) { + // Handle annotations such as property-write and phan-write-only + return; + } + if ($property->isFromPHPDoc()) { + $issue_type = Issue::WriteOnlyPHPDocProperty; + } elseif ($property->isPrivate()) { + $issue_type = Issue::WriteOnlyPrivateProperty; + } elseif ($property->isProtected()) { + $issue_type = Issue::WriteOnlyProtectedProperty; + } else { + $issue_type = Issue::WriteOnlyPublicProperty; + } + if ($property->checkHasSuppressIssueAndIncrementCount($issue_type)) { + return; + } + $property_alt = self::findAlternateReferencedElementDeclaration($code_base, $property); + if ($property_alt instanceof Property) { + if ($property_alt->hasReadReference()) { + return; + } + } + Issue::maybeEmit( + $code_base, + $property->getContext(), + $issue_type, + $property->getFileRef()->getLineNumberStart(), + $property->getRepresentationForIssue() + ); + } + + private static function maybeWarnReadOnlyProperty(CodeBase $code_base, Property $property): void + { + if ($property->isReadOnly()) { + // Handle annotations such as property-read and phan-read-only. + return; + } + if ($property->isFromPHPDoc()) { + $issue_type = Issue::ReadOnlyPHPDocProperty; + } elseif ($property->isPrivate()) { + $issue_type = Issue::ReadOnlyPrivateProperty; + } elseif ($property->isProtected()) { + $issue_type = Issue::ReadOnlyProtectedProperty; + } else { + $issue_type = Issue::ReadOnlyPublicProperty; + } + if ($property->checkHasSuppressIssueAndIncrementCount($issue_type)) { + return; + } + $property_alt = self::findAlternateReferencedElementDeclaration($code_base, $property); + if ($property_alt instanceof Property) { + if ($property_alt->hasWriteReference()) { + return; + } + } + // echo "known references to $property: " . implode(array_map('strval', $property->getReferenceList())) . "\n"; + Issue::maybeEmit( + $code_base, + $property->getContext(), + $issue_type, + $property->getFileRef()->getLineNumberStart(), + $property->getRepresentationForIssue() + ); + } + + /** + * Find Elements with FQSENs that are the same as $element's FQSEN, + * apart from the alternate id. + * (i.e. duplicate declarations) + */ + public static function findAlternateReferencedElementDeclaration( + CodeBase $code_base, + AddressableElement $element + ): ?AddressableElement { + $old_fqsen = $element->getFQSEN(); + if ($old_fqsen instanceof FullyQualifiedGlobalStructuralElement) { + $fqsen = $old_fqsen->getCanonicalFQSEN(); + if ($fqsen === $old_fqsen) { + return null; // $old_fqsen was not an alternative + } + if ($fqsen instanceof FullyQualifiedFunctionName) { + if ($code_base->hasFunctionWithFQSEN($fqsen)) { + return $code_base->getFunctionByFQSEN($fqsen); + } + return null; + } elseif ($fqsen instanceof FullyQualifiedClassName) { + if ($code_base->hasClassWithFQSEN($fqsen)) { + return $code_base->getClassByFQSEN($fqsen); + } + return null; + } elseif ($fqsen instanceof FullyQualifiedGlobalConstantName) { + if ($code_base->hasGlobalConstantWithFQSEN($fqsen)) { + return $code_base->getGlobalConstantByFQSEN($fqsen); + } + return null; + } + } elseif ($old_fqsen instanceof FullyQualifiedClassElement) { + // If this needed to be more thorough, + // the code adding references could treat uses from within the classes differently from outside. + $fqsen = $old_fqsen->getCanonicalFQSEN(); + if ($fqsen === $old_fqsen) { + return null; // $old_fqsen was not an alternative + } + + if ($fqsen instanceof FullyQualifiedMethodName) { + if ($code_base->hasMethodWithFQSEN($fqsen)) { + return $code_base->getMethodByFQSEN($fqsen); + } + return null; + } elseif ($fqsen instanceof FullyQualifiedPropertyName) { + if ($code_base->hasPropertyWithFQSEN($fqsen)) { + return $code_base->getPropertyByFQSEN($fqsen); + } + return null; + } elseif ($fqsen instanceof FullyQualifiedClassConstantName) { + if ($code_base->hasClassConstantWithFQSEN($fqsen)) { + return $code_base->getClassConstantByFQSEN($fqsen); + } + return null; + } + } + return null; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/RegexAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/RegexAnalyzer.php new file mode 100644 index 000000000..ca81e67e2 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/RegexAnalyzer.php @@ -0,0 +1,128 @@ + $argument_list + */ + public static function getPregMatchUnionType( + CodeBase $code_base, + Context $context, + array $argument_list + ): UnionType { + static $string_array_type = null; + static $string_type = null; + static $array_type = null; + static $shape_array_type = null; + static $shape_array_inner_type = null; + if ($string_array_type === null) { + // Note: Patterns **can** have named subpatterns + $string_array_type = UnionType::fromFullyQualifiedPHPDocString('string[]'); + $string_type = UnionType::fromFullyQualifiedPHPDocString('string'); + $array_type = UnionType::fromFullyQualifiedPHPDocString('array'); + $shape_array_type = UnionType::fromFullyQualifiedPHPDocString('array{0:string,1:int}[]'); + $shape_array_inner_type = UnionType::fromFullyQualifiedPHPDocString('array{0:string,1:int}'); + } + $regex_node = $argument_list[0]; + $regex = $regex_node instanceof Node ? (new ContextNode($code_base, $context, $regex_node))->getEquivalentPHPScalarValue() : $regex_node; + try { + $regex_group_keys = RegexKeyExtractor::getKeys($regex); + } catch (InvalidArgumentException $_) { + $regex_group_keys = null; + } + if (\count($argument_list) > 3) { + $offset_flags_node = $argument_list[3]; + $bit = (new ContextNode($code_base, $context, $offset_flags_node))->getEquivalentPHPScalarValue(); + } else { + $bit = 0; + } + + if (!\is_int($bit)) { + return $array_type; + } + // TODO: Support PREG_UNMATCHED_AS_NULL + if ($bit & \PREG_OFFSET_CAPTURE) { + if (\is_array($regex_group_keys)) { + return self::makeArrayShape($regex_group_keys, $shape_array_inner_type); + } + return $shape_array_type; + } + + if (\is_array($regex_group_keys)) { + return self::makeArrayShape($regex_group_keys, $string_type); + } + return $string_array_type; + } + + /** + * Returns the union type of the matches output parameter in a call to `preg_match_all()` + * with the nodes in $argument_list. + * + * @param list $argument_list + */ + public static function getPregMatchAllUnionType( + CodeBase $code_base, + Context $context, + array $argument_list + ): UnionType { + if (\count($argument_list) > 3) { + $offset_flags_node = $argument_list[3]; + $bit = (new ContextNode($code_base, $context, $offset_flags_node))->getEquivalentPHPScalarValue(); + } else { + $bit = 0; + } + + if (!\is_int($bit)) { + return UnionType::fromFullyQualifiedPHPDocString('array[]'); + } + + $shape_array_type = self::getPregMatchUnionType($code_base, $context, $argument_list); + if ($bit & \PREG_SET_ORDER) { + return $shape_array_type->asGenericArrayTypes(GenericArrayType::KEY_INT); + } + return $shape_array_type->withMappedElementTypes(static function (UnionType $type): UnionType { + return $type->elementTypesToGenericArray(GenericArrayType::KEY_INT); + }); + } + + /** + * @param associative-array $regex_group_keys + */ + private static function makeArrayShape( + array $regex_group_keys, + UnionType $type + ): UnionType { + $field_types = \array_map( + /** @param true $_ */ + static function (bool $_) use ($type): UnionType { + return $type; + }, + $regex_group_keys + ); + // NOTE: This is treated as not 100% guaranteed to be an array to avoid false positives about comparing to non-arrays + return ArrayShapeType::fromFieldTypes($field_types, false)->asPHPDocUnionType(); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ScopeVisitor.php b/bundled-libs/phan/phan/src/Phan/Analysis/ScopeVisitor.php new file mode 100644 index 000000000..d623e1edd --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ScopeVisitor.php @@ -0,0 +1,337 @@ +context; + } + + /** + * Visit a node with kind `\ast\AST_DECLARE` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitDeclare(Node $node): Context + { + $declares = $node->children['declares']; + $context = $this->context; + foreach ($declares->children as $elem) { + if (!$elem instanceof Node) { + throw new AssertionError('Expected an array of declaration elements'); + } + ['name' => $name, 'value' => $value] = $elem->children; + if ('strict_types' === $name && \is_int($value)) { + $context = $context->withStrictTypes($value); + } + } + + return $context; + } + + /** + * Visit a node with kind `\ast\AST_NAMESPACE` + * + * @param Node $node + * A node to parse + * + * @return Context + * A new context resulting from parsing the node + */ + public function visitNamespace(Node $node): Context + { + $namespace = '\\' . (string)$node->children['name']; + return $this->context->withNamespace($namespace); + } + + /** + * Visit a node with kind `\ast\AST_GROUP_USE` + * such as `use \ast\Node;`. + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitGroupUse(Node $node): Context + { + $children = $node->children; + + $prefix = \array_shift($children); + + $context = $this->context; + + $alias_target_map = self::aliasTargetMapFromUseNode( + $children['uses'], // @phan-suppress-current-line PhanTypeMismatchArgumentNullable the key is also used by AST_CLOSURE + $prefix, + $node->flags ?? 0 + ); + foreach ($alias_target_map as $alias => [$flags, $target, $lineno]) { + $context = $context->withNamespaceMap( + $flags, + $alias, + $target, + $lineno, + $this->code_base + ); + } + + return $context; + } + + /** + * Visit a node with kind `\ast\AST_USE` + * such as `use \ast\Node;`. + * + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitUse(Node $node): Context + { + $context = $this->context; + $minimum_target_php_version = Config::get_closest_minimum_target_php_version_id(); + + foreach (self::aliasTargetMapFromUseNode($node) as $alias => [$flags, $target, $lineno]) { + $flags = $node->flags ?: $flags; + if ($flags === \ast\flags\USE_NORMAL && $minimum_target_php_version < 70200) { + self::analyzeUseElemCompatibility($alias, $target, $minimum_target_php_version, $lineno); + } + if (\strcasecmp($target->getNamespace(), $context->getNamespace()) === 0) { + $this->maybeWarnSameNamespaceUse($alias, $target, $flags, $lineno); + } + $context = $context->withNamespaceMap( + $flags, + $alias, + $target, + $lineno, + $this->code_base + ); + } + + return $context; + } + + private function maybeWarnSameNamespaceUse(string $alias, FullyQualifiedGlobalStructuralElement $target, int $flags, int $lineno): void + { + if (\strcasecmp($alias, $target->getName()) !== 0) { + return; + } + if ($flags === ast\flags\USE_FUNCTION) { + if ($target->getNamespace() !== '\\') { + return; + } + $issue_type = Issue::UseFunctionNoEffect; + } elseif ($flags === ast\flags\USE_CONST) { + if ($target->getNamespace() !== '\\') { + return; + } + $issue_type = Issue::UseConstantNoEffect; + } else { + if ($target->getNamespace() !== '\\') { + if (!Config::getValue('warn_about_relative_include_statement')) { + return; + } + $issue_type = Issue::UseNormalNamespacedNoEffect; + } else { + $issue_type = Issue::UseNormalNoEffect; + } + } + $this->emitIssue( + $issue_type, + $lineno, + $target + ); + } + + private const USE_ERRORS = [ + 'iterable' => Issue::CompatibleUseIterablePHP71, + 'object' => Issue::CompatibleUseObjectPHP71, + 'mixed' => Issue::CompatibleUseMixed, + ]; + + private function analyzeUseElemCompatibility( + string $alias, + FQSEN $target, + int $minimum_target_php_version, + int $lineno + ): void { + $alias_lower = \strtolower($alias); + if ($minimum_target_php_version < 70100) { + if ($alias_lower === 'void') { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::CompatibleUseVoidPHP70, + $lineno, + $target + ); + return; + } + } + $issue_name = self::USE_ERRORS[$alias_lower] ?? null; + if ($issue_name) { + Issue::maybeEmit( + $this->code_base, + $this->context, + $issue_name, + $lineno, + $target + ); + } + } + + /** + * @param Node $node + * The node with the use statement + * + * @param int $flags + * An optional node flag specifying the type + * of the use clause. + * + * @return array + * A map from alias to target + * + * @suppress PhanPartialTypeMismatchReturn TODO: investigate + * @suppress PhanThrowTypeAbsentForCall + */ + public static function aliasTargetMapFromUseNode( + Node $node, + string $prefix = '', + int $flags = 0 + ): array { + if ($node->kind !== \ast\AST_USE) { + throw new AssertionError('Method takes AST_USE nodes'); + } + + $map = []; + foreach ($node->children as $child_node) { + if (!$child_node instanceof Node) { + throw new AssertionError('Expected array of AST_USE_ELEM nodes'); + } + $target = $child_node->children['name']; + + if (isset($child_node->children['alias'])) { + $alias = $child_node->children['alias']; + } else { + if (($pos = \strrpos($target, '\\')) !== false) { + $alias = \substr($target, $pos + 1); + } else { + $alias = $target; + } + } + if (!\is_string($alias)) { + // Should be impossible + continue; + } + + // if AST_USE does not have any flags set, then its AST_USE_ELEM + // children will (this will be for AST_GROUP_USE) + + // The 'use' type can be defined on the `AST_GROUP_USE` node, the + // `AST_USE_ELEM` or on the child element. + $use_flag = $flags ?: $node->flags ?: $child_node->flags; + + if ($use_flag === \ast\flags\USE_FUNCTION) { + $parts = \explode('\\', $target); + $function_name = \array_pop($parts); + $target = FullyQualifiedFunctionName::make( + rtrim($prefix, '\\') . '\\' . implode('\\', $parts), + $function_name + ); + } elseif ($use_flag === \ast\flags\USE_CONST) { + $parts = \explode('\\', $target); + $name = \array_pop($parts); + $target = FullyQualifiedGlobalConstantName::make( + rtrim($prefix, '\\') . '\\' . implode('\\', $parts), + $name + ); + } elseif ($use_flag === \ast\flags\USE_NORMAL) { + $target = FullyQualifiedClassName::fromFullyQualifiedString( + rtrim($prefix, '\\') . '\\' . $target + ); + } else { + // If we get to this spot and don't know what + // kind of a use clause we're dealing with, its + // likely that this is a `USE` node which is + // a child of a `GROUP_USE` and we already + // handled it when analyzing the parent + // node. + continue; + } + + $map[$alias] = [$use_flag, $target, $child_node->lineno]; + } + + return $map; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Analysis/ThrowsTypesAnalyzer.php b/bundled-libs/phan/phan/src/Phan/Analysis/ThrowsTypesAnalyzer.php new file mode 100644 index 000000000..9a9a448e9 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Analysis/ThrowsTypesAnalyzer.php @@ -0,0 +1,144 @@ +getThrowsUnionType()->getTypeSet() as $type) { + // TODO: When analyzing the method body, only check the valid exceptions + self::analyzeSingleThrowType($code_base, $method, $type); + } + } catch (RecursionDepthException $_) { + } + } + + /** + * Check a throw type to make sure it's valid + * + * @return bool - True if the type can be thrown + */ + private static function analyzeSingleThrowType( + CodeBase $code_base, + FunctionInterface $method, + Type $type + ): bool { + /** + * @param list $args + */ + $maybe_emit_for_method = static function (string $issue_type, array $args, Suggestion $suggestion = null) use ($code_base, $method): void { + Issue::maybeEmitWithParameters( + $code_base, + $method->getContext(), + $issue_type, + $method->getContext()->getLineNumberStart(), + $args, + $suggestion + ); + }; + if (!$type->isObject()) { + $maybe_emit_for_method( + Issue::TypeInvalidThrowsNonObject, + [$method->getName(), (string)$type] + ); + return false; + } + if ($type instanceof TemplateType) { + // TODO: Add unit tests of templates for return types and checks. + // E.g. should warn if passing in something that can't cast to throwable + if ($method instanceof Method && $method->isStatic() && !$method->declaresTemplateTypeInComment($type)) { + $maybe_emit_for_method( + Issue::TemplateTypeStaticMethod, + [(string)$method->getFQSEN()] + ); + } + return false; + } + if ($type instanceof ObjectType) { + // (at)throws object is valid and should be treated like Throwable + // NOTE: catch (object $o) does nothing in php 7.2. + return true; + } + static $throwable; + if ($throwable === null) { + $throwable = Type::throwableInstance(); + } + if ($type === $throwable) { + // allow (at)throws Throwable. + return true; + } + $type = $type->withStaticResolvedInContext($method->getContext()); + + $type_fqsen = FullyQualifiedClassName::fromType($type); + if (!$code_base->hasClassWithFQSEN($type_fqsen)) { + $maybe_emit_for_method( + Issue::UndeclaredTypeThrowsType, + [$method->getName(), $type], + self::suggestSimilarClassForThrownClass($code_base, $method->getContext(), $type_fqsen) + ); + return false; + } + $exception_class = $code_base->getClassByFQSEN($type_fqsen); + if ($exception_class->isTrait() || $exception_class->isInterface()) { + $maybe_emit_for_method( + $exception_class->isTrait() ? Issue::TypeInvalidThrowsIsTrait : Issue::TypeInvalidThrowsIsInterface, + [$method->getName(), $type] + ); + return $exception_class->isInterface(); + } + + if (!($type->asExpandedTypes($code_base)->hasType($throwable))) { + $maybe_emit_for_method( + Issue::TypeInvalidThrowsNonThrowable, + [$method->getName(), $type], + self::suggestSimilarClassForThrownClass($code_base, $method->getContext(), $type_fqsen) + ); + } + return true; + } + + protected static function suggestSimilarClassForThrownClass( + CodeBase $code_base, + Context $context, + FullyQualifiedClassName $type_fqsen + ): ?Suggestion { + return IssueFixSuggester::suggestSimilarClass( + $code_base, + $context, + $type_fqsen, + IssueFixSuggester::createFQSENFilterFromClassFilter($code_base, static function (Clazz $class) use ($code_base): bool { + if ($class->isTrait()) { + return false; + } + return $class->getFQSEN()->asType()->asExpandedTypes($code_base)->hasType(Type::throwableInstance()); + }) + ); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/BlockAnalysisVisitor.php b/bundled-libs/phan/phan/src/Phan/BlockAnalysisVisitor.php new file mode 100644 index 000000000..dd9b6a609 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/BlockAnalysisVisitor.php @@ -0,0 +1,3247 @@ +parent_node_list[] = $parent_node; + } + } + + // No-ops for frequent node types + public function visitVar(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + $name_node = $node->children['name']; + // E.g. ${expr()} is valid PHP. Recurse if that's a node. + if ($name_node instanceof Node) { + // Step into each child node and get an + // updated context for the node + $context = $this->analyzeAndGetUpdatedContext($context, $node, $name_node); + } + + return $this->postOrderAnalyze($context, $node); + } + + /** + * @param Node $node @phan-unused-param this was analyzed in visitUse + */ + public function visitUseElem(Node $node): Context + { + // Could invoke plugins, but not right now + return $this->context; + } + + /** + * Analyzes a namespace block or statement (e.g. `namespace NS\SubNS;` or `namespace OtherNS { ... }`) + * @param Node $node a node of type AST_NAMESPACE + */ + public function visitNamespace(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + // If there are multiple namespaces in the file, have to warn about unused entries in the current namespace first. + // If this is the first namespace, then there wouldn't be any use statements yet. + // TODO: This may not be the case if the language server is used + // @phan-suppress-next-line PhanAccessMethodInternal + $context->warnAboutUnusedUseElements($this->code_base); + + // Visit the given node populating the code base + // with anything we learn and get a new context + // indicating the state of the world within the + // given node + $context = (new PreOrderAnalysisVisitor( + $this->code_base, + $context + ))->visitNamespace($node); + + // We already imported namespace constants earlier; use those. + // @phan-suppress-next-line PhanAccessMethodInternal + $context->importNamespaceMapFromParsePhase($this->code_base); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + // The namespace may either have a list of statements (`namespace Foo {}`) + // or be null (`namespace Foo;`) + $stmts_node = $node->children['stmts']; + if ($stmts_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext($context, $node, $stmts_node); + } + + return $this->postOrderAnalyze($context, $node); + } + + /** + * Analyzes a node with type AST_NAME (Relative or fully qualified name) + */ + public function visitName(Node $node): Context + { + $context = $this->context; + // Only invoke post-order plugins, needed for NodeSelectionPlugin. + // PostOrderAnalysisVisitor and PreOrderAnalysisVisitor don't do anything. + // Optimized because this is frequently called + ConfigPluginSet::instance()->postAnalyzeNode( + $this->code_base, + $context, + $node, + $this->parent_node_list + ); + return $context; + } + + /** + * For non-special nodes such as statement lists (AST_STMT_LIST), + * we propagate the context and scope from the parent, + * through the individual statements, and return a Context with the modified scope. + * + * │ + * ▼ + * ┌──● + * │ + * ●──●──● + * │ + * ●──┘ + * │ + * ▼ + */ + public function visitStmtList(Node $node): Context + { + $context = $this->context; + $plugin_set = ConfigPluginSet::instance(); + $plugin_set->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + foreach ($node->children as $child_node) { + // Skip any non Node children. + if (!($child_node instanceof Node)) { + $this->handleScalarStmt($node, $context, $child_node); + continue; + } + $context->clearCachedUnionTypes(); + + // Step into each child node and get an + // updated context for the node + try { + $context = $this->analyzeAndGetUpdatedContext($context, $node, $child_node); + } catch (IssueException $e) { + // This is a fallback - Exceptions should be caught at a deeper level if possible + Issue::maybeEmitInstance($this->code_base, $context, $e->getIssueInstance()); + } + } + $plugin_set->postAnalyzeNode( + $this->code_base, + $context, + $node, + $this->parent_node_list + ); + return $context; + } + + /** + * Optimized visitor for arrays, skipping unnecessary steps. + * Equivalent to visit() + */ + public function visitArray(Node $node): Context + { + $context = $this->context; + $context->setLineNumberStart($node->lineno); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + $this->parent_node_list[] = $node; + try { + foreach ($node->children as $child_node) { + // Skip any non Node children. + if (!($child_node instanceof Node)) { + continue; + } + + // Step into each child node and get an + // updated context for the node + if ($child_node->kind === ast\AST_ARRAY_ELEM) { + $context = $this->visitArrayElem($child_node); + } elseif ($child_node->kind === ast\AST_UNPACK) { + // @phan-suppress-next-line PhanUndeclaredProperty set to distinguish this from unpack in calls, which does require array keys to be consecutive + $child_node->is_in_array = true; + $context = $this->visitUnpack($child_node); + } else { + throw new AssertionError("Unexpected node in ast\AST_ARRAY: " . \Phan\Debug::nodeToString($child_node)); + } + } + } finally { + \array_pop($this->parent_node_list); + } + + return $this->postOrderAnalyze($context, $node); + } + + /** + * Optimized visitor for array elements, skipping unnecessary steps. + * Equivalent to visitArrayElem + */ + public function visitArrayElem(Node $node): Context + { + $context = $this->context; + $context->setLineNumberStart($node->lineno); + + // Let any configured plugins do a pre-order + // analysis of the node. + $plugin_set = ConfigPluginSet::instance(); + $plugin_set->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + foreach ($node->children as $child_node) { + // Skip any non Node children. + if (!($child_node instanceof Node)) { + continue; + } + + // Step into each child node and get an + // updated context for the node + $context = $this->analyzeAndGetUpdatedContext($context, $node, $child_node); + } + + $plugin_set->postAnalyzeNode( + $this->code_base, + $context, + $node + ); + return $context; + } + + /** + * @param Node $node + * @param Context $context + * @param int|float|string|null $child_node (probably not null) + */ + private function handleScalarStmt(Node $node, Context $context, $child_node): void + { + if (\is_string($child_node)) { + $consumed = false; + if (\strpos($child_node, '@phan-') !== false) { + // Add @phan-var and @phan-suppress annotations in string literals to the local scope + $this->analyzeSubstituteVarAssert($this->code_base, $context, $child_node); + $consumed = true; + } + $consumed = ConfigPluginSet::instance()->analyzeStringLiteralStatement($this->code_base, $context, $child_node) || $consumed; + if (!$consumed) { + Issue::maybeEmit( + $this->code_base, + $context, + Issue::NoopStringLiteral, + $context->getLineNumberStart() ?: $this->getLineNumberOfParent() ?: $node->lineno, + StringUtil::jsonEncode($child_node) + ); + } + } elseif (\is_scalar($child_node)) { + Issue::maybeEmit( + $this->code_base, + $context, + Issue::NoopNumericLiteral, + $context->getLineNumberStart() ?: $this->getLineNumberOfParent() ?: $node->lineno, + \var_export($child_node, true) + ); + } + } + + private function getLineNumberOfParent(): int + { + $parent = end($this->parent_node_list); + if (!($parent instanceof Node)) { + return 0; + } + return $parent->lineno; + } + + private const PHAN_FILE_SUPPRESS_REGEX = + '/@phan-file-suppress\s+' . Builder::SUPPRESS_ISSUE_LIST . '/'; // @phan-suppress-current-line PhanAccessClassConstantInternal + + + private const PHAN_VAR_REGEX = + '/@(phan-var(?:-force)?)\b\s*(' . UnionType::union_type_regex . ')\s*&?\\$' . Builder::WORD_REGEX . '/'; + // @phan-suppress-previous-line PhanAccessClassConstantInternal + + private const PHAN_DEBUG_VAR_REGEX = + '/@phan-debug-var\s+\$(' . Builder::WORD_REGEX . '(,\s*\$' . Builder::WORD_REGEX . ')*)/'; + // @phan-suppress-previous-line PhanAccessClassConstantInternal + + /** + * Parses annotations such as "(at)phan-var int $myVar" and "(at)phan-var-force ?MyClass $varName" annotations from inline string literals. + * (php-ast isn't able to parse inline doc comments, so string literals are used for rare edge cases where assert/if statements don't work) + * + * Modifies the type of the variable (in the scope of $context) to be identical to the annotated union type. + */ + private function analyzeSubstituteVarAssert(CodeBase $code_base, Context $context, string $text): void + { + $has_known_annotations = false; + if (\preg_match_all(self::PHAN_VAR_REGEX, $text, $matches, \PREG_SET_ORDER) > 0) { + $has_known_annotations = true; + foreach ($matches as $group) { + $annotation_name = $group[1]; + $type_string = $group[2]; + $var_name = $group[16]; + $type = UnionType::fromStringInContext($type_string, $context, Type::FROM_PHPDOC); + self::createVarForInlineComment($code_base, $context, $var_name, $type, $annotation_name === 'phan-var-force'); + } + } + + if (\preg_match_all(self::PHAN_FILE_SUPPRESS_REGEX, $text, $matches, \PREG_SET_ORDER) > 0) { + $has_known_annotations = true; + if (!Config::getValue('disable_file_based_suppression')) { + foreach ($matches as $group) { + $issue_name_list = $group[1]; + foreach (array_map('trim', explode(',', $issue_name_list)) as $issue_name) { + $code_base->addFileLevelSuppression($context->getFile(), $issue_name); + } + } + } + } + if (\preg_match_all(self::PHAN_DEBUG_VAR_REGEX, $text, $matches, \PREG_SET_ORDER) > 0) { + $has_known_annotations = true; + foreach ($matches as $group) { + foreach (explode(',', $group[1]) as $var_name) { + $var_name = \ltrim(\trim($var_name), '$'); + if ($context->getScope()->hasVariableWithName($var_name)) { + $union_type_string = $context->getScope()->getVariableByName($var_name)->getUnionType()->getDebugRepresentation(); + } else { + $union_type_string = '(undefined)'; + } + Issue::maybeEmit( + $this->code_base, + $context, + Issue::DebugAnnotation, + $context->getLineNumberStart(), + $var_name, + $union_type_string + ); + } + } + } + + if (!$has_known_annotations && preg_match('/@phan-.*/', $text, $match) > 0) { + Issue::maybeEmit( + $code_base, + $context, + Issue::UnextractableAnnotation, + $context->getLineNumberStart(), + rtrim($match[0]) + ); + } + return; + } + + /** + * @see ConditionVarUtil::getVariableFromScope() + */ + private static function createVarForInlineComment(CodeBase $code_base, Context $context, string $variable_name, UnionType $type, bool $create_variable): void + { + if (!$context->getScope()->hasVariableWithName($variable_name)) { + if (Variable::isHardcodedVariableInScopeWithName($variable_name, $context->isInGlobalScope())) { + return; + } + if (!$create_variable && !($context->isInGlobalScope() && Config::getValue('ignore_undeclared_variables_in_global_scope'))) { + Issue::maybeEmitWithParameters( + $code_base, + $context, + Variable::chooseIssueForUndeclaredVariable($context, $variable_name), + $context->getLineNumberStart(), + [$variable_name], + IssueFixSuggester::suggestVariableTypoFix($code_base, $context, $variable_name) + ); + return; + } + $variable = new Variable( + $context, + $variable_name, + $type, + 0 + ); + $context->addScopeVariable($variable); + return; + } + $variable = clone($context->getScope()->getVariableByName( + $variable_name + )); + $variable->setUnionType($type); + $context->addScopeVariable($variable); + } + + /** + * For non-special nodes, we propagate the context and scope + * from the parent, through the children and return the + * modified scope, + * + * │ + * ▼ + * ┌──● + * │ + * ●──●──● + * │ + * ●──┘ + * │ + * ▼ + * + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + */ + public function visit(Node $node): Context + { + $context = $this->context; + $context->setLineNumberStart($node->lineno); + + // Visit the given node populating the code base + // with anything we learn and get a new context + // indicating the state of the world within the + // given node + $context = (new PreOrderAnalysisVisitor( + $this->code_base, + $context + ))->{Element::VISIT_LOOKUP_TABLE[$node->kind] ?? 'handleMissingNodeKind'}($node); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + foreach ($node->children as $child_node) { + // Skip any non Node children. + if (!($child_node instanceof Node)) { + continue; + } + + // Step into each child node and get an + // updated context for the node + $context = $this->analyzeAndGetUpdatedContext($context, $node, $child_node); + } + + return $this->postOrderAnalyze($context, $node); + } + + /** + * This is an abstraction for getting a new, updated context for a child node. + * + * Effectively the same as (new BlockAnalysisVisitor(..., $context, $node, ...)child_node)) + * but is much less repetitive and verbose, and slightly more efficient. + * + * @param Context $context - The original context for $node, before analyzing $child_node + * + * @param Node $node - The parent node of $child_node + * + * @param Node $child_node - The node which will be analyzed to create the updated context. + * + * @return Context (The unmodified $context, or a different Context instance with modifications) + * + * @suppress PhanPluginCanUseReturnType + * NOTE: This is called extremely frequently, so the real signature types were omitted for performance. + */ + private function analyzeAndGetUpdatedContext(Context $context, Node $node, Node $child_node) + { + // Modify the original object instead of creating a new BlockAnalysisVisitor. + // this is slightly more efficient, especially if a large number of unchanged parameters would exist. + $old_context = $this->context; + $this->context = $context; + $this->parent_node_list[] = $node; + try { + return $this->{Element::VISIT_LOOKUP_TABLE[$child_node->kind] ?? 'handleMissingNodeKind'}($child_node); + } finally { + $this->context = $old_context; + \array_pop($this->parent_node_list); + } + } + + /** + * For "for loop" nodes, we analyze the components in the following order as a heuristic: + * + * 1. propagate the context and scope from the parent, + * 2. Update the scope with the initializer of the loop, + * 3. Update the scope with the side effects (e.g. assignments) of the condition of the loop + * 4. Update the scope with the child statements both inside and outside the loop (ignoring branches which will continue/break), + * 5. Update the scope with the statement evaluated after the loop + * + * Then, Phan returns the context with the modified scope. + * + * TODO: merge the contexts together, for better analysis of possibly undefined variables + * + * │ + * cond ▼ + * ●──────●────● init + * │ + * │ (TODO: merge contexts instead) + * ●──●──▶● + * stmts │ + * │ + * ● 'loop' child node (after inner statements) + * │ + * ▼ + * + * Note: Loop analysis uses heuristics for performance and simplicity. + * If we analyzed the stmts of the inner loop body another time, + * we might discover even more possible types of input/resulting variables. + * + * Current limitations: + * + * - contexts from individual break/continue stmts aren't merged + * - contexts from individual break/continue stmts aren't merged + * + * @param Node $node + * An AST node (for a for loop) we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * + * @suppress PhanUndeclaredProperty + * TODO: Add similar handling (e.g. of possibility of 0 iterations) for foreach + */ + public function visitFor(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + $init_node = $node->children['init']; + if ($init_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext( + $context->withLineNumberStart($init_node->lineno), + $node, + $init_node + ); + } + $context = $context->withEnterLoop($node); + + $condition_node = $node->children['cond']; + if ($condition_node instanceof Node) { + $this->parent_node_list[] = $node; + $condition_subnode = false; + try { + // The typical case is `for (init; $x; loop) {}` + // But `for (init; $x, $y; loop) {}` is rare but possible, which requires evaluating those in order. + // Evaluate the list of cond expressions in order. + foreach ($condition_node->children as $condition_subnode) { + if ($condition_subnode instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext( + $context->withLineNumberStart($condition_subnode->lineno), + $condition_node, + $condition_subnode + ); + } + } + } finally { + \array_pop($this->parent_node_list); + } + if ($condition_subnode instanceof Node) { + // Analyze the cond expression for its side effects and the code it contains, + // not the effect of the condition. + // e.g. `while ($x = foo())` + $context = $this->analyzeAndGetUpdatedContext( + $context->withLineNumberStart($condition_subnode->lineno), + $node, + $condition_subnode + ); + if (!$this->context->isInGlobalScope() && !$this->context->isInLoop()) { + $condition_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $condition_subnode); + $always_iterates_at_least_once = !$condition_type->containsFalsey() && !$condition_type->isEmpty(); + } else { + $always_iterates_at_least_once = UnionTypeVisitor::checkCondUnconditionalTruthiness($condition_subnode); + } + } else { + $always_iterates_at_least_once = (bool)$condition_subnode; + } + } else { + $always_iterates_at_least_once = true; + } + $original_context = $context; + + $stmts_node = $node->children['stmts']; + if (!($stmts_node instanceof Node)) { + throw new AssertionError('Expected php-ast to always return a statement list for ast\AST_FOR'); + } + // Look to see if any proofs we do within the condition of the for + // can say anything about types within the statement + // list. + // TODO: Distinguish between inner and outer context. + // E.g. `for (; $x = cond(); ) {}` will have truthy $x within the loop + // but falsey outside the loop, if there are no breaks. + if ($condition_node instanceof Node) { + $context = (new LoopConditionVisitor( + $this->code_base, + $context, + $condition_node, + false, + BlockExitStatusChecker::willUnconditionallyProceed($stmts_node) + ))->__invoke($condition_node); + } elseif (Config::getValue('redundant_condition_detection')) { + $condition_node = $condition_node ?? new Node( + ast\AST_CONST, + 0, + ['name' => new Node(ast\AST_NAME, ast\flags\NAME_NOT_FQ, ['name' => 'true'], $node->lineno)], + $node->lineno + ); + (new LoopConditionVisitor( + $this->code_base, + $context, + $condition_node, + false, + BlockExitStatusChecker::willUnconditionallyProceed($stmts_node) + ))->checkRedundantOrImpossibleTruthyCondition($condition_node, $context, null, false); + } + + // Give plugins a chance to analyze the loop condition now + ConfigPluginSet::instance()->analyzeLoopBeforeBody( + $this->code_base, + $context, + $node + ); + + $context = $this->analyzeAndGetUpdatedContext( + $context->withScope( + new BranchScope($context->getScope()) + )->withLineNumberStart($stmts_node->lineno), + $node, + $stmts_node + ); + // Analyze the loop after analyzing the statements, in case it uses variables defined within the statements. + $loop_node = $node->children['loop']; + if ($loop_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext( + $context->withLineNumberStart($loop_node->lineno), + $node, + $loop_node + ); + } + + if (isset($node->phan_loop_contexts)) { + // Combine contexts from continue/break statements within this for loop + $context = (new ContextMergeVisitor($context, \array_merge([$context], $node->phan_loop_contexts)))->combineChildContextList(); + unset($node->phan_loop_contexts); + } + + $context = $context->withExitLoop($node); + if (!$always_iterates_at_least_once) { + $context = (new ContextMergeVisitor($context, [$context, $original_context]))->combineChildContextList(); + } + + // Check if this is side effect free with the variable types inferred by analyzing the loop body (heuristic). + if (Config::getValue('unused_variable_detection') && + InferPureSnippetVisitor::isSideEffectFreeSnippet($this->code_base, $context, $loop_node) && + InferPureSnippetVisitor::isSideEffectFreeSnippet($this->code_base, $context, $condition_node) && + InferPureSnippetVisitor::isSideEffectFreeSnippet($this->code_base, $context, $stmts_node)) { + VariableTrackerVisitor::recordHasLoopBodyWithoutSideEffects($node); + } + + // Now that we know all about our context (like what + // 'self' means), we can analyze statements like + // assignments and method calls. + + // When coming out of a scoped element, we pop the + // context to be the incoming context. Otherwise, + // we pass our new context up to our parent + return $this->postOrderAnalyze($context, $node); + } + + /** + * For "while loop" nodes, we analyze the components in the following order as a heuristic: + * (This is pretty much the same as analyzing a for loop with the 'init' and 'loop' nodes left blank) + * + * 1. propagate the context and scope from the parent, + * 2. Update the scope with the side effects (e.g. assignments) of the condition of the loop + * 3. Update the scope with the child statements both inside and outside the loop (ignoring branches which will continue/break), + * + * Then, Phan returns the context with the modified scope. + * + * TODO: merge the contexts together, for better analysis of possibly undefined variables + * + * NOTE: "Do while" loops are just handled by visit(), Phan sees and analyzes 'stmts' before 'cond'. + * + * + * │ + * ▼ + * ●──────● cond + * │ + * │ (TODO: merge contexts instead) + * ●──●──▶● + * stmts │ + * │ + * │ + * │ + * ▼ + * + * @param Node $node + * An AST node (for a while loop) we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * + * @suppress PhanUndeclaredProperty + */ + public function visitWhile(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + )->withEnterLoop($node); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + $condition_node = $node->children['cond']; + if ($condition_node instanceof Node) { + // Analyze the cond expression for its side effects and the code it contains, + // not the effect of the condition. + // e.g. `while ($x = foo())` + $context = $this->analyzeAndGetUpdatedContext( + $context->withLineNumberStart($condition_node->lineno), + $node, + $condition_node + ); + if (!$this->context->isInGlobalScope() && !$this->context->isInLoop()) { + $condition_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $condition_node); + $always_iterates_at_least_once = !$condition_type->containsFalsey() && !$condition_type->isEmpty(); + } else { + $always_iterates_at_least_once = UnionTypeVisitor::checkCondUnconditionalTruthiness($condition_node); + } + } else { + $always_iterates_at_least_once = (bool)$condition_node; + } + $original_context = $context; + + $stmts_node = $node->children['stmts']; + if (!$stmts_node instanceof Node) { + throw new AssertionError('Expected php-ast to always return an ast\AST_STMT_LIST for a while loop\'s statement list'); + } + + // Look to see if any proofs we do within the condition of the while + // can say anything about types within the statement + // list. + // TODO: Distinguish between inner and outer context. + // E.g. `while ($x = cond()) {}` will have truthy $x within the loop + // but falsey outside the loop, if there are no breaks. + if ($condition_node instanceof Node) { + $context = (new LoopConditionVisitor( + $this->code_base, + $context, + $condition_node, + false, + BlockExitStatusChecker::willUnconditionallyProceed($stmts_node) + ))->__invoke($condition_node); + } elseif (Config::getValue('redundant_condition_detection')) { + (new LoopConditionVisitor( + $this->code_base, + $context, + $condition_node, + false, + BlockExitStatusChecker::willUnconditionallyProceed($stmts_node) + ))->checkRedundantOrImpossibleTruthyCondition($condition_node, $context, null, false); + } + + // Give plugins a chance to analyze the loop condition now + ConfigPluginSet::instance()->analyzeLoopBeforeBody( + $this->code_base, + $context, + $node + ); + + $context = $this->analyzeAndGetUpdatedContext( + $context->withScope( + new BranchScope($context->getScope()) + )->withLineNumberStart($stmts_node->lineno), + $node, + $stmts_node + ); + + if (isset($node->phan_loop_contexts)) { + // Combine contexts from continue/break statements within this while loop + $context = (new ContextMergeVisitor($context, \array_merge([$context], $node->phan_loop_contexts)))->combineChildContextList(); + unset($node->phan_loop_contexts); + } + + $context = $context->withExitLoop($node); + if (!$always_iterates_at_least_once) { + $context = (new ContextMergeVisitor($context, [$context, $original_context]))->combineChildContextList(); + } + + // Check if this is side effect free with the variable types inferred by analyzing the loop body (heuristic). + if (Config::getValue('unused_variable_detection') && + InferPureSnippetVisitor::isSideEffectFreeSnippet($this->code_base, $context, $node)) { + VariableTrackerVisitor::recordHasLoopBodyWithoutSideEffects($node); + } + + // Now that we know all about our context (like what + // 'self' means), we can analyze statements like + // assignments and method calls. + + // When coming out of a scoped element, we pop the + // context to be the incoming context. Otherwise, + // we pass our new context up to our parent + return $this->postOrderAnalyze($context, $node); + } + + /** + * For "foreach loop" nodes, we analyze the loop variables, + * then analyze the statements in a different scope (e.g. BranchScope when there may be 0 iterations) + * + * @param Node $node a node of type ast\AST_FOREACH + * @throws NodeException + * @suppress PhanUndeclaredProperty + */ + public function visitForeach(Node $node): Context + { + $code_base = $this->code_base; + $context = $this->context; + $context->setLineNumberStart($node->lineno); + + $expr_node = $node->children['expr']; + + $has_at_least_one_iteration = false; + $expression_union_type = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $expr_node + )->withStaticResolvedInContext($context); + + if ($expr_node instanceof Node) { + if ($expr_node->kind === ast\AST_ARRAY) { + // e.g. foreach ([1, 2] as $value) has at least one + $has_at_least_one_iteration = \count($expr_node->children) > 0; + } else { + // e.g. look up global constants and class constants. + // TODO: Handle non-empty-array, etc. + $union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $this->context, $expr_node); + $has_at_least_one_iteration = !$union_type->containsFalsey() && !$union_type->isEmpty() && + !$union_type->hasTypeMatchingCallback(static function (Type $type): bool { + return !$type instanceof ArrayType; + }); + } + } + + // Check the expression type to make sure it's + // something we can iterate over + $this->checkCanIterate($expression_union_type, $node); + + $expr_node = $node->children['expr']; + if ($expr_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext($context, $node, $expr_node); + } + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $code_base, + $context, + $node + ); + + // PreOrderAnalysisVisitor is not used, to avoid issues analyzing edge cases such as `foreach ($x->method() as $x)` + + // Analyze the context inside the loop. The keys/values would not get created in the outer scope if the iterable expression was empty. + if ($has_at_least_one_iteration) { + $context_inside_loop_start = $context; + } else { + $context_inside_loop_start = $context->withScope(new BranchScope($context->getScope())); + } + + // withEnterLoop and withExitLoop must get called on the same Scope, + // so that the deferred callbacks get called. + $context_inside_loop_start = $context_inside_loop_start->withEnterLoop($node); + + // Add types of the key and value expressions, + // and check for errors in the foreach expression + $inner_context = $this->analyzeForeachIteration($context_inside_loop_start, $expression_union_type, $node); + + $value_node = $node->children['value']; + if ($value_node instanceof Node) { + $inner_context = $this->analyzeAndGetUpdatedContext($inner_context, $node, $value_node); + } + $key_node = $node->children['key']; + if ($key_node instanceof Node) { + $inner_context = $this->analyzeAndGetUpdatedContext($inner_context, $node, $key_node); + } + + // Give plugins a chance to analyze the loop condition now + ConfigPluginSet::instance()->analyzeLoopBeforeBody( + $code_base, + $inner_context, + $node + ); + + $stmts_node = $node->children['stmts']; + if ($stmts_node instanceof Node) { + $inner_context = $this->analyzeAndGetUpdatedContext($inner_context, $node, $stmts_node); + } + + // TODO: Also warn about object types when iterating over that class should not have side effects + if (Config::getValue('unused_variable_detection') && + !$expression_union_type->isEmpty() && !$expression_union_type->hasPossiblyObjectTypes() && + InferPureSnippetVisitor::isSideEffectFreeSnippet($this->code_base, $inner_context, $stmts_node) && + self::isLoopVariableWithoutSideEffects($node->children['key']) && + self::isLoopVariableWithoutSideEffects($node->children['value']) + ) { + VariableTrackerVisitor::recordHasLoopBodyWithoutSideEffects($node); + } + + if ($has_at_least_one_iteration) { + $context = $inner_context; + $context_list = [$inner_context]; + if (isset($node->phan_loop_contexts)) { + $context_list = \array_merge($context_list, $node->phan_loop_contexts); + // Combine contexts from continue/break statements within this foreach loop + unset($node->phan_loop_contexts); + } + + if (\count($context_list) >= 2) { + $context = (new ContextMergeVisitor($context, $context_list))->combineChildContextList(); + } + // Perform deferred checks about the inside of the loop. + $context = $context->withExitLoop($node); + + // This is the context after performing at least one iteration of the foreach loop. + } else { + $inner_context_list = [$context_inside_loop_start, $inner_context]; + + if (isset($node->phan_loop_contexts)) { + $inner_context_list = \array_merge($inner_context_list, $node->phan_loop_contexts); + // Combine contexts from continue/break statements within this foreach loop + unset($node->phan_loop_contexts); + } + // Perform deferred checks about the inside of the loop. + // Here, this combines the states of the inner loop (but not the outer loop) to avoid some types of false positives + // such as undeclared variable warnings. (imperfect heuristic but works well for most uses) + $context_inside_loop_start = (new ContextMergeVisitor($context_inside_loop_start, $inner_context_list))->combineChildContextList(); + $context_inside_loop_start = $context_inside_loop_start->withExitLoop($node); + + // Combine the outer scope with the inner scope + $context_list = [$context, $context_inside_loop_start]; + $context = (new ContextMergeVisitor($context, $context_list))->combineChildContextList(); + } + + return $this->postOrderAnalyze($context, $node); + } + + /** + * @param Node|string|int|float|null $node + * + * Returns true if this is probably a loop variable without side effects + * (e.g. not a reference, not modifying properties, etc) + */ + private static function isLoopVariableWithoutSideEffects($node): bool + { + if (!$node instanceof Node) { + return true; + } + switch ($node->kind) { + case ast\AST_VAR: + return is_string($node->children['name']); + case ast\AST_ARRAY: + case ast\AST_ARRAY_ELEM: + foreach ($node->children as $child_node) { + if (!self::isLoopVariableWithoutSideEffects($child_node)) { + return false; + } + } + return true; + default: + return ParseVisitor::isConstExpr($node); + } + } + + /** + * @param UnionType $union_type the type of $node->children['expr'] + * @param Node $node a node of kind AST_FOREACH + */ + private function checkCanIterate(UnionType $union_type, Node $node): void + { + if ($union_type->isEmpty()) { + return; + } + if (!$union_type->hasPossiblyObjectTypes() && !$union_type->hasIterable()) { + $this->emitIssue( + Issue::TypeMismatchForeach, + $node->children['expr']->lineno ?? $node->lineno, + (string)$union_type + ); + return; + } + $has_object = false; + foreach ($union_type->getTypeSet() as $type) { + if (!$type->isObjectWithKnownFQSEN()) { + continue; + } + try { + if ($type->asExpandedTypes($this->code_base)->hasTraversable()) { + continue; + } + } catch (RecursionDepthException $_) { + } + $this->warnAboutNonTraversableType($node, $type); + $has_object = true; + } + if ($has_object) { + return; + } + if (self::isEmptyIterable($union_type)) { + RedundantCondition::emitInstance( + $node->children['expr'], + $this->code_base, + (clone($this->context))->withLineNumberStart($node->children['expr']->lineno ?? $node->lineno), + Issue::EmptyForeach, + [(string)$union_type], + Closure::fromCallable([self::class, 'isEmptyIterable']) + ); + } + if (self::isDefinitelyNotObject($union_type)) { + if (!($node->children['stmts']->children ?? null)) { + RedundantCondition::emitInstance( + $node->children['expr'], + $this->code_base, + (clone($this->context))->withLineNumberStart($node->children['expr']->lineno ?? $node->lineno), + Issue::EmptyForeachBody, + [(string)$union_type], + Closure::fromCallable([self::class, 'isDefinitelyNotObject']) + ); + } + } + } + + private static function isDefinitelyNotObject(UnionType $type): bool + { + $type_set = $type->getRealTypeSet(); + if (!$type_set) { + return false; + } + foreach ($type_set as $type) { + if ($type->isPossiblyObject()) { + return false; + } + } + return true; + } + + /** + * Returns true if there is at least one iterable type, + * no object types, and that iterable type is the empty array shape. + */ + public static function isEmptyIterable(UnionType $union_type): bool + { + $has_iterable_types = false; + foreach ($union_type->getRealTypeSet() as $type) { + if ($type->isPossiblyObject()) { + return false; + } + if (!$type->isIterable()) { + continue; + } + if ($type->isPossiblyTruthy()) { + // This has possibly non-empty iterable types. + // We only track emptiness of array shapes. + return false; + } + $has_iterable_types = true; + } + return $has_iterable_types; + } + + private function warnAboutNonTraversableType(Node $node, Type $type): void + { + $fqsen = FullyQualifiedClassName::fromType($type); + if (!$this->code_base->hasClassWithFQSEN($fqsen)) { + return; + } + if ($fqsen->__toString() === '\stdClass') { + // stdClass is the only non-Traversable that I'm aware of that's commonly traversed over. + return; + } + $class = $this->code_base->getClassByFQSEN($fqsen); + $status = $class->checkCanIterateFromContext( + $this->code_base, + $this->context + ); + switch ($status) { + case Clazz::CAN_ITERATE_STATUS_NO_ACCESSIBLE_PROPERTIES: + $issue = Issue::TypeNoAccessiblePropertiesForeach; + break; + case Clazz::CAN_ITERATE_STATUS_NO_PROPERTIES: + $issue = Issue::TypeNoPropertiesForeach; + break; + default: + $issue = Issue::TypeSuspiciousNonTraversableForeach; + break; + } + + $this->emitIssue( + $issue, + $node->children['expr']->lineno ?? $node->lineno, + $type + ); + } + + private function analyzeForeachIteration(Context $context, UnionType $expression_union_type, Node $node): Context + { + $code_base = $this->code_base; + $value_node = $node->children['value']; + if ($value_node instanceof Node) { + // should be a parse error when not a Node + if ($value_node->kind === ast\AST_ARRAY) { + if (Config::get_closest_minimum_target_php_version_id() < 70100) { + self::analyzeArrayAssignBackwardsCompatibility($code_base, $context, $value_node); + } + } + + $context = (new AssignmentVisitor( + $code_base, + $context, + $value_node, + $expression_union_type->iterableValueUnionType($code_base) + ))->__invoke($value_node); + } + + // If there's a key, make a variable out of that too + $key_node = $node->children['key']; + if ($key_node instanceof Node) { + if ($key_node->kind === ast\AST_ARRAY) { + $this->emitIssue( + Issue::InvalidNode, + $key_node->lineno, + "Can't use list() as a key element - aborting" + ); + } else { + // TODO: Support Traversable then return Key. + // If we see array or array and no other array types, we're reasonably sure the foreach key is an integer or a string, so set it. + // (Or if we see iterable + $context = (new AssignmentVisitor( + $code_base, + $context, + $key_node, + $expression_union_type->iterableKeyUnionType($code_base) + ))->__invoke($key_node); + } + } + + // Note that we're not creating a new scope, just + // adding variables to the existing scope + return $context; + } + + /** + * Analyze an expression such as `[$a] = $values` or `list('key' => $v) = $values` for backwards compatibility issues + */ + public static function analyzeArrayAssignBackwardsCompatibility(CodeBase $code_base, Context $context, Node $node): void + { + if ($node->flags !== ast\flags\ARRAY_SYNTAX_LIST) { + Issue::maybeEmit( + $code_base, + $context, + Issue::CompatibleShortArrayAssignPHP70, + $node->lineno + ); + } + foreach ($node->children as $array_elem) { + if (isset($array_elem->children['key'])) { + Issue::maybeEmit( + $code_base, + $context, + Issue::CompatibleKeyedArrayAssignPHP70, + $array_elem->lineno + ); + break; + } + } + } + + + /** + * For "do-while loop" nodes of kind ast\AST_DO_WHILE, we analyze the 'stmts', 'cond' in order. + * (right now, the statements are just analyzed without creating a BranchScope) + * + * @suppress PhanUndeclaredProperty + */ + public function visitDoWhile(Node $node): Context + { + $context = $this->context; + $context->setLineNumberStart($node->lineno); + $context = $context->withEnterLoop($node); + + + // Visit the given node populating the code base + // with anything we learn and get a new context + // indicating the state of the world within the + // given node + $context = (new PreOrderAnalysisVisitor( + $this->code_base, + $context + ))->visitDoWhile($node); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + // (copied from visit(), this ensures plugins and other code get called) + $stmts_node = $node->children['stmts']; + if ($stmts_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext($context, $node, $stmts_node); + } + $cond_node = $node->children['cond']; + if ($cond_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext($context, $node, $cond_node); + } + if (Config::getValue('redundant_condition_detection')) { + // Analyze - don't warn about `do...while(true)` or `do...while(false)` because they might be a way to `break;` out of a group of statements + (new LoopConditionVisitor( + $this->code_base, + $context, + $cond_node, + true, + !$stmts_node || BlockExitStatusChecker::willUnconditionallyProceed($stmts_node) + ))->checkRedundantOrImpossibleTruthyCondition($cond_node, $context, null, false); + } + + if (isset($node->phan_loop_contexts)) { + // Combine contexts from continue/break statements within this do-while loop + $context = (new ContextMergeVisitor($context, \array_merge([$context], $node->phan_loop_contexts)))->combineChildContextList(); + unset($node->phan_loop_contexts); + } + $context = $context->withExitLoop($node); + + // Check if this is side effect free with the variable types inferred by analyzing the loop body (heuristic). + if (Config::getValue('unused_variable_detection') && + InferPureSnippetVisitor::isSideEffectFreeSnippet($this->code_base, $context, $node)) { + VariableTrackerVisitor::recordHasLoopBodyWithoutSideEffects($node); + } + + return $this->postOrderAnalyze($context, $node); + } + + /** + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * + * NOTE: This should never get called. + */ + public function visitIfElem(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + // NOTE: This is different from other analysis visitors because analyzing 'cond' with `||` has side effects + // after supporting `BlockAnalysisVisitor->visitBinaryOp()` + // TODO: Calling analyzeAndGetUpdatedContext before preOrderAnalyze is a hack. + + // TODO: This is redundant and has worse knowledge of the specific types of blocks than ConditionVisitor does. + // TODO: Implement a hybrid BlockAnalysisVisitor+ConditionVisitor that will do a better job of inferences and reducing false positives? (and reduce the redundant work) + + // E.g. the below code would update the context of BlockAnalysisVisitor in BlockAnalysisVisitor->visitBinaryOp() + // + // if (!(is_string($x) || $x === null)) {} + // + // But we want to let BlockAnalysisVisitor modify the context for cases such as the below: + // + // $result = !($x instanceof User) || $x->meetsCondition() + $condition_node = $node->children['cond']; + if ($condition_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext( + $context->withLineNumberStart($condition_node->lineno), + $node, + $condition_node + ); + } elseif (Config::getValue('redundant_condition_detection')) { + (new ConditionVisitor($this->code_base, $context))->checkRedundantOrImpossibleTruthyCondition($condition_node, $context, null, false); + } + + $context = $this->preOrderAnalyze($context, $node); + + if ($stmts_node = $node->children['stmts']) { + if ($stmts_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext( + $context->withScope( + new BranchScope($context->getScope()) + )->withLineNumberStart($stmts_node->lineno), + $node, + $stmts_node + ); + } + } + + // Now that we know all about our context (like what + // 'self' means), we can analyze statements like + // assignments and method calls. + + // When coming out of a scoped element, we pop the + // context to be the incoming context. Otherwise, + // we pass our new context up to our parent + return $this->postOrderAnalyze($context, $node); + } + + /** + * For 'closed context' items (classes, methods, functions, + * closures), we analyze children in the parent context, but + * then return the parent context itself unmodified by the + * children. + * + * │ + * ▼ + * ┌──●────┐ + * │ │ + * ●──●──● │ + * ┌────┘ + * ● + * │ + * ▼ + * + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + */ + public function visitClosedContext(Node $node): Context + { + // Make a copy of the internal context so that we don't + // leak any changes within the closed context to the + // outer scope + $context = $this->context; + $context->setLineNumberStart($node->lineno); + $context = $this->preOrderAnalyze(clone($context), $node); + + // We collect all child context so that the + // PostOrderAnalysisVisitor can optionally operate on + // them + $child_context_list = []; + + $child_context = $context; + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + foreach ($node->children as $child_node) { + // Skip any non Node children. + if (!($child_node instanceof Node)) { + continue; + } + + // Step into each child node and get an + // updated context for the node + $child_context = $this->analyzeAndGetUpdatedContext($child_context, $node, $child_node); + + $child_context_list[] = $child_context; + } + + // For if statements, we need to merge the contexts + // of all child context into a single scope based + // on any possible branching structure + $context = (new ContextMergeVisitor( + $context, + $child_context_list + ))->__invoke($node); + + $this->postOrderAnalyze($context, $node); + + // Return the initial context as we exit + return $this->context; + } + + /** + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * @suppress PhanAccessMethodInternal + */ + public function visitSwitchList(Node $node): Context + { + // Make a copy of the internal context so that we don't + // leak any changes within the closed context to the + // outer scope + $context = $this->context; + $context->setLineNumberStart($node->lineno); + $context = $this->preOrderAnalyze(clone($context), $node); + + $child_context_list = []; + + // TODO: Improve inferences in switch statements? + // TODO: Behave differently if switch lists don't cover every case (e.g. if there is no default) + $has_default = false; + // parent_node_list should always end in AST_SWITCH + // @phan-suppress-next-next-line PhanPossiblyUndeclaredProperty + [$switch_variable_node, $switch_variable_condition, $switch_variable_negated_condition] = $this->createSwitchConditionAnalyzer( + end($this->parent_node_list)->children['cond'] + ); + if (($switch_variable_condition || $switch_variable_negated_condition) && $switch_variable_node instanceof Node) { + $switch_variable_cond_variable_set = RedundantCondition::getVariableSet($switch_variable_node); + } else { + $switch_variable_cond_variable_set = []; + } + $children = $node->children; + if (\count($children) <= 1 && !isset($children[0]->children['cond'])) { + $this->emitIssue( + Issue::NoopSwitchCases, + end($this->parent_node_list)->lineno ?? $node->lineno + ); + } + $fallthrough_context = $context; + + $previous_child_context = null; + foreach ($node->children as $i => $child_node) { + if (!$child_node instanceof Node) { + throw new AssertionError("Switch case statement must be a node"); + } + ['cond' => $case_cond_node, 'stmts' => $case_stmts_node] = $child_node->children; + // Step into each child node and get an + // updated context for the node + + try { + $this->parent_node_list[] = $node; + $fallthrough_context->withLineNumberStart($child_node->lineno); + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $fallthrough_context, + $child_node + ); + // Statements such as `case $x = 2;` should affect both the body of that case statements and following case statements. + // Modify the $fallthrough_context. + if ($case_cond_node instanceof Node) { + $fallthrough_context = $this->analyzeAndGetUpdatedContext($fallthrough_context, $child_node, $case_cond_node); + } + if ($previous_child_context instanceof Context) { + // The previous case statement fell through some of the time or all of the time. + $child_context = (new ContextMergeVisitor( + $previous_child_context, + [$previous_child_context, $fallthrough_context] + ))->combineScopeList([$previous_child_context->getScope(), $fallthrough_context->getScope()]); + } else { + // The previous case statement did not fall through, or does not exist. + $child_context = $fallthrough_context->withScope(clone($fallthrough_context->getScope())); + } + if ($case_cond_node !== null) { + if ($switch_variable_condition) { + // e.g. make sure to handle $x from `switch (true) { case $x instanceof stdClass: }` or `switch ($x)` + // Note that this won't properly combine types from `case $x = expr: case $x = expr2:` (latter would override former), + // but I don't expect to see that in reasonable code. + $variables_to_check = $switch_variable_cond_variable_set + RedundantCondition::getVariableSet($case_cond_node); + // Add the variable type from the above case statements, if it was possible for it to fall through + // TODO: Also support switch(get_class($variable)) + $child_context = $switch_variable_condition($child_context, $case_cond_node); + foreach ($variables_to_check as $var_name) { + if ($previous_child_context !== null) { + $variable = $child_context->getScope()->getVariableByNameOrNull($var_name); + if ($variable) { + $old_variable = $previous_child_context->getScope()->getVariableByNameOrNull($var_name); + + if ($old_variable) { + $variable = clone($variable); + $variable->setUnionType($variable->getUnionType()->withUnionType($old_variable->getUnionType())); + $child_context->addScopeVariable($variable); + } + } + } + } + } + if ($switch_variable_negated_condition) { + // e.g. make sure to handle $x from `switch (true) { case $x instanceof stdClass: }` or `switch ($x)` + // Note that this won't properly combine types from `case $x = expr: case $x = expr2:` (latter would override former), + // but I don't expect to see that in reasonable code. + $variables_to_check = $switch_variable_cond_variable_set + RedundantCondition::getVariableSet($case_cond_node); + foreach ($variables_to_check as $var_name) { + // Add the variable type that were ruled out by the above case statements, if it was possible for it to fall through + // TODO: Also support switch(get_class($variable)) + $fallthrough_context = $switch_variable_negated_condition($fallthrough_context, $case_cond_node); + } + } + } else { + foreach ($switch_variable_cond_variable_set as $var_name) { + // Add the variable types from the default to the + // TODO: Handle the default not being the last case statement + // TODO: Improve handling of possibly undefined variables + $variable = $child_context->getScope()->getVariableByNameOrNull($var_name); + if (!$variable) { + continue; + } + if ($previous_child_context) { + $old_variable = $previous_child_context->getScope()->getVariableByNameOrNull($var_name); + + if ($old_variable) { + $variable = clone($variable); + $variable->setUnionType($variable->getUnionType()->withUnionType($old_variable->getUnionType())); + $child_context->addScopeVariable($variable); + } + } + } + } + + if ($case_stmts_node instanceof Node) { + $child_context = $this->analyzeAndGetUpdatedContext($child_context, $child_node, $case_stmts_node); + } + ConfigPluginSet::instance()->postAnalyzeNode( + $this->code_base, + $fallthrough_context, + $child_node + ); + } finally { + \array_pop($this->parent_node_list); + } + + + if ($case_cond_node === null) { + $has_default = true; + } + // We can improve analysis of `case` blocks by using + // a BlockExitStatusChecker to avoid propagating invalid inferences. + $stmts_node = $child_node->children['stmts']; + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null + $block_exit_status = (new BlockExitStatusChecker())->__invoke($stmts_node); + // equivalent to !willUnconditionallyThrowOrReturn() + $previous_child_context = null; + if (($block_exit_status & ~BlockExitStatusChecker::STATUS_THROW_OR_RETURN_BITMASK)) { + // Skip over case statements that only ever throw or return + if (count($stmts_node->children ?? []) !== 0 || $i === count($node->children) - 1) { + // and skip over empty statement lists, unless they're the last in a long line of empty statement lists + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable the finally block is not perfectly analyzed by Phan + $child_context_list[] = $child_context; + } + + if ($block_exit_status & BlockExitStatusChecker::STATUS_PROCEED) { + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable the finally block is not perfectly analyzed by Phan + $previous_child_context = $child_context; + } + } + } + + if (count($child_context_list) > 0) { + if (!$has_default) { + $child_context_list[] = $fallthrough_context; + } + if (count($child_context_list) >= 2) { + // For case statements, we need to merge the contexts + // of all child context into a single scope based + // on any possible branching structure + $context = (new ContextMergeVisitor( + $context, + $child_context_list + ))->combineChildContextList(); + } else { + $context = $child_context_list[0]; + } + } + + // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Phan cannot infer child_context_list was non-empty due to finally block + return $this->postOrderAnalyze($context, $node); + } + + private const NOOP_SWITCH_COND_ANALYZER = [null, null, null]; + + /** + * @param Node|int|string|float $switch_case_node + * @return array{0:?Node, 1:?Closure(Context, mixed): Context, 2:?Closure(Context, mixed): Context} + */ + private function createSwitchConditionAnalyzer($switch_case_node): array + { + $switch_kind = ($switch_case_node->kind ?? null); + try { + if ($switch_kind === ast\AST_VAR) { + $switch_variable = (new ConditionVisitor($this->code_base, $this->context))->getVariableFromScope($switch_case_node, $this->context); + if (!$switch_variable) { + return self::NOOP_SWITCH_COND_ANALYZER; + } + return [ + $switch_case_node, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($switch_case_node): Context { + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->updateVariableToBeEqual($switch_case_node, $cond_node, $child_context); + }, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($switch_case_node): Context { + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->updateVariableToBeNotEqual($switch_case_node, $cond_node, $child_context); + }, + ]; + } elseif ($switch_kind === ast\AST_CALL) { + $name = $switch_case_node->children['expr']->children['name'] ?? null; + if (\is_string($name)) { + $name = \strtolower($name); + if ($name === 'get_class') { + $switch_variable_node = $switch_case_node->children['args']->children[0] ?? null; + if (!$switch_variable_node instanceof Node) { + return self::NOOP_SWITCH_COND_ANALYZER; + } + if ($switch_variable_node->kind !== ast\AST_VAR) { + return self::NOOP_SWITCH_COND_ANALYZER; + } + $switch_variable = (new ConditionVisitor($this->code_base, $this->context))->getVariableFromScope($switch_variable_node, $this->context); + if (!$switch_variable) { + return self::NOOP_SWITCH_COND_ANALYZER; + } + return [ + $switch_variable_node, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($switch_variable_node): Context { + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->analyzeClassAssertion( + $switch_variable_node, + $cond_node + ) ?? $child_context; + }, + null, + ]; + } + } + } + + if (!ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $switch_case_node)) { + // e.g. switch(true), switch(MY_CONST), switch(['x']) + return [ + $switch_case_node, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($switch_case_node): Context { + // Handle match(cond) { $x = constexpr => ... }. The assignment was already analyzed. + while ($cond_node instanceof Node && \in_array($cond_node->kind, [ast\AST_ASSIGN, ast\AST_ASSIGN_REF, ast\AST_ASSIGN_OP], true)) { + $cond_node = $cond_node->children['var']; + } + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->analyzeAndUpdateToBeEqual($switch_case_node, $cond_node); + }, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($switch_case_node): Context { + // Handle match(cond) { $x = constexpr => ... }. The assignment was already analyzed. + while ($cond_node instanceof Node && \in_array($cond_node->kind, [ast\AST_ASSIGN, ast\AST_ASSIGN_REF, ast\AST_ASSIGN_OP], true)) { + $cond_node = $cond_node->children['var']; + } + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->analyzeAndUpdateToBeNotEqual($switch_case_node, $cond_node); + }, + ]; + } + } catch (IssueException $_) { + // do nothing, we warn elsewhere + } + return self::NOOP_SWITCH_COND_ANALYZER; + } + + /** + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * + * Based on visitSwitchList + * @suppress PhanAccessMethodInternal + */ + public function visitMatchArmList(Node $node): Context + { + // Make a copy of the internal context so that we don't + // leak any changes within the closed context to the + // outer scope + $context = $this->context; + $context->setLineNumberStart($node->lineno); + $context = $this->preOrderAnalyze(clone($context), $node); + + $child_context_list = []; + + // parent_node_list should always end in kind ast\AST_MATCH + $match_expression_node = end($this->parent_node_list); + if (!$match_expression_node instanceof Node) { + throw new AssertionError('Expected AST_MATCH node as parent of AST_MATCH_ARM_LIST'); + } + $children = $node->children; + if (\count($children) <= 1 && !isset($children[0]->children['cond'])) { + // Warn about match expressions with only the default condition. + $this->emitIssue( + Issue::NoopMatchArms, + $match_expression_node->lineno, + ASTReverter::toShortString($match_expression_node) + ); + } + // Create closures to infer the effects of checking the match expression against the match condition lists (assertions and their negations) + $cond_node = $match_expression_node->children['cond']; + [$unused_match_variable_node, $match_variable_condition, $match_variable_negated_condition] = $this->createMatchConditionAnalyzer( + $cond_node + ); + if (Config::getValue('redundant_condition_detection')) { + $cond_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $match_expression_node->children['cond']); + if ($cond_type->hasRealTypeSet()) { + $cond_type = $cond_type->getRealUnionType()->withStaticResolvedInContext($context); + } else { + $cond_type = null; + } + } else { + $cond_type = null; + } + /* + if (($match_variable_condition || $match_variable_negated_condition) && $match_variable_node instanceof Node) { + $match_variable_cond_variable_set = RedundantCondition::getVariableSet($match_variable_node); + } else { + $match_variable_cond_variable_set = []; + } + */ + $fallthrough_context = $context; + + $default_arm_node = null; + + foreach ($node->children as $arm_node) { + if (!$arm_node instanceof Node) { + throw new AssertionError("Match arm must be a node"); + } + + if ($arm_node->children['cond'] === null) { + if ($default_arm_node !== null) { + $this->emitIssue( + Issue::InvalidNode, + $arm_node->lineno, + 'Saw an invalid match arm with multiple default nodes' + ); + // fall through + } else { + // TODO: Warn about duplicate default nodes + $default_arm_node = $arm_node; + continue; + } + } + // Step into each child node and get an + // updated context for the node + + $child_context = $fallthrough_context->withScope(clone($fallthrough_context->getScope())); + + $child_context->withLineNumberStart($arm_node->lineno); + try { + $this->parent_node_list[] = $node; + [$child_context, $fallthrough_context] = $this->analyzeMatchArm( + $child_context, + $fallthrough_context, + $match_variable_condition, + $match_variable_negated_condition, + $arm_node, + $cond_node, + $cond_type + ); + } finally { + \array_pop($this->parent_node_list); + } + + // We can improve analysis of arms by using + // a BlockExitStatusChecker to avoid propagating invalid inferences. + if (self::willExecutionProceedAfterMatchArm($arm_node)) { + $child_context_list[] = $child_context; + } + } + + if ($default_arm_node instanceof Node) { + // Duplicates above code + // TODO: Check if the default is unreachable + + // Step into each child node and get an + // updated context for the node + + $child_context = $fallthrough_context->withScope(clone($fallthrough_context->getScope())); + + $child_context->withLineNumberStart($default_arm_node->lineno); + try { + $this->parent_node_list[] = $node; + [$child_context, $unused_fallthrough_context] = $this->analyzeMatchArm( + $child_context, + $fallthrough_context, + $match_variable_condition, + $match_variable_negated_condition, + $default_arm_node, + $cond_node, + $cond_type + ); + } finally { + \array_pop($this->parent_node_list); + } + + // We can improve analysis of arms by using + // a BlockExitStatusChecker to avoid propagating invalid inferences. + if (self::willExecutionProceedAfterMatchArm($default_arm_node)) { + $child_context_list[] = $child_context; + } + } + + // Match will throw an UnhandledMatchError if none of the arms apply. + if (count($child_context_list) > 0) { + if (count($child_context_list) >= 2) { + // For case statements, we need to merge the contexts + // of all child context into a single scope based + // on any possible branching structure + $context = (new ContextMergeVisitor( + $context, + $child_context_list + ))->combineChildContextList(); + } else { + $context = $child_context_list[0]; + } + } + + return $this->postOrderAnalyze($context, $node); + } + + private static function willExecutionProceedAfterMatchArm(Node $arm_node): bool + { + $expr_node = $arm_node->children['expr']; + if (!$expr_node instanceof Node) { + return true; + } + $block_exit_status = (new BlockExitStatusChecker())->__invoke($expr_node); + // Skip over arms that only ever throw/exit. + // equivalent to !willUnconditionallyThrowOrReturn() + return ($block_exit_status & ~BlockExitStatusChecker::STATUS_THROW_OR_RETURN_BITMASK) !== 0; + } + + /** + * @param ?Closure(Context, mixed): Context $match_variable_condition + * @param ?Closure(Context, mixed): Context $match_variable_negated_condition + * @param Node|int|string|float $match_cond_node + * @param ?UnionType $cond_type for impossible condition detection + * @return array{0: Context, 1: Context} new values of [$child_context, $fallthrough_context] + */ + private function analyzeMatchArm( + Context $child_context, + Context $fallthrough_context, + ?Closure $match_variable_condition, + ?Closure $match_variable_negated_condition, + Node $arm_node, + $match_cond_node, + ?UnionType $cond_type + ): array { + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $fallthrough_context, + $arm_node + ); + ['expr' => $arm_expr_node, 'cond' => $arm_cond_node] = $arm_node->children; + if ($arm_cond_node !== null) { + if ($arm_cond_node instanceof Node) { + $child_context = $this->analyzeAndGetUpdatedContext($child_context, $arm_node, $arm_cond_node); + } + if ($cond_type) { + (new RedundantConditionVisitor($this->code_base, $child_context))->checkImpossibleMatchArm($match_cond_node, $cond_type, $arm_node); + } + if ($match_variable_condition) { + // e.g. make sure to handle $x from `match (true) { $x instanceof stdClass => ... }` or `match ($x)` + // Note that this won't properly combine types from `($x = expr) => ... , ($x = expr2) => ...` (latter would override former), + // but I don't expect to see that in reasonable code. + //$variables_to_check = $match_variable_cond_variable_set + RedundantCondition::getVariableSet($arm_cond_node); + + // Add the variable type from the **conditions of the** above arms, + // if it was possible for it to fall through + // TODO: Also support match(get_class($variable)) + $child_context = $match_variable_condition($child_context, $arm_cond_node); + } + if ($match_variable_negated_condition) { + // Add the variable types that were ruled out by the above case statements, if it was possible for it to fall through. + // TODO: Also support match(get_class($variable)) + $fallthrough_context = $match_variable_negated_condition($fallthrough_context, $arm_cond_node); + } + } + + if ($arm_expr_node instanceof Node) { + $child_context = $this->analyzeAndGetUpdatedContext($child_context, $arm_node, $arm_expr_node); + } + ConfigPluginSet::instance()->postAnalyzeNode( + $this->code_base, + $fallthrough_context, + $arm_node + ); + return [$child_context, $fallthrough_context]; + } + + /** + * @param Node $node + * An AST node we'd like to analyze the statements + * + * @return Context + * The updated context after visiting the node + * + * XXX this is complicated because we need to know for each `if`/`elseif` clause + * + * - What the side effects of executing the expression are on the chain (e.g. variable assignments, assignments by references) + * - What the context would be if that expression were truthy (ConditionVisitor) + * - What the context would be if that expression were falsey (NegatedConditionVisitor) + * + * The code in visitIfElem had to be inlined in order to properly modify the associated contexts. + * @suppress PhanSuspiciousTruthyCondition, PhanSuspiciousTruthyString the value Phan infers for conditions can be array literals. This seems to be handled properly. + */ + public function visitIf(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + $context = $this->preOrderAnalyze($context, $node); + + // We collect all child context so that the + // PostOrderAnalysisVisitor can optionally operate on + // them + $child_context_list = []; + + $scope = $context->getScope(); + if ($scope instanceof GlobalScope) { + $fallthrough_context = $context->withScope(new BranchScope($scope)); + } else { + $fallthrough_context = $context; + } + + $child_nodes = $node->children; + $excluded_elem_count = 0; + + $first_unconditionally_true_index = null; + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + foreach ($child_nodes as $child_node) { + // The conditions need to communicate to the outer + // scope for things like assigning variables. + // $child_context = $fallthrough_context->withClonedScope(); + + '@phan-var Node $child_node'; + $fallthrough_context->setLineNumberStart($child_node->lineno); + + $old_context = $this->context; + $this->context = $fallthrough_context; + $this->parent_node_list[] = $node; + + try { + // NOTE: This is different from other analysis visitors because analyzing 'cond' with `||` has side effects + // after supporting `BlockAnalysisVisitor->visitBinaryOp()` + // TODO: Calling analyzeAndGetUpdatedContext before preOrderAnalyze is a hack. + + // TODO: This is redundant and has worse knowledge of the specific types of blocks than ConditionVisitor does. + // TODO: Implement a hybrid BlockAnalysisVisitor+ConditionVisitor that will do a better job of inferences and reducing false positives? (and reduce the redundant work) + + // E.g. the below code would update the context of BlockAnalysisVisitor in BlockAnalysisVisitor->visitBinaryOp() + // + // if (!(is_string($x) || $x === null)) {} + // + // But we want to let BlockAnalysisVisitor modify the context for cases such as the below: + // + // $result = !($x instanceof User) || $x->meetsCondition() + [$child_context, $fallthrough_context] = $this->preAnalyzeIfElemCondition($child_node, $fallthrough_context); + $condition_node = $child_node->children['cond']; + + $stmts_node = $child_node->children['stmts']; + if (!$stmts_node instanceof Node) { + throw new AssertionError('Did not expect null/empty statements list node'); + } + + $this->parent_node_list[] = $child_node; + $old_context = $this->context; + $this->context = $child_context->withScope( + new BranchScope($child_context->getScope()) + )->withLineNumberStart($stmts_node->lineno); + try { + $child_context = $this->visitStmtList($stmts_node); + } finally { + \array_pop($this->parent_node_list); + $this->context = $old_context; + } + + // Now that we know all about our context (like what + // 'self' means), we can analyze statements like + // assignments and method calls. + $child_context = $this->postOrderAnalyze($child_context, $child_node); + + // Issue #406: We can improve analysis of `if` blocks by using + // a BlockExitStatusChecker to avoid propagating invalid inferences. + // TODO: we may wish to check for a try block between this line's scope + // and the parent function's (or global) scope, + // to reduce false positives. + // (Variables will be available in `catch` and `finally`) + // This is mitigated by finally and catch blocks being unaware of new variables from try{} blocks. + + // inferred_value is either: + // 1. truthy non-Node if the value could be inferred + // 2. falsy non-Node if the value could be inferred + // 3. A Node if the value could not be inferred (most conditionals) + if ($condition_node instanceof Node) { + $inferred_cond_value = (new ContextNode($this->code_base, $fallthrough_context, $condition_node))->getEquivalentPHPValueForControlFlowAnalysis(); + } else { + // Treat `else` as equivalent to `elseif (true)` + $inferred_cond_value = $condition_node ?? true; + } + if (!$inferred_cond_value) { + // Don't merge this scope into the outer scope + // e.g. "if (false) { anything }" + $excluded_elem_count++; + } elseif (BlockExitStatusChecker::willUnconditionallySkipRemainingStatements($stmts_node)) { + // e.g. "if (!is_string($x)) { return; }" or break + $excluded_elem_count++; + if (!BlockExitStatusChecker::willUnconditionallyThrowOrReturn($stmts_node)) { + $this->recordLoopContextForBreakOrContinue($child_context); + } + } else { + $child_context_list[] = $child_context; + } + + if ($condition_node instanceof Node) { + // fwrite(STDERR, "Checking if unconditionally true: " . \Phan\Debug::nodeToString($condition_node) . "\n"); + // TODO: Could add a check for conditions that are unconditionally falsey and warn + if (!$inferred_cond_value instanceof Node && $inferred_cond_value) { + // TODO: Could warn if this is not a condition on a static variable + $first_unconditionally_true_index = $first_unconditionally_true_index ?? \count($child_context_list); + } + $fallthrough_context = (new NegatedConditionVisitor($this->code_base, $fallthrough_context))->__invoke($condition_node); + } elseif ($condition_node) { + $first_unconditionally_true_index = $first_unconditionally_true_index ?? \count($child_context_list); + } + // If cond_node was null, it would be an else statement. + } finally { + $this->context = $old_context; + \array_pop($this->parent_node_list); + } + } + // fprintf(STDERR, "First unconditionally true index is %s: %s\n", $first_unconditionally_true_index ?? 'null', \Phan\Debug::nodeToString($node)); + + if ($excluded_elem_count === count($child_nodes)) { + // If all of the AST_IF_ELEM bodies would unconditionally throw or return, + // then analyze the remaining statements with the negation of all of the conditions. + $context = $fallthrough_context; + } elseif ($first_unconditionally_true_index > 0) { + // If we have at least one child context that falls through, then use that one. + $context = (new ContextMergeVisitor( + $fallthrough_context, // e.g. "if (!is_string($x)) { $x = ''; }" should result in inferring $x is a string. + \array_slice($child_context_list, 0, $first_unconditionally_true_index) + ))->mergePossiblySingularChildContextList(); + } else { + // For if statements, we need to merge the contexts + // of all child context into a single scope based + // on any possible branching structure + + // ContextMergeVisitor will include the incoming scope($context) if the if elements aren't comprehensive + $context = (new ContextMergeVisitor( + $fallthrough_context, // e.g. "if (!is_string($x)) { $x = ''; }" should result in inferring $x is a string. + $child_context_list + ))->visitIf($node); + } + + + // When coming out of a scoped element, we pop the + // context to be the incoming context. Otherwise, + // we pass our new context up to our parent + return $this->postOrderAnalyze($context, $node); + } + + /** + * Returns contexts in which the condition was true and which the condition was false. + * + * This has special cases for handling `if (A && (B = C)) {}` + * + * @param Node $if_elem_node a node of kind ast\AST_IF_ELEM + * @return array{0:Context,1:Context} [$child_context, new value of $fallthrough_context] + */ + private function preAnalyzeIfElemCondition(Node $if_elem_node, Context $fallthrough_context): array + { + $condition_node = $if_elem_node->children['cond']; + if ($condition_node instanceof Node) { + if ($condition_node->kind === ast\AST_BINARY_OP) { + // TODO: Support BINARY_BOOL_OR for fallthrough_context. + // This will be inconvenient with needing to check for initially false/initially true condition nodes in loops. + if ($condition_node->flags === ast\flags\BINARY_BOOL_AND) { + $child_context = $this->analyzeAndGetUpdatedContextAndAssertTruthy( + $fallthrough_context, + $if_elem_node, + $condition_node + ); + $fallthrough_context = $this->analyzeAndGetUpdatedContext( + $fallthrough_context->withLineNumberStart($condition_node->lineno), + $if_elem_node, + $condition_node + ); + return [$child_context, $fallthrough_context]; + } + } + $fallthrough_context = $this->analyzeAndGetUpdatedContext( + $fallthrough_context->withLineNumberStart($condition_node->lineno), + $if_elem_node, + $condition_node + ); + } elseif (Config::getValue('redundant_condition_detection')) { + (new ConditionVisitor($this->code_base, $fallthrough_context))->checkRedundantOrImpossibleTruthyCondition($condition_node, $fallthrough_context, null, false); + } + + $child_context = $fallthrough_context->withClonedScope(); + $child_context = $this->preOrderAnalyze($child_context, $if_elem_node); + + return [$child_context, $fallthrough_context]; + } + + /** + * @param Node|string|int|float $condition_node + */ + private function analyzeAndGetUpdatedContextAndAssertTruthy( + Context $context, + Node $parent_node, + $condition_node + ): Context { + if (!$condition_node instanceof Node) { + return $context; + } + if ($condition_node->kind === ast\AST_BINARY_OP) { + if ($condition_node->flags === ast\flags\BINARY_BOOL_AND) { + return $this->analyzeAndGetUpdatedContextAndAssertTruthy( + $this->analyzeAndGetUpdatedContextAndAssertTruthy( + $context, + $condition_node, + $condition_node->children['left'] + ), + $condition_node, + $condition_node->children['right'] + ); + } + } + // Let any configured plugins do a pre-order + // analysis of the node. + // This may run multiple times on the same node due to need to analyze conditions and their negation. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $condition_node + ); + // Infer the side effects of a generic node kind + $context = $this->analyzeAndGetUpdatedContext( + $context->withLineNumberStart($condition_node->lineno), + $parent_node, + $condition_node + ); + // Assert the generic node kind is truthy + return (new ConditionVisitor( + $this->code_base, + $context + ))->__invoke($condition_node); + } + + /** + * Returns a closure to analyze the conditions for match expressions for a match arm + * + * @param Node|int|string|float $match_case_node + * @return array{0:?Node, 1:?Closure(Context, mixed): Context, 2:?Closure(Context, mixed): Context} + * @see self::createSwitchConditionAnalyzer() - Based on that but uses strict equality instead + */ + private function createMatchConditionAnalyzer($match_case_node): array + { + $match_kind = ($match_case_node->kind ?? null); + try { + if ($match_kind === ast\AST_VAR) { + $match_variable = (new ConditionVisitor($this->code_base, $this->context))->getVariableFromScope($match_case_node, $this->context); + if (!$match_variable) { + return self::NOOP_SWITCH_COND_ANALYZER; + } + return [ + $match_case_node, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($match_case_node): Context { + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->updateVariableToBeIdentical($match_case_node, $cond_node, $child_context); + }, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($match_case_node): Context { + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->updateVariableToBeNotIdentical($match_case_node, $cond_node, $child_context); + }, + ]; + } elseif ($match_kind === ast\AST_CALL) { + $name = $match_case_node->children['expr']->children['name'] ?? null; + if (\is_string($name)) { + $name = \strtolower($name); + if ($name === 'get_class') { + $match_variable_node = $match_case_node->children['args']->children[0] ?? null; + if (!$match_variable_node instanceof Node) { + return self::NOOP_SWITCH_COND_ANALYZER; + } + if ($match_variable_node->kind !== ast\AST_VAR) { + return self::NOOP_SWITCH_COND_ANALYZER; + } + $match_variable = (new ConditionVisitor($this->code_base, $this->context))->getVariableFromScope($match_variable_node, $this->context); + if (!$match_variable) { + return self::NOOP_SWITCH_COND_ANALYZER; + } + return [ + $match_variable_node, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($match_variable_node): Context { + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->analyzeClassAssertion( + $match_variable_node, + $cond_node + ) ?? $child_context; + }, + null, + ]; + } + } + } + if (!ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $match_case_node)) { + // e.g. match(true), match(MY_CONST), match(['x']) + return [ + $match_case_node, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($match_case_node): Context { + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->analyzeAndUpdateToBeIdentical($match_case_node, $cond_node); + }, + /** + * @param Node|string|int|float $cond_node + */ + function (Context $child_context, $cond_node) use ($match_case_node): Context { + $visitor = new ConditionVisitor($this->code_base, $child_context); + return $visitor->analyzeAndUpdateToBeNotIdentical($match_case_node, $cond_node); + }, + ]; + } + } catch (IssueException $_) { + // do nothing, we warn elsewhere + } + return self::NOOP_SWITCH_COND_ANALYZER; + } + + /** + * Handle break/continue statements in conditionals within a loop. + * Record scope with the inferred variable types so it can be merged later outside of the loop. + * + * TODO: This is a heuristic that could be improved (differentiate break/continue, check if all branches are already handled, etc.) + * @suppress PhanUndeclaredProperty + */ + private function recordLoopContextForBreakOrContinue(Context $child_context): void + { + for ($i = \count($this->parent_node_list) - 1; $i >= 0; $i--) { + $node = $this->parent_node_list[$i]; + switch ($node->kind) { + // switch handles continue/break the same way as regular loops. (in PostOrderAnalysisVisitor::visitSwitch) + case ast\AST_SWITCH: + case ast\AST_FOR: + case ast\AST_WHILE: + case ast\AST_DO_WHILE: + case ast\AST_FOREACH: + if (isset($node->phan_loop_contexts)) { + $node->phan_loop_contexts[] = $child_context; + } else { + $node->phan_loop_contexts = [$child_context]; + } + break; + case ast\AST_FUNC_DECL: + case ast\AST_CLOSURE: + case ast\AST_ARROW_FUNC: + case ast\AST_METHOD: + case ast\AST_CLASS: + // We didn't find it. + return; + } + } + } + + /** + * TODO: Diagram similar to visit() for this and other visitors handling branches? + * + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + */ + public function visitTry(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + $context = $this->preOrderAnalyze($context, $node); + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + + $try_node = $node->children['try']; + + // The conditions need to communicate to the outer + // scope for things like assigning variables. + $try_context = $context->withScope( + new BranchScope($context->getScope()) + ); + + $try_context->withLineNumberStart( + $try_node->lineno + ); + + // Step into each try node and get an + // updated context for the node + $try_context = $this->analyzeAndGetUpdatedContext($try_context, $node, $try_node); + + // Analyze the catch blocks and finally blocks with a mix of the types + // from the try block and the catch blocks. + // There's still some ways this could be improved for combining contexts. + // (It's difficult to do this perfectly, especially since almost any expression in a try block + // may throw under some circumstances) + // + // NOTE: We let ContextMergeVisitor->visitTry decide if the block exit status is valid. + $context = (new ContextMergeVisitor( + $context, + [$try_context] + ))->mergeTryContext($node); + + // We collect all child context so that the + // PostOrderAnalysisVisitor can optionally operate on + // them + $catch_context_list = [$try_context]; + + $catch_nodes = $node->children['catches']->children ?? []; + + foreach ($catch_nodes as $catch_node) { + // Note: ContextMergeVisitor expects to get each individual catch + if (!$catch_node instanceof Node) { + throw new AssertionError("Expected catch_node to be a Node"); + } + + // The conditions need to communicate to the outer + // scope for things like assigning variables. + $catch_context = $context->withScope( + new BranchScope($context->getScope()) + ); + + $catch_context->withLineNumberStart( + $catch_node->lineno + ); + + // Step into each catch node and get an + // updated context for the node + $catch_context = $this->analyzeAndGetUpdatedContext($catch_context, $node, $catch_node); + // NOTE: We let ContextMergeVisitor->mergeCatchContext decide if the block exit status is valid. + $catch_context_list[] = $catch_context; + } + + $this->checkUnreachableCatch($catch_nodes, $context); + + // first context is the try. If there's a second context, it's a catch. + if (count($catch_context_list) >= 2) { + // For switch/try statements, we need to merge the contexts + // of all child context into a single scope based + // on any possible branching structure + $context = (new ContextMergeVisitor( + $context, + $catch_context_list + ))->mergeCatchContext($node); + } + + $finally_node = $node->children['finally'] ?? null; + if ($finally_node) { + if (!($finally_node instanceof Node)) { + throw new AssertionError("Expected non-null finally node to be a Node"); + } + // Because finally is always executed, we reuse $context + + // The conditions need to communicate to the outer + // scope for things like assigning variables. + $context = $context->withScope( + new BranchScope($context->getScope()) + ); + + $context->withLineNumberStart( + $finally_node->lineno + ); + + // Step into each finally node and get an + // updated context for the node. + // Don't bother checking if finally unconditionally returns here + // If it does, dead code detection would also warn. + $context = $this->analyzeAndGetUpdatedContext($context, $node, $finally_node); + } + + // When coming out of a scoped element, we pop the + // context to be the incoming context. Otherwise, + // we pass our new context up to our parent + return $this->postOrderAnalyze($context, $node); + } + + /** + * @param list $catch_nodes + * @param Context $context + */ + private function checkUnreachableCatch(array $catch_nodes, Context $context): void + { + if (count($catch_nodes) <= 1) { + return; + } + $caught_union_types = []; + $code_base = $this->code_base; + + foreach ($catch_nodes as $catch_node) { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall should be impossible to throw + $union_type = UnionTypeVisitor::unionTypeFromClassNode( + $code_base, + $context, + $catch_node->children['class'] + )->objectTypesWithKnownFQSENs(); + + $catch_line = $catch_node->lineno; + + foreach ($union_type->getTypeSet() as $type) { + foreach ($type->asExpandedTypes($code_base)->getTypeSet() as $ancestor_type) { + // Check if any of the ancestors were already caught by a previous catch statement + $line = $caught_union_types[\spl_object_id($ancestor_type)] ?? null; + + if ($line !== null) { + Issue::maybeEmit( + $code_base, + $context, + Issue::UnreachableCatch, + $catch_line, + (string)$type, + $line, + (string)$ancestor_type + ); + break; + } + } + } + foreach ($union_type->getTypeSet() as $type) { + // Track where this ancestor type was thrown + $caught_union_types[\spl_object_id($type)] = $catch_line; + } + } + } + + /** + * @param Node $node + * A node to parse + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function visitBinaryOp(Node $node): Context + { + $flags = $node->flags; + switch ($flags) { + case ast\flags\BINARY_BOOL_AND: + return $this->analyzeBinaryBoolAnd($node); + case ast\flags\BINARY_BOOL_OR: + return $this->analyzeBinaryBoolOr($node); + case ast\flags\BINARY_COALESCE: + return $this->analyzeBinaryCoalesce($node); + } + return $this->visit($node); + } + + /** + * @param Node $node + * A node to parse (for `&&` or `and` operator) + * + * @return Context + * A new context resulting from analyzing this logical `&&` operator. + */ + public function analyzeBinaryBoolAnd(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + $left_node = $node->children['left']; + $right_node = $node->children['right']; + + // With (left) && (right) + // 1. Update context with any side effects of left + // 2. Create a context to analyze the right-hand side with any inferences possible from left (e.g. ($x instanceof MyClass) && $x->foo() + // 3. Analyze the right-hand side + // 4. Merge the possibly evaluated $right_context for the right-hand side into the original context. (The left_node is guaranteed to have been evaluated, so it becomes $context) + + // TODO: Warn about non-node, they're guaranteed to be always false or true + if ($left_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext($context, $node, $left_node); + + $base_context = $context; + $base_context_scope = $base_context->getScope(); + if ($base_context_scope instanceof GlobalScope) { + $base_context = $context->withScope(new BranchScope($base_context_scope)); + } + $context_with_left_condition = (new ConditionVisitor( + $this->code_base, + $base_context + ))->__invoke($left_node); + $context_with_false_left_condition = (new NegatedConditionVisitor( + $this->code_base, + $base_context + ))->__invoke($left_node); + } else { + $context_with_left_condition = $context; + $context_with_false_left_condition = $context; + } + + if ($right_node instanceof Node) { + $right_context = $this->analyzeAndGetUpdatedContext($context_with_left_condition, $node, $right_node); + if ($right_node->kind === ast\AST_THROW) { + return $this->postOrderAnalyze($context_with_false_left_condition, $node); + } + if (ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $context, $right_node)) { + // If the expression on the right side does have side effects (e.g. `$cond || $x = foo()`), then we need to merge all possibilities. + // + // However, if it doesn't have side effects (e.g. `$a && $b` in `var_export($a || $b)`, then adding the inferences is counterproductive) + $context = (new ContextMergeVisitor( + $context, + [$context, $context_with_false_left_condition, $right_context] + ))->combineChildContextList(); + } + } + + return $this->postOrderAnalyze($context, $node); + } + + /** + * @param Node $node + * A node to parse (for `||` or `or` operator) + * + * @return Context + * A new context resulting from analyzing this `||` operator. + */ + public function analyzeBinaryBoolOr(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + $left_node = $node->children['left']; + $right_node = $node->children['right']; + + // With (left) || (right) + // 1. Update context with any side effects of left + // 2. Create a context to analyze the right-hand side with any inferences possible from left (e.g. (!($x instanceof MyClass)) || $x->foo() + // 3. Analyze the right-hand side + // 4. Merge the possibly evaluated $right_context for the right-hand side into the original context. (The left_node is guaranteed to have been evaluated, so it becomes $context) + + // TODO: Warn about non-node, they're guaranteed to be always false or true + if ($left_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext($context, $node, $left_node); + + $base_context = $context; + $base_context_scope = $base_context->getScope(); + if ($base_context_scope instanceof GlobalScope) { + $base_context = $context->withScope(new BranchScope($base_context_scope)); + } + $context_with_true_left_condition = (new ConditionVisitor( + $this->code_base, + $base_context + ))($left_node); + $context_with_false_left_condition = (new NegatedConditionVisitor( + $this->code_base, + $base_context + ))($left_node); + } else { + $context_with_false_left_condition = $context; + $context_with_true_left_condition = $context; + } + + if ($right_node instanceof Node) { + $right_context = $this->analyzeAndGetUpdatedContext($context_with_false_left_condition, $node, $right_node); + if ($right_node->kind === ast\AST_THROW) { + return $this->postOrderAnalyze($context_with_true_left_condition, $node); + } + if (ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $context, $right_node)) { + // If the expression on the right side does have side effects (e.g. `$cond || $x = foo()`), then we need to merge all possibilities. + // + // However, if it doesn't have side effects (e.g. `$a || $b` in `var_export($a || $b)`, then adding the inferences is counterproductive) + $context = (new ContextMergeVisitor( + $context, + // XXX don't merge $context? + [$context, $context_with_true_left_condition, $right_context] + ))->combineChildContextList(); + } + } + + return $this->postOrderAnalyze($context, $node); + } + + /** + * @param Node $node + * A node to parse (for `??` operator) + * + * @return Context + * A new or an unchanged context resulting from + * parsing the node + */ + public function analyzeBinaryCoalesce(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + $left_node = $node->children['left']; + $right_node = $node->children['right']; + + // With (left) ?? (right) + // 1. Analyze left and update context with any side effects of left + // 2. Check if left is always null or never null, if redundant_condition_detection is enabled + // 3. Analyze right-hand side and update context with any side effects of the right-hand side + // (TODO: consider using a branch here for analyzing variable assignments, etc.) + // 4. Return the updated context + + if ($left_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext($context, $node, $left_node); + } + if (Config::getValue('redundant_condition_detection')) { + // Check for always null or never null values *before* modifying the context with inferences from the right hand side. + // Useful for analyzing `$x ?? ($x = expr)` + $this->analyzeBinaryCoalesceForRedundantCondition($context, $node); + } + if ($right_node instanceof Node) { + $context = $this->analyzeAndGetUpdatedContext($context, $node, $right_node); + } + return $this->postOrderAnalyze($context, $node); + } + + /** + * Returns true if $node represents an expression that can't be undefined + */ + private static function isAlwaysDefined(Context $context, Node $node): bool + { + switch ($node->kind) { + case ast\AST_VAR: + $var_name = $node->children['name']; + if (is_string($var_name) && !$context->isInLoop() && $context->isInFunctionLikeScope() && !Variable::isSuperglobalVariableWithName($var_name)) { + $variable = $context->getScope()->getVariableByNameOrNull($var_name); + return $variable && !$variable->getUnionType()->isPossiblyUndefined(); + } + return false; + case ast\AST_BINARY_OP: + if ($node->flags === ast\flags\BINARY_COALESCE) { + $right_node = $node->children['right']; + return !($right_node instanceof Node) || self::isAlwaysDefined($context, $right_node); + } + return true; + case ast\AST_PROP: + case ast\AST_DIM: + return false; + default: + return true; + } + } + + /** + * Checks if the left hand side of a null coalescing operator is never null or always null + */ + private function analyzeBinaryCoalesceForRedundantCondition(Context $context, Node $node): void + { + $left_node = $node->children['left']; + $right_node = $node->children['right']; + // @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal, PhanPossiblyUndeclaredProperty + if ($right_node instanceof Node && $right_node->kind === ast\AST_CONST && \strcasecmp($right_node->children['name']->children['name'] ?? '', 'null') === 0) { + if ($left_node instanceof Node && self::isAlwaysDefined($context, $left_node)) { + $this->emitIssue( + Issue::CoalescingNeverUndefined, + $node->lineno, + ASTReverter::toShortString($left_node) + ); + } + } + $left = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $left_node); + if (!$left->hasRealTypeSet()) { + return; + } + $left = $left->getRealUnionType(); + if (!$left->containsNullableOrUndefined()) { + if (RedundantCondition::shouldNotWarnAboutIssetCheckForNonNullExpression($this->code_base, $context, $left_node)) { + return; + } + RedundantCondition::emitInstance( + $left_node, + $this->code_base, + (clone($context))->withLineNumberStart($node->lineno), + Issue::CoalescingNeverNull, + [ + ASTReverter::toShortString($left_node), + $left + ], + static function (UnionType $type): bool { + return !$type->containsNullableOrUndefined(); + }, + self::canNodeKindBeNull($left_node) + ); + return; + } elseif ($left->isNull()) { + RedundantCondition::emitInstance( + $left_node, + $this->code_base, + (clone($context))->withLineNumberStart($node->lineno), + Issue::CoalescingAlwaysNull, + [ + ASTReverter::toShortString($left_node), + $left + ], + static function (UnionType $type): bool { + return $type->isNull(); + } + ); + return; + } + } + + /** + * @param Node|string|int|float $node + */ + private static function canNodeKindBeNull($node): bool + { + if (!$node instanceof Node) { + return false; + } + // Look at the nodes that can be null + switch ($node->kind) { + case ast\AST_CAST: + return $node->flags === ast\flags\TYPE_NULL; + case ast\AST_UNARY_OP: + return $node->flags === ast\flags\UNARY_SILENCE && + self::canNodeKindBeNull($node->children['expr']); + case ast\AST_BINARY_OP: + return $node->flags === ast\flags\BINARY_COALESCE && + self::canNodeKindBeNull($node->children['left']) && + self::canNodeKindBeNull($node->children['right']); + case ast\AST_CONST: + case ast\AST_VAR: + case ast\AST_SHELL_EXEC: // SHELL_EXEC will return null instead of an empty string for no output. + case ast\AST_INCLUDE_OR_EVAL: + // $x++, $x--, and --$x return null when $x is null. ++$x doesn't. + case ast\AST_PRE_INC: + case ast\AST_PRE_DEC: + case ast\AST_POST_DEC: + case ast\AST_YIELD_FROM: + case ast\AST_YIELD: + case ast\AST_DIM: + case ast\AST_PROP: + case ast\AST_NULLSAFE_PROP: + case ast\AST_STATIC_PROP: + case ast\AST_CALL: + case ast\AST_CLASS_CONST: + case ast\AST_ASSIGN: + case ast\AST_ASSIGN_REF: + case ast\AST_ASSIGN_OP: // XXX could figure out what kinds of assign ops are guaranteed to be non-null + case ast\AST_METHOD_CALL: + case ast\AST_NULLSAFE_METHOD_CALL: + case ast\AST_STATIC_CALL: + return true; + case ast\AST_CONDITIONAL: + return self::canNodeKindBeNull($node->children['right']); + default: + return false; + } + } + + public function visitConditional(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + // Visit the given node populating the code base + // with anything we learn and get a new context + // indicating the state of the world within the + // given node + // NOTE: unused for AST_CONDITIONAL + // $context = (new PreOrderAnalysisVisitor( + // $this->code_base, $context + // ))($node); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + $true_node = $node->children['true'] ?? null; + $false_node = $node->children['false'] ?? null; + + $cond_node = $node->children['cond']; + if ($cond_node instanceof Node) { + // Step into each child node and get an + // updated context for the node + // (e.g. there may be assignments such as '($x = foo()) ? $a : $b) + $context = $this->analyzeAndGetUpdatedContext($context, $node, $cond_node); + + // TODO: Use different contexts and merge those, in case there were assignments or assignments by reference in both sides of the conditional? + // Reuse the BranchScope (sort of unintuitive). The ConditionVisitor returns a clone and doesn't modify the original. + $base_context = $context; + $base_context_scope = $base_context->getScope(); + if ($base_context_scope instanceof GlobalScope) { + $base_context = $context->withScope(new BranchScope($base_context_scope)); + } + $true_context = (new ConditionVisitor( + $this->code_base, + isset($true_node) ? $base_context : $context // special case: (($d = foo()) ?: 'fallback') + ))->__invoke($cond_node); + $false_context = (new NegatedConditionVisitor( + $this->code_base, + $base_context + ))->__invoke($cond_node); + } else { + $true_context = $context; + $false_context = $context; + if (Config::getValue('redundant_condition_detection')) { + (new ConditionVisitor($this->code_base, $context))->warnRedundantOrImpossibleScalar($cond_node); + } + } + + $child_context_list = []; + // In the long form, there's a $true_node, but in the short form (?:), + // $cond_node is the (already processed) value for truthy. + if ($true_node instanceof Node) { + $child_context = $this->analyzeAndGetUpdatedContext($true_context, $node, $true_node); + $child_context_list[] = $child_context; + } + + if ($false_node instanceof Node) { + $child_context = $this->analyzeAndGetUpdatedContext($false_context, $node, $false_node); + $child_context_list[] = $child_context; + } + if (count($child_context_list) >= 1) { + if (count($child_context_list) < 2) { + $child_context_list[] = $context; + } + $context = (new ContextMergeVisitor( + $context, + $child_context_list + ))->combineChildContextList(); + } + + return $this->postOrderAnalyze($context, $node); + } + + /** + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * + * @see self::visitClosedContext() + */ + public function visitClass(Node $node): Context + { + return $this->visitClosedContext($node); + } + + /** + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * + * @see self::visitClosedContext() + */ + public function visitMethod(Node $node): Context + { + // Make a copy of the internal context so that we don't + // leak any changes within the method to the + // outer scope + $context = $this->context; + $context->setLineNumberStart($node->lineno); + $context = $this->preOrderAnalyze($context, $node); + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + foreach ($node->children as $child_node) { + // Skip any non Node children. + if (!($child_node instanceof Node)) { + continue; + } + + // Step into each child node and get an + // updated context for the node + $this->analyzeAndGetUpdatedContext($context, $node, $child_node); + } + + $this->postOrderAnalyze($context, $node); + + // Return the initial context as we exit + return $this->context; + } + + /** + * @param Node $node + * An AST node of kind ast\AST_FUNC_DECL we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * + * @see self::visitClosedContext() + */ + public function visitFuncDecl(Node $node): Context + { + // Analyze nodes with AST_FUNC_DECL the same way as AST_METHOD + return $this->visitMethod($node); + } + + /** + * @param Node $node + * An AST node of kind ast\AST_CLOSURE we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * + * @see self::visitClosedContext() + */ + public function visitClosure(Node $node): Context + { + return $this->visitClosedContext($node); + } + + /** + * @param Node $node + * An AST node of kind ast\AST_ARROW_FUNC we'd like to analyze the statements for + * + * @return Context + * The updated context after visiting the node + * + * @see self::visitClosedContext() + */ + public function visitArrowFunc(Node $node): Context + { + return $this->visitClosedContext($node); + } + + /** + * Run the common steps for pre-order analysis phase of a Node. + * + * 1. Run the pre-order analysis steps, updating the context and scope + * 2. Run plugins with pre-order steps, usually (but not always) updating the context and scope. + * + * @param Context $context - The context before pre-order analysis + * + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after pre-order analysis of the node + */ + private function preOrderAnalyze(Context $context, Node $node): Context + { + // Visit the given node populating the code base + // with anything we learn and get a new context + // indicating the state of the world within the + // given node + // Equivalent to (new PostOrderAnalysisVisitor(...)($node)) but faster than using __invoke() + $context = (new PreOrderAnalysisVisitor( + $this->code_base, + $context + ))->{Element::VISIT_LOOKUP_TABLE[$node->kind] ?? 'handleMissingNodeKind'}($node); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + return $context; + } + + /** + * Common options for post-order analysis phase of a Node. + * + * 1. Run analysis steps and update the scope and context + * 2. Run plugins (usually (but not always) without updating the scope) + * + * @param Context $context - The context before post-order analysis + * + * @param Node $node + * An AST node we'd like to analyze the statements for + * + * @return Context + * The updated context after post-order analysis of the node + */ + private function postOrderAnalyze(Context $context, Node $node): Context + { + // Now that we know all about our context (like what + // 'self' means), we can analyze statements like + // assignments and method calls. + // Equivalent to (new PostOrderAnalysisVisitor(...)($node)) but faster than using __invoke() + $context = (new PostOrderAnalysisVisitor( + $this->code_base, + $context->withLineNumberStart($node->lineno), + $this->parent_node_list + ))->{Element::VISIT_LOOKUP_TABLE[$node->kind] ?? 'handleMissingNodeKind'}($node); + + // let any configured plugins analyze the node + ConfigPluginSet::instance()->postAnalyzeNode( + $this->code_base, + $context, + $node, + $this->parent_node_list + ); + return $context; + } + + /** + * Analyzes a node of type \ast\AST_GROUP_USE + * This is the same as visit(), but does not recurse into the child nodes. + * + * If this function override didn't exist, + * then visit() would recurse into \ast\AST_USE, + * which would lack part of the namespace. + * (E.g. for use \NS\{const X, const Y}, we don't want to analyze const X or const Y + * without the preceding \NS\) + */ + public function visitGroupUse(Node $node): Context + { + $context = $this->context->withLineNumberStart( + $node->lineno + ); + + // Visit the given node populating the code base + // with anything we learn and get a new context + // indicating the state of the world within the + // given node + $context = (new PreOrderAnalysisVisitor( + $this->code_base, + $context + ))->{Element::VISIT_LOOKUP_TABLE[$node->kind] ?? 'handleMissingNodeKind'}($node); + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + return $this->postOrderAnalyze($context, $node); + } + + /** + * @param Node $node + * An AST node we'd like to analyze the statements for + * @see self::visit() - This is similar to visit() except that the check if $is_static requires parent_node, + * so PreOrderAnalysisVisitor can't be used to modify the Context. + * + * @return Context + * The updated context after visiting the node + */ + public function visitPropElem(Node $node): Context + { + $prop_name = (string)$node->children['name']; + + $context = $this->context; + $class = $context->getClassInScope($this->code_base); + + $context = $this->context->withScope(new PropertyScope( + $context->getScope(), + FullyQualifiedPropertyName::make($class->getFQSEN(), $prop_name) + ))->withLineNumberStart( + $node->lineno + ); + + // Don't bother calling PreOrderAnalysisVisitor, it does nothing + + // Let any configured plugins do a pre-order + // analysis of the node. + ConfigPluginSet::instance()->preAnalyzeNode( + $this->code_base, + $context, + $node + ); + + // With a context that is inside of the node passed + // to this method, we analyze all children of the + // node. + $default = $node->children['default'] ?? null; + if ($default instanceof Node) { + // Step into each child node and get an + // updated context for the node + $context = $this->analyzeAndGetUpdatedContext($context, $node, $default); + } + + return $this->postOrderAnalyze($context, $node); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Bootstrap.php b/bundled-libs/phan/phan/src/Phan/Bootstrap.php new file mode 100644 index 000000000..6eb811c85 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Bootstrap.php @@ -0,0 +1,432 @@ +getMessage()); + } +} + +if (PHP_VERSION_ID < 70200) { + fprintf( + STDERR, + "ERROR: Phan 3.x requires PHP 7.2+ to run, but PHP %s is installed." . PHP_EOL, + PHP_VERSION + ); + fwrite(STDERR, "PHP 7.1 reached its end of life in December 2019." . PHP_EOL); + fwrite(STDERR, "Exiting without analyzing code." . PHP_EOL); + // The version of vendor libraries this depends on will also require php 7.1 + exit(1); +} elseif (PHP_VERSION_ID >= 80100) { + if (!getenv('PHAN_SUPPRESS_PHP_UPGRADE_NOTICE')) { + fprintf( + STDERR, + "Phan %s does not support PHP 8.1+ (PHP %s is installed)" . PHP_EOL, + CLI::PHAN_VERSION, + PHP_VERSION + ); + fwrite( + STDERR, + "Phan 4 or newer may support the latest features of PHP 8.1+." . PHP_EOL + ); + fwrite( + STDERR, + "Executing anyway (Phan may crash). This notice can be disabled with PHAN_SUPPRESS_PHP_UPGRADE_NOTICE=1." . PHP_EOL + ); + } +} + +const LATEST_KNOWN_PHP_AST_VERSION = '1.0.10'; + +/** + * Dump instructions on how to install php-ast + */ +function phan_output_ast_installation_instructions(): void +{ + require_once __DIR__ . '/Library/StringUtil.php'; + $ini_path = php_ini_loaded_file() ?: '(php.ini path could not be determined - try creating one at ' . dirname(PHP_BINARY) . '\\php.ini as a new empty file, or one based on php.ini.development or php.ini.production)'; + $configured_extension_dir = ini_get('extension_dir'); + $extension_dir = StringUtil::isNonZeroLengthString($configured_extension_dir) ? $configured_extension_dir : '(extension directory could not be determined)'; + $extension_dir = "'$extension_dir'"; + $new_extension_dir = dirname(PHP_BINARY) . '\\ext'; + if (!is_dir((string)$configured_extension_dir)) { + $extension_dir .= ' (extension directory does not exist and may need to be changed)'; + } + if (DIRECTORY_SEPARATOR === '\\') { + if (PHP_VERSION_ID < 70500 || !preg_match('/[a-zA-Z]/', PHP_VERSION)) { + fprintf( + STDERR, + PHP_EOL . "Windows users can download php-ast from https://windows.php.net/downloads/pecl/releases/ast/%s/php_ast-%s-%s-%s-%s-%s.zip" . PHP_EOL, + LATEST_KNOWN_PHP_AST_VERSION, + LATEST_KNOWN_PHP_AST_VERSION, + PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, + PHP_ZTS ? 'ts' : 'nts', + 'vc15', + PHP_INT_SIZE == 4 ? 'x86' : 'x64' + ); + fwrite(STDERR, "(if that link doesn't work, check https://windows.php.net/downloads/pecl/releases/ast/ )" . PHP_EOL); + fwrite(STDERR, "To install php-ast, add php_ast.dll from the zip to $extension_dir," . PHP_EOL); + fwrite(STDERR, "Then, enable php-ast by adding the following lines to your php.ini file at '$ini_path'" . PHP_EOL . PHP_EOL); + if (!is_dir((string)$configured_extension_dir) && is_dir($new_extension_dir)) { + fwrite(STDERR, "extension_dir=$new_extension_dir" . PHP_EOL); + } + fwrite(STDERR, "extension=php_ast.dll" . PHP_EOL . PHP_EOL); + } + } else { + fwrite(STDERR, <<= 80000 && version_compare($ast_version, '1.0.10') < 0) { + fprintf( + STDERR, + "ERROR: Phan 3.x requires php-ast 1.0.10+ to properly analyze ASTs for php 8.0+. php-ast %s and php %s is installed." . PHP_EOL, + $ast_version, + PHP_VERSION + ); + phan_output_ast_installation_instructions(); + fwrite(STDERR, "Exiting without analyzing files." . PHP_EOL); + exit(1); + } elseif (PHP_VERSION_ID >= 70400 && version_compare($ast_version, '1.0.2') < 0) { + $did_warn = true; + fprintf( + STDERR, + "WARNING: Phan 3.x requires php-ast 1.0.2+ to properly analyze ASTs for php 7.4+ (1.0.10+ is recommended). php-ast %s and php %s is installed." . PHP_EOL, + $ast_version, + PHP_VERSION + ); + phan_output_ast_installation_instructions(); + } elseif (version_compare($ast_version, '1.0.0') <= 0) { + if ($ast_version === '') { + // Seen in php 7.3 with file_cache when ast is initially enabled but later disabled, due to the result of extension_loaded being assumed to be a constant by opcache. + fwrite(STDERR, "ERROR: extension_loaded('ast') is true, but phpversion('ast') is the empty string. You probably need to clear opcache (opcache.file_cache='" . ini_get('opcache.file_cache') . "')" . PHP_EOL); + } + // TODO: Change this to a warning for 0.1.5 - 1.0.0. (https://github.com/phan/phan/issues/2954) + // 0.1.5 introduced the ast\Node constructor, which is required by the polyfill + // + // NOTE: We haven't loaded the autoloader yet, so these issue messages can't be colorized. + fprintf( + STDERR, + "ERROR: Phan 3.x requires php-ast %s+ because it depends on AST version %d. php-ast '%s' is installed." . PHP_EOL, + Config::MINIMUM_AST_EXTENSION_VERSION, + Config::AST_VERSION, + $ast_version + ); + phan_output_ast_installation_instructions(); + fwrite(STDERR, "Exiting without analyzing files." . PHP_EOL); + exit(1); + } + if (!$did_warn && version_compare($ast_version, '1.0.7') < 0) { + if (!getenv('PHAN_SUPPRESS_AST_DEPRECATION')) { + fprintf(STDERR, "Phan 4.0 will require php-ast 1.0.7+ to parse code using the native parser (1.0.10+ is recommended), but php-ast %s is installed." . PHP_EOL, $ast_version); + phan_output_ast_installation_instructions(); + fwrite(STDERR, "(Set PHAN_SUPPRESS_AST_DEPRECATION=1 to suppress this message)" . PHP_EOL); + } + } + // @phan-suppress-next-line PhanRedundantCondition, PhanImpossibleCondition, PhanSuspiciousValueComparison + if (PHP_VERSION_ID >= 80000 && version_compare(PHP_VERSION, '8.0.0') < 0) { + fwrite(STDERR, "WARNING: Phan may not work properly in PHP 8 versions before PHP 8.0.0. The currently used PHP version is " . PHP_VERSION . PHP_EOL); + } + unset($did_warn); + unset($ast_version); +} + +// Use the composer autoloader +$found_autoloader = false; +foreach ([ + dirname(__DIR__, 2) . '/vendor/autoload.php', // autoloader is in this project (we're in src/Phan and want vendor/autoload.php) + dirname(__DIR__, 5) . '/vendor/autoload.php', // autoloader is in parent project (we're in vendor/phan/phan/src/Phan/Bootstrap.php and want autoload.php + dirname(__DIR__, 4) . '/autoload.php', // autoloader is in parent project (we're in non-standard-vendor/phan/phan/src/Phan/Bootstrap.php and want non-standard-vendor/autoload.php + ] as $file) { + if (file_exists($file)) { + require_once($file); + $found_autoloader = true; + break; + } +} +unset($file); +if (!$found_autoloader) { + fwrite(STDERR, "Could not locate the autoloader\n"); +} +unset($found_autoloader); + +define('EXIT_SUCCESS', 0); +define('EXIT_FAILURE', 1); +define('EXIT_ISSUES_FOUND', EXIT_FAILURE); + +// Throw exceptions so asserts can be linked to the code being analyzed +ini_set('assert.exception', '1'); +// Set a substitute character for StringUtil::asUtf8() +ini_set('mbstring.substitute_character', (string)0xFFFD); + +// Explicitly set each option in case INI is set otherwise +assert_options(ASSERT_ACTIVE, true); +assert_options(ASSERT_WARNING, false); +assert_options(ASSERT_BAIL, false); +// ASSERT_QUIET_EVAL has been removed starting with PHP 8 +if (defined('ASSERT_QUIET_EVAL')) { + assert_options(ASSERT_QUIET_EVAL, false); // @phan-suppress-current-line UnusedPluginSuppression, PhanTypeMismatchArgumentNullableInternal +} +assert_options(ASSERT_CALLBACK, ''); // Can't explicitly set ASSERT_CALLBACK to null? + +// php 8 seems to have segfault issues with disable_function +if (!extension_loaded('filter') && !function_exists('filter_var')) { + if (!($_ENV['PHAN_DISABLE_FILTER_VAR_POLYFILL'] ?? null)) { + fwrite(STDERR, "WARNING: Using a limited polyfill for filter_var() instead of the real filter_var(). **ANALYSIS RESULTS MAY DIFFER AND PLUGINS MAY HAVE ISSUES.** Install and/or enable https://www.php.net/filter to fix this. PHAN_DISABLE_FILTER_VAR_POLYFILL=1 can be used to disable this polyfill.\n"); + require_once __DIR__ . '/filter_var.php_polyfill'; + } +} + +/** + * Print more of the backtrace than is done by default + * @suppress PhanAccessMethodInternal + */ +set_exception_handler(static function (Throwable $throwable): void { + fwrite(STDERR, "ERROR: $throwable\n"); + if (class_exists(CodeBase::class, false)) { + $most_recent_file = CodeBase::getMostRecentlyParsedOrAnalyzedFile(); + if (is_string($most_recent_file)) { + fprintf(STDERR, "(Phan %s crashed due to an uncaught Throwable when parsing/analyzing '%s')\n", CLI::PHAN_VERSION, $most_recent_file); + } else { + fprintf(STDERR, "(Phan %s crashed due to an uncaught Throwable)\n", CLI::PHAN_VERSION); + } + } + // Flush output in case this is related to a bug in a php or its engine that may crash when generating a frame + fflush(STDERR); + fwrite(STDERR, 'More details:' . PHP_EOL); + if (class_exists(Config::class, false)) { + $max_frame_length = max(100, Config::getValue('debug_max_frame_length')); + } else { + $max_frame_length = 1000; + } + $truncated = false; + foreach ($throwable->getTrace() as $i => $frame) { + $frame_details = \Phan\Debug\Frame::frameToString($frame); + if (strlen($frame_details) > $max_frame_length) { + $truncated = true; + if (function_exists('mb_substr')) { + $frame_details = mb_substr($frame_details, 0, $max_frame_length) . '...'; + } else { + $frame_details = substr($frame_details, 0, $max_frame_length) . '...'; + } + } + fprintf(STDERR, '#%d: %s' . PHP_EOL, $i, $frame_details); + fflush(STDERR); + } + + if ($truncated) { + fwrite(STDERR, "(Some long strings (usually JSON of AST Nodes) were truncated. To print more details for some stack frames of this uncaught exception," . + "increase the Phan config setting debug_max_frame_length)" . PHP_EOL); + } + + exit(EXIT_FAILURE); +}); + +/** + * Executes $closure with Phan's default error handler disabled. + * + * This is useful in cases where PHP notices are unavoidable, + * e.g. notices in preg_match() when checking if a regex is valid + * and you don't want the default behavior of terminating the program. + * + * @template T + * @param Closure():T $closure + * @return T + * @see PregRegexCheckerPlugin + */ +function with_disabled_phan_error_handler(Closure $closure) +{ + global $__no_echo_phan_errors; + $__no_echo_phan_errors = true; + try { + return $closure(); + } finally { + $__no_echo_phan_errors = false; + } +} + +/** + * Print a backtrace with values to stderr. + */ +function phan_print_backtrace(bool $is_crash = false, int $frames_to_skip = 2): void +{ + // Uncomment this if even trying to print the details would crash + /* + ob_start(); + debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + fwrite(STDERR, rtrim(ob_get_clean() ?: "failed to dump backtrace") . PHP_EOL); + */ + + $frames = debug_backtrace(); + if (isset($frames[1])) { + fwrite(STDERR, 'More details:' . PHP_EOL); + if (class_exists(Config::class, false)) { + $max_frame_length = max(100, Config::getValue('debug_max_frame_length')); + } else { + $max_frame_length = 1000; + } + $truncated = false; + foreach ($frames as $i => $frame) { + if ($i < $frames_to_skip) { + continue; + } + $frame_details = \Phan\Debug\Frame::frameToString($frame); + if (strlen($frame_details) > $max_frame_length) { + $truncated = true; + if (function_exists('mb_substr')) { + $frame_details = mb_substr($frame_details, 0, $max_frame_length) . '...'; + } else { + $frame_details = substr($frame_details, 0, $max_frame_length) . '...'; + } + } + fprintf(STDERR, '#%d: %s' . PHP_EOL, $i, $frame_details); + } + if ($truncated) { + fwrite(STDERR, "(Some long strings (usually JSON of AST Nodes) were truncated. To print more details for some stack frames of this " . ($is_crash ? "crash" : "log") . ", " . + "increase the Phan config setting debug_max_frame_length)" . PHP_EOL); + } + } +} + +/** + * The error handler for PHP notices, etc. + * This is a named function instead of a closure to make stack traces easier to read. + * + * @suppress PhanAccessMethodInternal + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + */ +function phan_error_handler(int $errno, string $errstr, string $errfile, int $errline): bool +{ + global $__no_echo_phan_errors; + if ($__no_echo_phan_errors) { + if ($__no_echo_phan_errors instanceof Closure) { + if ($__no_echo_phan_errors($errno, $errstr, $errfile, $errline)) { + return true; + } + } else { + return false; + } + } + // php-src/ext/standard/streamsfuncs.c suggests that this is the only error caused by signal handlers and there are no translations. + // In PHP 8.0, "Unable" becomes uppercase. + if ($errno === E_WARNING && preg_match('/^stream_select.*unable to select/i', $errstr)) { + // Don't execute the PHP internal error handler + return true; + } + if ($errno === E_USER_DEPRECATED && preg_match('/(^Passing a command as string when creating a |method is deprecated since Symfony 4\.4)/', $errstr)) { + // Suppress deprecation notices running `vendor/bin/paratest`. + // Don't execute the PHP internal error handler. + return true; + } + if ($errno === E_DEPRECATED && preg_match('/^(Constant |Method ReflectionParameter::getClass)/', $errstr)) { + // Suppress deprecation notices running `vendor/bin/paratest` in php 8 + // Constants such as ENCHANT can be deprecated when calling constant() + return true; + } + if ($errno === E_NOTICE && preg_match('/^(iconv_strlen)/', $errstr)) { + // Suppress deprecation notices in symfony/polyfill-mbstring + return true; + } + if ($errno === E_DEPRECATED && preg_match('/ast\\\\parse_.*Version.*is deprecated/i', $errstr)) { + static $did_warn = false; + if (!$did_warn) { + $did_warn = true; + if (!getenv('PHAN_SUPPRESS_AST_DEPRECATION')) { + CLI::printWarningToStderr(sprintf( + "php-ast AST version %d used by Phan %s has been deprecated in php-ast %s. Check if a newer version of Phan is available." . PHP_EOL, + Config::AST_VERSION, + CLI::PHAN_VERSION, + (string)phpversion('ast') + )); + fwrite(STDERR, "(Set PHAN_SUPPRESS_AST_DEPRECATION=1 to suppress this message)" . PHP_EOL); + } + } + // Don't execute the PHP internal error handler + return true; + } + fwrite(STDERR, "$errfile:$errline [$errno] $errstr\n"); + if (error_reporting() === 0) { + // https://secure.php.net/manual/en/language.operators.errorcontrol.php + // Don't make Phan terminate if the @-operator was being used on an expression. + return false; + } + + if (class_exists(CodeBase::class, false)) { + $most_recent_file = CodeBase::getMostRecentlyParsedOrAnalyzedFile(); + if (is_string($most_recent_file)) { + fprintf(STDERR, "(Phan %s crashed when parsing/analyzing '%s')" . PHP_EOL, CLI::PHAN_VERSION, $most_recent_file); + } else { + fprintf(STDERR, "(Phan %s crashed)" . PHP_EOL, CLI::PHAN_VERSION); + } + } + + phan_print_backtrace(true); + + exit(EXIT_FAILURE); +} +set_error_handler('phan_error_handler'); + +if (!class_exists(CompileError::class)) { + /** + * For self-analysis, add CompileError if it was not already declared. + * + * In PHP 7.3, a new CompileError class was introduced, and ParseError was turned into a subclass of CompileError. + * + * Phan handles both of those separately, so that Phan will work in 7.1+ + * + * @suppress PhanRedefineClassInternal + */ + // phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace + class CompileError extends Error + { + } +} diff --git a/bundled-libs/phan/phan/src/Phan/CLI.php b/bundled-libs/phan/phan/src/Phan/CLI.php new file mode 100644 index 000000000..cfe01608f --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/CLI.php @@ -0,0 +1,2880 @@ +output; + } + + /** + * @var list + * The set of file names to analyze, from the config + */ + private $file_list_in_config = []; + + /** + * @var list + * The set of file names to analyze, from the combined config and CLI options + */ + private $file_list = []; + + /** + * @var bool + * Set to true to ignore all files and directories + * added by means other than -file-list-only on the CLI + */ + private $file_list_only = false; + + /** + * @var string|null + * A possibly null path to the config file to load + */ + private $config_file = null; + + /** + * @param string|string[] $value + * @return list + */ + public static function readCommaSeparatedListOrLists($value): array + { + if (is_array($value)) { + $value = \implode(',', $value); + } + $value_set = []; + foreach (\explode(',', (string)$value) as $file) { + if ($file === '') { + continue; + } + $value_set[$file] = $file; + } + return \array_values($value_set); + } + + /** + * @param array $opts + * @param list $argv + * @throws UsageException + */ + private static function checkAllArgsUsed(array $opts, array &$argv): void + { + $pruneargv = []; + foreach ($opts as $opt => $value) { + foreach ($argv as $key => $chunk) { + $regex = '/^' . (isset($opt[1]) ? '--' : '-') . \preg_quote((string) $opt, '/') . '/'; + + if (in_array($chunk, is_array($value) ? $value : [$value], true) + && $argv[$key - 1][0] === '-' + || \preg_match($regex, $chunk) + ) { + $pruneargv[] = $key; + } + } + } + + while (count($pruneargv) > 0) { + $key = \array_pop($pruneargv); + unset($argv[$key]); + } + + foreach ($argv as $arg) { + if ($arg[0] === '-') { + $parts = \explode('=', $arg, 2); + $key = $parts[0]; + $value = $parts[1] ?? ''; // php getopt() treats --processes and --processes= the same way + $key = \preg_replace('/^--?/', '', $key); + if ($value === '') { + if (in_array($key . ':', self::GETOPT_LONG_OPTIONS, true)) { + throw new UsageException("Missing required value for '$arg'", EXIT_FAILURE); + } + if (strlen($key) === 1 && strlen($parts[0]) === 2) { + // @phan-suppress-next-line PhanParamSuspiciousOrder this is deliberate + if (\strpos(self::GETOPT_SHORT_OPTIONS, "$key:") !== false) { + throw new UsageException("Missing required value for '-$key'", EXIT_FAILURE); + } + } + } + throw new UsageException("Unknown option '$arg'" . self::getFlagSuggestionString($key), EXIT_FAILURE); + } + } + } + + /** + * Creates a CLI object from argv + */ + public static function fromArgv(): CLI + { + global $argv; + + // Parse command line args + $opts = \getopt(self::GETOPT_SHORT_OPTIONS, self::GETOPT_LONG_OPTIONS); + $opts = \is_array($opts) ? $opts : []; + + try { + return new self($opts, $argv); + } catch (UsageException $e) { + self::usage($e->getMessage(), (int)$e->getCode(), $e->print_type, $e->forbid_color); + exit((int)$e->getCode()); // unreachable + } catch (ExitException $e) { + \fwrite(STDERR, $e->getMessage()); + exit($e->getCode()); + } + } + + /** + * Create and read command line arguments, configuring + * \Phan\Config as a side effect. + * + * @param array|false> $opts + * @param list $argv + * @throws ExitException + * @throws UsageException + * @internal - used for unit tests only + */ + public static function fromRawValues(array $opts, array $argv): CLI + { + return new self($opts, $argv); + } + + /** + * Create and read command line arguments, configuring + * \Phan\Config as a side effect. + * + * @param array|false> $opts + * @param list $argv + * @return void + * @throws ExitException + * @throws UsageException + */ + private function __construct(array $opts, array $argv) + { + self::detectAndConfigureColorSupport($opts); + + if (\array_key_exists('extended-help', $opts)) { + throw new UsageException('', EXIT_SUCCESS, UsageException::PRINT_EXTENDED); // --extended-help prints help and calls exit(0) + } + + if (\array_key_exists('h', $opts) || \array_key_exists('help', $opts)) { + throw new UsageException('', EXIT_SUCCESS, UsageException::PRINT_NORMAL); // --help prints help and calls exit(0) + } + if (\array_key_exists('help-annotations', $opts)) { + $result = "See https://github.com/phan/phan/wiki/Annotating-Your-Source-Code for more details." . PHP_EOL . PHP_EOL; + + $result .= "Annotations specific to Phan:" . PHP_EOL; + // @phan-suppress-next-line PhanAccessClassConstantInternal + foreach (Builder::SUPPORTED_ANNOTATIONS as $key => $_) { + $result .= "- " . $key . PHP_EOL; + } + throw new ExitException($result, EXIT_SUCCESS); + } + if (\array_key_exists('v', $opts) || \array_key_exists('version', $opts)) { + printf("Phan %s" . PHP_EOL, self::PHAN_VERSION); + $ast_version = (string) phpversion('ast'); + $ast_version_repr = $ast_version !== '' ? "version $ast_version" : "is not installed"; + printf("php-ast %s" . PHP_EOL, $ast_version_repr); + printf("PHP version used to run Phan: %s" . PHP_EOL, \PHP_VERSION); + if (!\getenv('PHAN_SUPPRESS_PHP_UPGRADE_NOTICE')) { + fwrite(STDERR, "(Consider upgrading to Phan 4, which has the latest features and bug fixes)\n"); + } + throw new ExitException('', EXIT_SUCCESS); + } + self::restartWithoutProblematicExtensions(); + $this->warnSuspiciousShortOptions($argv); + + // Determine the root directory of the project from which + // we route all relative paths passed in as args + $overridden_project_root_directory = $opts['d'] ?? $opts['project-root-directory'] ?? null; + if (\is_string($overridden_project_root_directory)) { + if (!\is_dir($overridden_project_root_directory)) { + throw new UsageException(StringUtil::jsonEncode($overridden_project_root_directory) . ' is not a directory', EXIT_FAILURE, null, true); + } + // Set the current working directory so that relative paths within the project will work. + // TODO: Add an option to allow searching ancestor directories? + \chdir($overridden_project_root_directory); + } + $cwd = \getcwd(); + if (!is_string($cwd)) { + fwrite(STDERR, "Failed to find current working directory\n"); + exit(1); + } + Config::setProjectRootDirectory($cwd); + + if (\array_key_exists('init', $opts)) { + Initializer::initPhanConfig($opts); + exit(EXIT_SUCCESS); + } + + // Before reading the config, check for an override on + // the location of the config file path. + $config_file_override = $opts['k'] ?? $opts['config-file'] ?? null; + if ($config_file_override !== null) { + if (!is_string($config_file_override)) { + // Doesn't work for a mix of -k and --config-file, but low priority + throw new ExitException("Expected exactly one file for --config-file, but saw " . StringUtil::jsonEncode($config_file_override) . "\n", 1); + } + if (!\is_file($config_file_override)) { + throw new ExitException("Could not find the config file override " . StringUtil::jsonEncode($config_file_override) . "\n", 1); + } + $this->config_file = $config_file_override; + } + + if (isset($opts['language-server-force-missing-pcntl'])) { + Config::setValue('language_server_use_pcntl_fallback', true); + } elseif (!isset($opts['language-server-require-pcntl'])) { + // --language-server-allow-missing-pcntl is now the default + if (!\extension_loaded('pcntl')) { + Config::setValue('language_server_use_pcntl_fallback', true); + } + } + + // Now that we have a root directory, attempt to read a + // configuration file `.phan/config.php` if it exists + $this->maybeReadConfigFile(\array_key_exists('require-config-exists', $opts)); + + $this->output = new ConsoleOutput(); + $factory = new PrinterFactory(); + $printer_type = 'text'; + $minimum_severity = Config::getValue('minimum_severity'); + $mask = -1; + $progress_bar = null; + + self::throwIfUsingInitModifiersWithoutInit($opts); + + foreach ($opts as $key => $value) { + $key = (string)$key; + switch ($key) { + case 'r': + case 'file-list-only': + // Mark it so that we don't load files through + // other mechanisms. + $this->file_list_only = true; + + // Empty out the file list + $this->file_list_in_config = []; + + // Intentionally fall through to load the + // file list + case 'f': + case 'file-list': + $file_list = \is_array($value) ? $value : [$value]; + foreach ($file_list as $file_name) { + if (!is_string($file_name)) { + // Should be impossible? + throw new UsageException( + "invalid argument for --file-list", + EXIT_FAILURE, + null, + true + ); + } + $file_path = Config::projectPath($file_name); + if (\is_file($file_path) && \is_readable($file_path)) { + $lines = \file(Config::projectPath($file_name), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (is_array($lines)) { + $this->file_list_in_config = \array_merge( + $this->file_list_in_config, + $lines + ); + continue; + } + } + throw new UsageException( + "Unable to read --file-list of $file_path", + EXIT_FAILURE, + null, + true + ); + } + break; + case 'l': + case 'directory': + if (!$this->file_list_only) { + $directory_list = \is_array($value) ? $value : [$value]; + foreach ($directory_list as $directory_name) { + if (!is_string($directory_name)) { + throw new UsageException( + 'Invalid --directory setting (expected a single argument)', + EXIT_FAILURE, + null, + false + ); + } + $this->file_list_in_config = \array_merge( + $this->file_list_in_config, + $this->directoryNameToFileList( + $directory_name + ) + ); + } + } + break; + case 'k': + case 'config-file': + break; + case 'm': + case 'output-mode': + if (!is_string($value) || !in_array($value, $factory->getTypes(), true)) { + throw new UsageException( + \sprintf( + 'Unknown output mode %s. Known values are [%s]', + StringUtil::jsonEncode($value), + \implode(',', $factory->getTypes()) + ), + EXIT_FAILURE, + null, + true + ); + } + $printer_type = $value; + break; + case 'c': + case 'parent-constructor-required': + Config::setValue('parent_constructor_required', \explode(',', $value)); + break; + case 'q': + case 'quick': + Config::setValue('quick_mode', true); + break; + case 'b': + case 'backward-compatibility-checks': + Config::setValue('backward_compatibility_checks', true); + break; + case 'p': + case 'progress-bar': + $progress_bar = true; + break; + case 'long-progress-bar': + Config::setValue('__long_progress_bar', true); + $progress_bar = true; + break; + case 'no-progress-bar': + $progress_bar = false; + break; + case 'D': + case 'debug': + Config::setValue('debug_output', true); + break; + case 'debug-emitted-issues': + if (!is_string($value)) { + $value = Issue::TRACE_BASIC; + } + BufferingCollector::setTraceIssues($value); + break; + case 'debug-signal-handler': + SignalHandler::init(); + break; + case 'a': + case 'dump-ast': + Config::setValue('dump_ast', true); + break; + case 'dump-ctags': + if (strcasecmp($value, 'basic') !== 0) { + CLI::printErrorToStderr("Unsupported value --dump-ctags='$value'. Supported values are 'basic'.\n"); + exit(1); + } + Config::setValue('plugins', \array_merge( + Config::getValue('plugins'), + [__DIR__ . '/Plugin/Internal/CtagsPlugin.php'] + )); + break; + case 'dump-parsed-file-list': + Config::setValue('dump_parsed_file_list', true); + break; + case 'dump-analyzed-file-list': + Config::setValue('dump_parsed_file_list', self::DUMP_ANALYZED); + break; + case 'dump-signatures-file': + Config::setValue('dump_signatures_file', $value); + break; + case 'find-signature': + try { + if (!is_string($value)) { + throw new InvalidArgumentException("Expected a string, got " . \json_encode($value)); + } + // @phan-suppress-next-line PhanAccessMethodInternal + MethodSearcherPlugin::setSearchString($value); + } catch (InvalidArgumentException $e) { + throw new UsageException("Invalid argument '$value' to --find-signature. Error: " . $e->getMessage() . "\n", EXIT_FAILURE); + } + + Config::setValue('plugins', \array_merge( + Config::getValue('plugins'), + [__DIR__ . '/Plugin/Internal/MethodSearcherPluginLoader.php'] + )); + break; + case 'automatic-fix': + Config::setValue('plugins', \array_merge( + Config::getValue('plugins'), + [__DIR__ . '/Plugin/Internal/IssueFixingPlugin.php'] + )); + break; + case 'o': + case 'output': + if (!is_string($value)) { + throw new UsageException(\sprintf("Invalid arguments to --output: args=%s\n", StringUtil::jsonEncode($value)), EXIT_FAILURE); + } + $output_file = \fopen($value, 'w'); + if (!is_resource($output_file)) { + throw new UsageException("Failed to open output file '$value'\n", EXIT_FAILURE, null, true); + } + $this->output = new StreamOutput($output_file); + break; + case 'i': + case 'ignore-undeclared': + $mask &= ~Issue::CATEGORY_UNDEFINED; + break; + case '3': + case 'exclude-directory-list': + // @phan-suppress-next-line PhanPossiblyFalseTypeArgument + Config::setValue('exclude_analysis_directory_list', self::readCommaSeparatedListOrLists($value)); + break; + case 'exclude-file': + Config::setValue('exclude_file_list', \array_merge( + Config::getValue('exclude_file_list'), + \is_array($value) ? $value : [$value] + )); + break; + case 'I': + case 'include-analysis-file-list': + // @phan-suppress-next-line PhanPossiblyFalseTypeArgument + Config::setValue('include_analysis_file_list', self::readCommaSeparatedListOrLists($value)); + break; + case 'j': + case 'processes': + $processes = \filter_var($value, FILTER_VALIDATE_INT); + if ($processes <= 0) { + throw new UsageException(\sprintf("Invalid arguments to --processes: %s (expected a positive integer)\n", StringUtil::jsonEncode($value)), EXIT_FAILURE); + } + Config::setValue('processes', $processes); + break; + case 'z': + case 'signature-compatibility': + Config::setValue('analyze_signature_compatibility', true); + break; + case 'y': + case 'minimum-severity': + $minimum_severity = \strtolower($value); + if ($minimum_severity === 'low') { + $minimum_severity = Issue::SEVERITY_LOW; + } elseif ($minimum_severity === 'normal') { + $minimum_severity = Issue::SEVERITY_NORMAL; + } elseif ($minimum_severity === 'critical') { + $minimum_severity = Issue::SEVERITY_CRITICAL; + } else { + $minimum_severity = (int)$minimum_severity; + } + break; + case 'target-php-version': + Config::setValue('target_php_version', $value); + break; + case 'minimum-target-php-version': + Config::setValue('minimum_target_php_version', $value); + break; + case 'polyfill-parse-all-element-doc-comments': + // TODO: Drop in Phan 3 + fwrite(STDERR, "--polyfill-parse-all-element-doc-comments is a no-op and will be removed in a future Phan release (no longer needed since PHP 7.0 support was dropped)\n"); + break; + case 'd': + case 'project-root-directory': + // We handle this flag before parsing options so + // that we can get the project root directory to + // base other config flags values on + break; + case 'require-config-exists': + break; // handled earlier. + case 'language-server-allow-missing-pcntl': + case 'language-server-force-missing-pcntl': + case 'language-server-require-pcntl': + break; // handled earlier + case 'language-server-hide-category': + Config::setValue('language_server_hide_category_of_issues', true); + break; + case 'language-server-min-diagnostics-delay-ms': + Config::setValue('language_server_min_diagnostics_delay_ms', (float)$value); + break; + case 'native-syntax-check': + if ($value === '') { + throw new UsageException(\sprintf("Invalid arguments to --native-syntax-check: args=%s\n", StringUtil::jsonEncode($value)), EXIT_FAILURE); + } + if (!is_array($value)) { + $value = [$value]; + } + self::addPHPBinariesForSyntaxCheck($value); + break; + case 'disable-cache': + Config::setValue('cache_polyfill_asts', false); + break; + case 'disable-plugins': + // Slightly faster, e.g. for daemon mode with lowest latency (along with --quick). + Config::setValue('plugins', []); + break; + case 'P': + case 'plugin': + if (!is_array($value)) { + $value = [$value]; + } + self::addPlugins($value); + break; + case 'use-fallback-parser': + Config::setValue('use_fallback_parser', true); + break; + case 'strict-method-checking': + Config::setValue('strict_method_checking', true); + break; + case 'strict-param-checking': + Config::setValue('strict_param_checking', true); + break; + case 'strict-property-checking': + Config::setValue('strict_property_checking', true); + break; + case 'strict-object-checking': + Config::setValue('strict_object_checking', true); + break; + case 'strict-return-checking': + Config::setValue('strict_return_checking', true); + break; + case 'S': + case 'strict-type-checking': + Config::setValue('strict_method_checking', true); + Config::setValue('strict_object_checking', true); + Config::setValue('strict_param_checking', true); + Config::setValue('strict_property_checking', true); + Config::setValue('strict_return_checking', true); + break; + case 's': + case 'daemonize-socket': + self::checkCanDaemonize('unix', $key); + if (!is_string($value)) { + throw new UsageException(\sprintf("Invalid arguments to --daemonize-socket: args=%s", StringUtil::jsonEncode($value)), EXIT_FAILURE); + } + $socket_dirname = \realpath(\dirname($value)); + if (!is_string($socket_dirname) || !\file_exists($socket_dirname) || !\is_dir($socket_dirname)) { + $msg = \sprintf( + 'Requested to create Unix socket server in %s, but folder %s does not exist', + StringUtil::jsonEncode($value), + StringUtil::jsonEncode($socket_dirname) + ); + throw new UsageException($msg, 1, null, true); + } else { + Config::setValue('daemonize_socket', $value); // Daemonize. Assumes the file list won't change. Accepts requests over a Unix socket, or some other IPC mechanism. + } + break; + // TODO(possible idea): HTTP server binding to 127.0.0.1, daemonize-http-port. + case 'daemonize-tcp-host': + $this->checkCanDaemonize('tcp', $key); + Config::setValue('daemonize_tcp', true); + $host = \filter_var($value, FILTER_VALIDATE_IP); + if (\strcasecmp($value, 'default') !== 0 && !$host) { + throw new UsageException("--daemonize-tcp-host must be the string 'default' or a valid ip address to listen on, got '$value'", 1); + } + if ($host) { + Config::setValue('daemonize_tcp_host', $host); + } + break; + case 'daemonize-tcp-port': + $this->checkCanDaemonize('tcp', $key); + Config::setValue('daemonize_tcp', true); + $port = \filter_var($value, FILTER_VALIDATE_INT); + if (\strcasecmp($value, 'default') !== 0 && !($port >= 1024 && $port <= 65535)) { + throw new UsageException("--daemonize-tcp-port must be the string 'default' or a value between 1024 and 65535, got '$value'", 1); + } + if ($port) { + Config::setValue('daemonize_tcp_port', $port); + } + break; + case 'language-server-on-stdin': + Config::setValue('language_server_config', ['stdin' => true]); + break; + case 'language-server-tcp-server': + // TODO: could validate? + Config::setValue('language_server_config', ['tcp-server' => $value]); + break; + case 'language-server-tcp-connect': + Config::setValue('language_server_config', ['tcp' => $value]); + break; + case 'language-server-analyze-only-on-save': + Config::setValue('language_server_analyze_only_on_save', true); + break; + case 'language-server-disable-go-to-definition': + Config::setValue('language_server_enable_go_to_definition', false); + break; + case 'language-server-enable-go-to-definition': + Config::setValue('language_server_enable_go_to_definition', true); + break; + case 'language-server-disable-hover': + Config::setValue('language_server_enable_hover', false); + break; + case 'language-server-enable-hover': + Config::setValue('language_server_enable_hover', true); + break; + case 'language-server-completion-vscode': + break; + case 'language-server-disable-completion': + Config::setValue('language_server_enable_completion', false); + break; + case 'language-server-enable-completion': + break; + case 'language-server-verbose': + Config::setValue('language_server_debug_level', 'info'); + break; + case 'language-server-disable-output-filter': + Config::setValue('language_server_disable_output_filter', true); + break; + case 'x': + case 'dead-code-detection': + Config::setValue('dead_code_detection', true); + break; + case 'X': + case 'dead-code-detection-prefer-false-positive': + Config::setValue('dead_code_detection', true); + Config::setValue('dead_code_detection_prefer_false_negative', false); + break; + case 'u': + case 'unused-variable-detection': + Config::setValue('unused_variable_detection', true); + break; + case 'constant-variable-detection': + Config::setValue('constant_variable_detection', true); + Config::setValue('unused_variable_detection', true); + break; + case 't': + case 'redundant-condition-detection': + Config::setValue('redundant_condition_detection', true); + break; + case 'assume-real-types-for-internal-functions': + Config::setValue('assume_real_types_for_internal_functions', true); + break; + case 'allow-polyfill-parser': + // Just check if it's installed and of a new enough version. + // Assume that if there is an installation, it works, and warn later in ensureASTParserExists() + if (!\extension_loaded('ast')) { + Config::setValue('use_polyfill_parser', true); + break; + } + $ast_version = (new ReflectionExtension('ast'))->getVersion(); + if (\version_compare($ast_version, '1.0.0') <= 0) { + Config::setValue('use_polyfill_parser', true); + break; + } + break; + case 'force-polyfill-parser': + Config::setValue('use_polyfill_parser', true); + break; + case 'force-polyfill-parser-with-original-tokens': + Config::setValue('use_polyfill_parser', true); + Config::setValue('__parser_keep_original_node', true); + break; + case 'memory-limit': + if (\preg_match('@^([1-9][0-9]*)([KMG])?$@D', $value, $match)) { + \ini_set('memory_limit', $value); + } else { + fwrite(STDERR, "Invalid --memory-limit '$value', ignoring\n"); + } + break; + case 'print-memory-usage-summary': + Config::setValue('print_memory_usage_summary', true); + break; + case 'markdown-issue-messages': + Config::setValue('markdown_issue_messages', true); + break; + case 'absolute-path-issue-messages': + Config::setValue('absolute_path_issue_messages', true); + break; + case 'color-scheme': + case 'C': + case 'color': + case 'no-color': + case 'analyze-all-files': + // Handled before processing the CLI flag `--help` + break; + case 'save-baseline': + if (!is_string($value)) { + throw new UsageException("--save-baseline expects a single writeable file", 1); + } + if (!\is_dir(\dirname($value))) { + throw new UsageException("--save-baseline expects a file in a folder that already exists, got path '$value' in folder '" . \dirname($value) . "'", 1); + } + Config::setValue('__save_baseline_path', $value); + break; + case 'B': + case 'load-baseline': + if (!is_string($value)) { + throw new UsageException("--load-baseline expects a single readable file", 1); + } + if (!\is_file($value)) { + throw new UsageException("--load-baseline expects a path to a file, got '$value'", 1); + } + if (!\is_readable($value)) { + throw new UsageException("--load-baseline passed file '$value' which could not be read", 1); + } + Config::setValue('baseline_path', $value); + break; + case 'baseline-summary-type': + if (!is_string($value)) { + throw new UsageException("--baseline-summary-type expects 'ordered_by_count', 'ordered_by_count', 'or 'none', but got multiple values", 1); + } + Config::setValue('baseline_summary_type', $value); + break; + case 'analyze-twice': + Config::setValue('__analyze_twice', true); + break; + case 'always-exit-successfully-after-analysis': + Config::setValue('__always_exit_successfully_after_analysis', true); + break; + default: + // All of phan's long options are currently at least 2 characters long. + $key_repr = strlen($key) >= 2 ? "--$key" : "-$key"; + if ($value === false && in_array($key . ':', self::GETOPT_LONG_OPTIONS, true)) { + throw new UsageException("Missing required argument value for '$key_repr'", EXIT_FAILURE); + } + throw new UsageException("Unknown option '$key_repr'" . self::getFlagSuggestionString($key), EXIT_FAILURE); + } + } + if (isset($opts['language-server-completion-vscode']) && Config::getValue('language_server_enable_completion')) { + Config::setValue('language_server_enable_completion', Config::COMPLETION_VSCODE); + } + if (Config::getValue('color_issue_messages') === null && in_array($printer_type, ['text', 'verbose'], true)) { + if (Config::getValue('color_issue_messages_if_supported') && self::supportsColor(\STDOUT)) { + Config::setValue('color_issue_messages', true); + } + } + self::ensureASTParserExists(); + self::checkPluginsExist(); + self::checkValidFileConfig(); + if (\is_null($progress_bar)) { + if (self::isProgressBarDisabledByDefault()) { + $progress_bar = false; + } else { + $progress_bar = true; + if (!self::isTerminal(\STDERR)) { + Config::setValue('__long_progress_bar', true); + } + } + } + Config::setValue('progress_bar', $progress_bar); + + $output = $this->output; + $printer = $factory->getPrinter($printer_type, $output); + $filter = new ChainedIssueFilter([ + new FileIssueFilter(new Phan()), + new MinimumSeverityFilter($minimum_severity), + new CategoryIssueFilter($mask) + ]); + $collector = new BufferingCollector($filter); + + self::checkAllArgsUsed($opts, $argv); + + Phan::setPrinter($printer); + Phan::setIssueCollector($collector); + if (!$this->file_list_only) { + // Merge in any remaining args on the CLI + $this->file_list_in_config = \array_merge( + $this->file_list_in_config, + array_slice($argv, 1) + ); + } + if (isset($opts['analyze-all-files'])) { + Config::setValue('exclude_analysis_directory_list', []); + } + + $this->recomputeFileList(); + + // We can't run dead code detection on multiple cores because + // we need to update reference lists in a globally accessible + // way during analysis. With our parallelization mechanism, there + // is no shared state between processes, making it impossible to + // have a complete set of reference lists. + if (Config::getValue('processes') !== 1 + && Config::getValue('dead_code_detection')) { + throw new AssertionError("We cannot run dead code detection on more than one core."); + } + if (!\getenv('PHAN_SUPPRESS_PHP_UPGRADE_NOTICE')) { + fprintf(STDERR, "(Consider upgrading from Phan %s to Phan 4, which has the latest features and bug fixes. This notice can be disabled with PHAN_SUPPRESS_PHP_UPGRADE_NOTICE=1))\n", CLI::PHAN_VERSION); + } + self::checkSaveBaselineOptionsAreValid(); + self::ensureServerRunsSingleAnalysisProcess(); + } + + /** + * @param list $plugins plugins to add to the plugin list + */ + private static function addPlugins(array $plugins): void + { + Config::setValue( + 'plugins', + \array_unique(\array_merge(Config::getValue('plugins'), $plugins)) + ); + } + + /** + * @param list $binaries - various binaries, such as 'php72' and '/usr/bin/php' + * @throws UsageException + */ + private static function addPHPBinariesForSyntaxCheck(array $binaries): void + { + $resolved_binaries = []; + foreach ($binaries as $binary) { + if ($binary === '') { + throw new UsageException(\sprintf("Invalid arguments to --native-syntax-check: args=%s\n", StringUtil::jsonEncode($binaries)), EXIT_FAILURE); + } + if (DIRECTORY_SEPARATOR === '\\') { + $cmd = 'where ' . escapeshellarg($binary); + } else { + $cmd = 'command -v ' . escapeshellarg($binary); + } + $resolved = trim((string) shell_exec($cmd)); + if ($resolved === '') { + throw new UsageException(\sprintf("Could not find PHP binary for --native-syntax-check: arg=%s\n", StringUtil::jsonEncode($binary)), EXIT_FAILURE); + } + if (!is_executable($resolved)) { + throw new UsageException(\sprintf("PHP binary for --native-syntax-check is not executable: arg=%s\n", StringUtil::jsonEncode($binary)), EXIT_FAILURE); + } + $resolved_binaries[] = $resolved; + } + self::addPlugins(['InvokePHPNativeSyntaxCheckPlugin']); + $plugin_config = Config::getValue('plugin_config') ?: []; + $old_resolved_binaries = $plugin_config['php_native_syntax_check_binaries'] ?? []; + $resolved_binaries = array_values(array_unique(array_merge($old_resolved_binaries, $resolved_binaries))); + $plugin_config['php_native_syntax_check_binaries'] = $resolved_binaries; + Config::setValue('plugin_config', $plugin_config); + } + + /** + * @param list $argv + */ + private static function warnSuspiciousShortOptions(array $argv): void + { + $opt_set = []; + foreach (self::GETOPT_LONG_OPTIONS as $opt) { + $opt_set['-' . \rtrim($opt, ':')] = true; + } + foreach (array_slice($argv, 1) as $arg) { + $arg = \preg_replace('/=.*$/D', '', $arg); + if (\array_key_exists($arg, $opt_set)) { + self::printHelpSection( + "WARNING: Saw suspicious CLI arg '$arg' (did you mean '-$arg')\n", + false, + true + ); + } + } + } + + /** + * @param array $opts + * @throws UsageException if using a flag such as --init-level without --init + */ + private static function throwIfUsingInitModifiersWithoutInit(array $opts): void + { + if (isset($opts['init'])) { + return; + } + $bad_options = []; + foreach ($opts as $other_key => $_) { + // -3 is an option, and gets converted to `3` in an array key. + if (\strncmp((string)$other_key, 'init-', 5) === 0) { + $bad_options[] = "--$other_key"; + } + } + if (count($bad_options) > 0) { + $option_pluralized = count($bad_options) > 1 ? "options" : "option"; + $make_pluralized = count($bad_options) > 1 ? "make" : "makes"; + throw new UsageException("The $option_pluralized " . \implode(' and ', $bad_options) . " only $make_pluralized sense when initializing a new Phan config with --init", EXIT_FAILURE, UsageException::PRINT_INIT_ONLY); + } + } + + /** + * Configure settings for colorized output for help and issue messages. + * @param array $opts + */ + private static function detectAndConfigureColorSupport(array $opts): void + { + if (is_string($opts['color-scheme'] ?? false)) { + \putenv('PHAN_COLOR_SCHEME=' . $opts['color-scheme']); + } + if (isset($opts['C']) || isset($opts['color'])) { + Config::setValue('color_issue_messages', true); + } elseif (isset($opts['no-color'])) { + Config::setValue('color_issue_messages', false); + } elseif (self::hasNoColorEnv()) { + Config::setValue('color_issue_messages', false); + } elseif (getenv('PHAN_ENABLE_COLOR_OUTPUT')) { + Config::setValue('color_issue_messages_if_supported', true); + } + } + + private static function hasNoColorEnv(): bool + { + return getenv('PHAN_DISABLE_COLOR_OUTPUT') || getenv('NO_COLOR'); + } + + private static function checkValidFileConfig(): void + { + $include_analysis_file_list = Config::getValue('include_analysis_file_list'); + if ($include_analysis_file_list) { + $valid_files = 0; + foreach ($include_analysis_file_list as $file) { + $absolute_path = Config::projectPath($file); + if (\file_exists($absolute_path)) { + $valid_files++; + } else { + \fprintf( + STDERR, + "%sCould not find file '%s' passed in %s" . PHP_EOL, + self::colorizeHelpSectionIfSupported('WARNING: '), + $absolute_path, + self::colorizeHelpSectionIfSupported('--include-analysis-file-list') + ); + } + } + if ($valid_files === 0) { + // TODO convert this to an error in Phan 3. + $error_message = \sprintf( + "None of the files to analyze in %s exist - This will be an error in future Phan releases." . PHP_EOL, + Config::getProjectRootDirectory() + ); + CLI::printWarningToStderr($error_message); + } + } + } + + /** + * Returns true if the output stream supports colors + * + * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo + * terminals via named pipes, so we can only check the environment. + * + * Reference: Composer\XdebugHandler\Process::supportsColor + * https://github.com/composer/xdebug-handler + * (This is internal, so it was duplicated in case their API changed) + * + * @param resource $output A valid CLI output stream + * @suppress PhanUndeclaredFunction + */ + public static function supportsColor($output): bool + { + if (self::isDaemonOrLanguageServer()) { + return false; + } + if ('Hyper' === getenv('TERM_PROGRAM')) { + return true; + } + if (\defined('PHP_WINDOWS_VERSION_BUILD')) { + return (\function_exists('sapi_windows_vt100_support') + && \sapi_windows_vt100_support($output)) + || false !== \getenv('ANSICON') + || 'ON' === \getenv('ConEmuANSI') + || 'xterm' === \getenv('TERM'); + } + + if (\function_exists('stream_isatty')) { + return \stream_isatty($output); + } elseif (\function_exists('posix_isatty')) { + return \posix_isatty($output); + } + + $stat = \fstat($output); + // Check if formatted mode is S_IFCHR + return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; + } + + private static function isProgressBarDisabledByDefault(): bool + { + if (self::isDaemonOrLanguageServer()) { + return true; + } + if (\getenv('PHAN_DISABLE_PROGRESS_BAR')) { + return true; + } + return false; + } + + /** + * Returns true if the output stream is a TTY. + * + * @param resource $output A valid CLI output stream + * @suppress PhanUndeclaredFunction + */ + private static function isTerminal($output): bool + { + if (\defined('PHP_WINDOWS_VERSION_BUILD')) { + // https://www.php.net/sapi_windows_vt100_support + // > By the way, if a stream is redirected, the VT100 feature will not be enabled: + return (\function_exists('sapi_windows_vt100_support') + && \sapi_windows_vt100_support($output)); + } + + if (\function_exists('stream_isatty')) { + return \stream_isatty($output); + } elseif (\function_exists('posix_isatty')) { + return \posix_isatty($output); + } + + $stat = \fstat($output); + // Check if formatted mode is S_IFCHR + return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; + } + + private static function checkPluginsExist(): void + { + $all_plugins_exist = true; + foreach (Config::getValue('plugins') as $plugin_path_or_name) { + // @phan-suppress-next-line PhanAccessMethodInternal + $plugin_file_name = ConfigPluginSet::normalizePluginPath($plugin_path_or_name); + if (!\is_file($plugin_file_name)) { + if ($plugin_file_name === $plugin_path_or_name) { + $details = ''; + } else { + $details = ' (Referenced as ' . StringUtil::jsonEncode($plugin_path_or_name) . ')'; + $details .= self::getPluginSuggestionText($plugin_path_or_name); + } + self::printErrorToStderr(\sprintf( + "Phan %s could not find plugin %s%s\n", + CLI::PHAN_VERSION, + StringUtil::jsonEncode($plugin_file_name), + $details + )); + $all_plugins_exist = false; + } + } + if (!$all_plugins_exist) { + fwrite(STDERR, "Exiting due to invalid plugin config.\n"); + exit(1); + } + } + + /** + * @throws UsageException if the combination of options is invalid + */ + private static function checkSaveBaselineOptionsAreValid(): void + { + if (Config::getValue('__save_baseline_path')) { + if (Config::getValue('processes') !== 1) { + // This limitation may be fixed in a subsequent release. + throw new UsageException("--save-baseline is not supported in combination with --processes", 1); + } + if (self::isDaemonOrLanguageServer()) { + // This will never be supported + throw new UsageException("--save-baseline does not make sense to use in Daemon mode or as a language server.", 1); + } + } + } + + private static function ensureServerRunsSingleAnalysisProcess(): void + { + if (!self::isDaemonOrLanguageServer()) { + return; + } + // If the client has multiple files open at once (and requests analysis of multiple files), + // then there there would be multiple processes doing analysis. + // + // This would not work with Phan's current design - the socket used by the daemon can only be used by one process. + // Also, the implementation of some requests such as "Go to Definition", "Find References" (planned), etc. assume Phan runs as a single process. + $processes = Config::getValue('processes'); + if ($processes !== 1) { + \fprintf(STDERR, "Notice: Running with processes=1 instead of processes=%s - the daemon/language server assumes it will run as a single process" . PHP_EOL, (string)\json_encode($processes)); + Config::setValue('processes', 1); + } + if (Config::getValue('__analyze_twice')) { + \fwrite(STDERR, "Notice: Running analysis phase once instead of --analyze-twice - the daemon/language server assumes it will run as a single process" . PHP_EOL); + Config::setValue('__analyze_twice', false); + } + } + + /** + * @internal (visible for tests) + */ + public static function getPluginSuggestionText(string $plugin_path_or_name): string + { + $plugin_dirname = ConfigPluginSet::getBuiltinPluginDirectory(); + $candidates = []; + foreach (\scandir($plugin_dirname) as $basename) { + if (\substr($basename, -4) !== '.php') { + continue; + } + $plugin_name = \substr($basename, 0, -4); + $candidates[$plugin_name] = $plugin_name; + } + $suggestions = IssueFixSuggester::getSuggestionsForStringSet($plugin_path_or_name, $candidates); + if (!$suggestions) { + return ''; + } + return ' (Did you mean ' . \implode(' or ', $suggestions) . '?)'; + } + + /** + * Recompute the list of files (used in daemon mode or language server mode) + */ + public function recomputeFileList(): void + { + $this->file_list = $this->file_list_in_config; + + if (!$this->file_list_only) { + // Merge in any files given in the config + /** @var list */ + $this->file_list = \array_merge( + $this->file_list, + Config::getValue('file_list') + ); + + // Merge in any directories given in the config + foreach (Config::getValue('directory_list') as $directory_name) { + $this->file_list = \array_merge( + $this->file_list, + self::directoryNameToFileList($directory_name) + ); + } + + // Don't scan anything twice + $this->file_list = self::uniqueFileList($this->file_list); + } + + // Exclude any files that should be excluded from + // parsing and analysis (not read at all) + if (count(Config::getValue('exclude_file_list')) > 0) { + $exclude_file_set = []; + foreach (Config::getValue('exclude_file_list') as $file) { + $normalized_file = \str_replace('\\', '/', $file); + $exclude_file_set[$normalized_file] = true; + $exclude_file_set["./$normalized_file"] = true; + } + + $this->file_list = \array_values(\array_filter( + $this->file_list, + static function (string $file) use ($exclude_file_set): bool { + // Handle edge cases such as 'mydir/subdir\subsubdir' on Windows, if mydir/subdir was in the Phan config. + return !isset($exclude_file_set[\str_replace('\\', '/', $file)]); + } + )); + } + } + + /** + * @param string[] $file_list + * @return list $file_list without duplicates + */ + public static function uniqueFileList(array $file_list): array + { + $result = []; + foreach ($file_list as $file) { + // treat src/a.php, src//a.php, and src\a.php (on Windows) as the same file + $file_key = \preg_replace('@/{2,}@', '/', \str_replace(\DIRECTORY_SEPARATOR, '/', $file)); + if (!isset($result[$file_key])) { + $result[$file_key] = $file; + } + } + return \array_values($result); + } + + /** + * @return void - exits on usage error + * @throws UsageException + */ + private static function checkCanDaemonize(string $protocol, string $opt): void + { + $opt = strlen($opt) >= 2 ? "--$opt" : "-$opt"; + if (!in_array($protocol, \stream_get_transports(), true)) { + throw new UsageException("The $protocol:///path/to/file schema is not supported on this system, cannot create a daemon with $opt", 1); + } + if (!Config::getValue('language_server_use_pcntl_fallback') && !\function_exists('pcntl_fork')) { + throw new UsageException("The pcntl extension is not available to fork a new process, so $opt will not be able to create workers to respond to requests.", 1); + } + if ($opt === '--daemonize-socket' && Config::getValue('daemonize_tcp')) { + throw new UsageException('Can specify --daemonize-socket or --daemonize-tcp-port only once', 1); + } elseif (($opt === '--daemonize-tcp-host' || $opt === '--daemonize-tcp-port') && Config::getValue('daemonize_socket')) { + throw new UsageException("Can specify --daemonize-socket or $opt only once", 1); + } + } + + /** + * @return list + * Get the set of files to analyze + */ + public function getFileList(): array + { + return $this->file_list; + } + + public const INIT_HELP = <<<'EOT' + --init + [--init-level=3] + [--init-analyze-dir=path/to/src] + [--init-analyze-file=path/to/file.php] + [--init-no-composer] + + Generates a `.phan/config.php` in the current directory + based on the project's composer.json. + The logic used to generate the config file is currently very simple. + Some third party classes (e.g. in vendor/) + will need to be manually added to 'directory_list' or excluded, + and you may end up with a large number of issues to be manually suppressed. + See https://github.com/phan/phan/wiki/Tutorial-for-Analyzing-a-Large-Sloppy-Code-Base + + [--init-level ] affects the generated settings in `.phan/config.php` + (e.g. null_casts_as_array). + `--init-level` can be set to 1 (strictest) to 5 (least strict) + [--init-analyze-dir

] can be used as a relative path alongside directories + that Phan infers from composer.json's "autoload" settings + [--init-analyze-file ] can be used as a relative path alongside files + that Phan infers from composer.json's "bin" settings + [--init-no-composer] can be used to tell Phan that the project + is not a composer project. + Phan will not check for composer.json or vendor/, + and will not include those paths in the generated config. + [--init-overwrite] will allow 'phan --init' to overwrite .phan/config.php. + +EOT; + + // FIXME: If I stop using defined() in UnionTypeVisitor, + // this will warn about the undefined constant EXIT_SUCCESS when a + // user-defined constant is used in parse phase in a function declaration + /** + * Print usage message to stdout. + * @internal + */ + public static function usage(string $msg = '', ?int $exit_code = EXIT_SUCCESS, int $usage_type = UsageException::PRINT_NORMAL, bool $forbid_color = true): void + { + global $argv; + + if ($msg !== '') { + self::printHelpSection("ERROR:"); + self::printHelpSection(" $msg\n", $forbid_color); + } + + $init_help = self::INIT_HELP; + echo "Usage: {$argv[0]} [options] [files...]\n"; + if ($usage_type === UsageException::PRINT_INVALID_ARGS) { + self::printHelpSection("Type {$argv[0]} --help (or --extended-help) for usage.\n"); + if ($exit_code === null) { + return; + } + if (!getenv('PHAN_SUPPRESS_PHP_UPGRADE_NOTICE')) { + \fprintf(STDERR, "\nPhan %s is installed, but Phan 4 has the latest features and bug fixes.\n", CLI::PHAN_VERSION); + } + exit($exit_code); + } + if ($usage_type === UsageException::PRINT_INIT_ONLY) { + self::printHelpSection($init_help . "\n"); + if ($exit_code === null) { + return; + } + exit($exit_code); + } + self::printHelpSection( + << + A file containing a list of PHP files to be analyzed + + -l, --directory + A directory that should be parsed for class and + method information. After excluding the directories + defined in --exclude-directory-list, the remaining + files will be statically analyzed for errors. + + Thus, both first-party and third-party code being used by + your application should be included in this list. + + You may include multiple `--directory ` options. + + --exclude-file + A file that should not be parsed or analyzed (or read + at all). This is useful for excluding hopelessly + unanalyzable files. + + -3, --exclude-directory-list + A comma-separated list of directories that defines files + that will be excluded from static analysis, but whose + class and method information should be included. + (can be repeated, ignored if --include-analysis-file-list is used) + + Generally, you'll want to include the directories for + third-party code (such as "vendor/") in this list. + + -I, --include-analysis-file-list + A comma-separated list of files that will be included in + static analysis. All others won't be analyzed. + (can be repeated) + + This is primarily intended for performing standalone + incremental analysis. + + -d, --project-root-directory + The directory of the project to analyze. + Phan expects this directory to contain the configuration file `.phan/config.php`. + If not provided, the current working directory is analyzed. + + -r, --file-list-only + A file containing a list of PHP files to be analyzed to the + exclusion of any other directories or files passed in. This + is unlikely to be useful. + + -k, --config-file + A path to a config file to load (instead of the default of + `.phan/config.php`). + + -m, --output-mode + Output mode from 'text', 'verbose', 'json', 'csv', 'codeclimate', 'checkstyle', 'pylint', or 'html' + + -o, --output + Output filename + +$init_help + -C, --color, --no-color + Add colors to the outputted issues. + This is recommended for only --output-mode=text (the default) and 'verbose' + + [--color-scheme={default,code,eclipse_dark,vim,light,light_high_contrast}] + This (or the environment variable PHAN_COLOR_SCHEME) can be used to set the color scheme for emitted issues. + + -p, --progress-bar, --no-progress-bar, --long-progress-bar + Show progress bar. --no-progress-bar disables the progress bar. + --long-progress-bar shows a progress bar that doesn't overwrite the current line. + + -D, --debug + Print debugging output to stderr. Useful for looking into performance issues or crashes. + + -q, --quick + Quick mode - doesn't recurse into all function calls + + -b, --backward-compatibility-checks + Check for potential PHP 5 -> PHP 7 BC issues + + --target-php-version {7.0,7.1,7.2,7.3,7.4,8.0,native} + The PHP version that the codebase will be checked for compatibility against. + For best results, the PHP binary used to run Phan should have the same PHP version. + (Phan relies on Reflection for some param counts + and checks for undefined classes/methods/functions) + + --minimum-target-php-version {7.0,7.1,7.2,7.3,7.4,8.0,native} + The PHP version that will be used for feature/syntax compatibility warnings. + + -i, --ignore-undeclared + Ignore undeclared functions and classes + + -y, --minimum-severity + Minimum severity level (low=0, normal=5, critical=10) to report. + Defaults to `--minimum-severity 0` (i.e. `--minimum-severity low`) + + -c, --parent-constructor-required + Comma-separated list of classes that require + parent::__construct() to be called + + -x, --dead-code-detection + Emit issues for classes, methods, functions, constants and + properties that are probably never referenced and can + be removed. This implies `--unused-variable-detection`. + + -u, --unused-variable-detection + Emit issues for variables, parameters and closure use variables + that are probably never referenced. + This has a few known false positives, e.g. for loops or branches. + + -t, --redundant-condition-detection + Emit issues for conditions such as `is_int(expr)` that are redundant or impossible. + + This has some known false positives for loops, variables set in loops, + and global variables. + + -j, --processes + The number of parallel processes to run during the analysis + phase. Defaults to 1. + + -z, --signature-compatibility + Analyze signatures for methods that are overrides to ensure + compatibility with what they're overriding. + + --disable-cache + Don't cache any ASTs from the polyfill/fallback. + + ASTs from the native parser (php-ast) don't need to be cached. + + This is useful if Phan will be run only once and php-ast is unavailable (e.g. in Travis) + + --disable-plugins + Don't run any plugins. Slightly faster. + + -P, --plugin + Add a plugin to run. This flag can be repeated. + (Either pass the name of the plugin or a relative/absolute path to the plugin) + + --strict-method-checking + Warn if any type in a method invocation's object is definitely not an object, + or any type in an invoked expression is not a callable. + (Enables the config option `strict_method_checking`) + + --strict-param-checking + Warn if any type in an argument's union type cannot be cast to + the parameter's expected union type. + (Enables the config option `strict_param_checking`) + + --strict-property-checking + Warn if any type in a property assignment's union type + cannot be cast to a type in the property's declared union type. + (Enables the config option `strict_property_checking`) + + --strict-object-checking + Warn if any type of the object expression for a property access + does not contain that property. + (Enables the config option `strict_object_checking`) + + --strict-return-checking + Warn if any type in a returned value's union type + cannot be cast to the declared return type. + (Enables the config option `strict_return_checking`) + + -S, --strict-type-checking + Equivalent to + `--strict-method-checking --strict-object-checking --strict-param-checking --strict-property-checking --strict-return-checking`. + + --use-fallback-parser + If a file to be analyzed is syntactically invalid + (i.e. "php --syntax-check path/to/file" would emit a syntax error), + then retry, using a different, slower error tolerant parser to parse it. + (And phan will then analyze what could be parsed). + This flag is experimental and may result in unexpected exceptions or errors. + This flag does not affect excluded files and directories. + + --allow-polyfill-parser + If the `php-ast` extension isn't available or is an outdated version, + then use a slower parser (based on tolerant-php-parser) instead. + Note that https://github.com/Microsoft/tolerant-php-parser + has some known bugs which may result in false positive parse errors. + + --force-polyfill-parser + Use a slower parser (based on tolerant-php-parser) instead of the native parser, + even if the native parser is available. + Useful mainly for debugging. + + -s, --daemonize-socket + Unix socket for Phan to listen for requests on, in daemon mode. + + --daemonize-tcp-host + TCP hostname for Phan to listen for JSON requests on, in daemon mode. + (e.g. `default`, which is an alias for host `127.0.0.1`, or `0.0.0.0` for + usage with Docker). `phan_client` can be used to communicate with the Phan Daemon. + + --daemonize-tcp-port + TCP port for Phan to listen for JSON requests on, in daemon mode. + (e.g. `default`, which is an alias for port 4846.) + `phan_client` can be used to communicate with the Phan Daemon. + + --save-baseline + Generates a baseline of pre-existing issues that can be used to suppress + pre-existing issues in subsequent runs (with --load-baseline) + + This baseline depends on the environment, CLI and config settings used to run Phan + (e.g. --dead-code-detection, plugins, etc.) + + Paths such as .phan/baseline.php, .phan/baseline_deadcode.php, etc. are recommended. + + -B, --load-baseline + Loads a baseline of pre-existing issues to suppress. + + (For best results, the baseline should be generated with the same/similar + environment and settings as those used to run Phan) + + --analyze-twice + Runs the analyze phase twice. Because Phan gathers additional type information for properties, return types, etc. during analysis, + this may emit a more complete list of issues. + + This cannot be used with --processes . + + -v, --version + Print Phan's version number + + -h, --help + This help information + + --extended-help + This help information, plus less commonly used flags + (E.g. for daemon mode) + +EOB + , + $forbid_color + ); + if ($usage_type === UsageException::PRINT_EXTENDED) { + self::printHelpSection( + << + Emit JSON serialized signatures to the given file. + This uses a method signature format similar to FunctionSignatureMap.php. + + --dump-ctags=basic + Dump a ctags file to /tags using the parsed and analyzed files + in the Phan config. + Currently, this only dumps classes/constants/functions/properties, + and not variable definitions. + This should be used with --quick, and can't be used with --processes . + + --always-exit-successfully-after-analysis + Always exit with an exit code of 0, even if unsuppressed issues were emitted. + This helps in checking if Phan crashed. + + --automatic-fix + Automatically fix any issues Phan is capable of fixing. + NOTE: This is a work in progress and limited to a small subset of issues + (e.g. unused imports on their own line) + + --force-polyfill-parser-with-original-tokens + Force tracking the original tolerant-php-parser and tokens in every node + generated by the polyfill as `\$node->tolerant_ast_node`, where possible. + This is slower and more memory intensive. + Official or third-party plugins implementing functionality such as + `--automatic-fix` may end up requiring this, + because the original tolerant-php-parser node contains the original formatting + and token locations. + + --find-signature paramUnionType2->returnUnionType> + Find a signature in the analyzed codebase that is similar to the argument. + See `tool/phoogle` for examples. + + --memory-limit + Sets the memory limit for analysis (per process). + This is useful when developing or when you want guarantees on memory limits. + K, M, and G are optional suffixes (Kilobytes, Megabytes, Gigabytes). + + --print-memory-usage-summary + Prints a summary of memory usage and maximum memory usage. + This is accurate when there is one analysis process. + + --markdown-issue-messages + Emit issue messages with markdown formatting. + + --absolute-path-issue-messages + Emit issues with their absolute paths instead of relative paths. + This does not affect files mentioned within the issue. + + --analyze-all-files + Ignore the --exclude-directory-list flag and `exclude_analysis_directory_list` config settings and analyze all files that were parsed. + This is slow, but useful when third-party files being parsed have incomplete type information. + Also see --analyze-twice. + + --constant-variable-detection + Emit issues for variables that could be replaced with literals or constants. + (i.e. they are declared once (as a constant expression) and never modified). + This is almost entirely false positives for most coding styles. + Implies --unused-variable-detection + + -X, --dead-code-detection-prefer-false-positive + When performing dead code detection, prefer emitting false positives + (reporting dead code that is not actually dead) over false negatives + (failing to report dead code). This implies `--dead-code-detection`. + + --debug-emitted-issues={basic,verbose} + Print backtraces of emitted issues which weren't suppressed to stderr. + + --debug-signal-handler + Set up a signal handler that can handle interrupts, SIGUSR1, and SIGUSR2. + This requires pcntl, and slows down Phan. When this option is enabled, + + Ctrl-C (kill -INT ) can be used to make Phan stop and print a crash report. + (This is useful for diagnosing why Phan or a plugin is slow or not responding) + kill -USR1 can be used to print a backtrace and continue running. + kill -USR2 can be used to print a backtrace, plus values of parameters, and continue running. + + --baseline-summary-type={ordered_by_count,ordered_by_type,none} + Configures the summary comment generated by --save-baseline. Does not affect analysis. + + --language-server-on-stdin + Start the language server (For the Language Server protocol). + This is a different protocol from --daemonize, clients for various IDEs already exist. + + --language-server-tcp-server + Start the language server listening for TCP connections on (e.g. 127.0.0.1:) + + --language-server-tcp-connect + Start the language server and connect to the client listening on (e.g. 127.0.0.1:) + + --language-server-analyze-only-on-save + Prevent the client from sending change notifications (Only notify the language server when the user saves a document) + This significantly reduces CPU usage, but clients won't get notifications about issues immediately. + + --language-server-disable-go-to-definition, --language-server-enable-go-to-definition + Disables/Enables support for "Go To Definition" and "Go To Type Definition" in the Phan Language Server. + Enabled by default. + + --language-server-disable-hover, --language-server-enable-hover + Disables/Enables support for "Hover" in the Phan Language Server. + Enabled by default. + + --language-server-disable-completion, --language-server-enable-completion + Disables/Enables support for "Completion" in the Phan Language Server. + Enabled by default. + + --language-server-completion-vscode + Adds a workaround to make completion of variables and static properties + that are compatible with language clients such as VS Code. + + --language-server-verbose + Emit verbose logging messages related to the language server implementation to stderr. + This is useful when developing or debugging language server clients. + + --language-server-disable-output-filter + Emit all issues detected from the language server (e.g. invalid phpdoc in parsed files), + not just issues in files currently open in the editor/IDE. + This can be very verbose and has more false positives. + + This is useful when developing or debugging language server clients. + + --language-server-allow-missing-pcntl + No-op (This is the default behavior). + Allow the fallback that doesn't use pcntl (New and experimental) to be used if the pcntl extension is not installed. + This is useful for running the language server on Windows. + + --language-server-hide-category + Remove the Phan issue category from diagnostic messages. + Makes issue messages slightly shorter. + + --language-server-force-missing-pcntl + Force Phan to use the fallback for when pcntl is absent (New and experimental). Useful for debugging that fallback. + + --language-server-require-pcntl + Don't start the language server if PCNTL isn't installed (don't use the fallback). Useful for debugging. + + --language-server-min-diagnostics-delay-ms <0..1000> + Sets a minimum delay between publishing diagnostics (i.e. Phan issues) to the language client. + This can be increased to work around race conditions in clients processing Phan issues (e.g. if your editor/IDE shows outdated diagnostics) + Defaults to 0. (no delay) + + --native-syntax-check + If php_binary (e.g. `php72`, `/usr/bin/php`) can be found in `\$PATH`, enables `InvokePHPNativeSyntaxCheckPlugin` + and adds `php_binary` (resolved using `\$PATH`) to the `php_native_syntax_check_binaries` array of `plugin_config` + (treated here as initially being the empty array) + Phan exits if any php binary could not be found. + + This can be repeated to run native syntax checks with multiple php versions. + + --require-config-exists + Exit immediately with an error code if `.phan/config.php` does not exist. + + --help-annotations + Print details on annotations supported by Phan. + +EOB + , + $forbid_color + ); + } + if ($exit_code === null) { + return; + } + exit($exit_code); + } + + /** + * Prints a warning to stderr (except for the label, nothing else is colorized). + * This clears the progress bar if needed. + * + * NOTE: Callers should usually add a trailing newline. + */ + public static function printWarningToStderr(string $message): void + { + self::printToStderr(self::colorizeHelpSectionIfSupported('WARNING: ') . $message); + } + + /** + * Prints an error to stderr (except for the label, nothing else is colorized). + * This clears the progress bar if needed. + * + * NOTE: Callers should usually add a trailing newline. + */ + public static function printErrorToStderr(string $message): void + { + self::printToStderr(self::colorizeHelpSectionIfSupported('ERROR: ') . $message); + } + + /** + * Prints to stderr, clearing the progress bar if needed. + * NOTE: Callers should usually add a trailing newline. + */ + public static function printToStderr(string $message): void + { + if (self::shouldClearStderrBeforePrinting()) { + // http://ascii-table.com/ansi-escape-sequences.php + // > Clears all characters from the cursor position to the end of the line (including the character at the cursor position). + $message = "\033[2K" . $message; + } + if (\defined('STDERR')) { + fwrite(STDERR, $message); + } else { + // Fallback in case Phan runs interactively or in non-CLI SAPIs. + // This is incomplete. + echo $message; + } + } + + /** + * Check if the progress bar should be cleared. + */ + private static function shouldClearStderrBeforePrinting(): bool + { + // Don't clear if a regular progress bar isn't being rendered. + if (!CLI::shouldShowProgress()) { + return false; + } + if (CLI::shouldShowLongProgress() || CLI::shouldShowDebugOutput()) { + return false; + } + // @phan-suppress-next-line PhanUndeclaredFunction + if (\function_exists('sapi_windows_vt100_support') && !\sapi_windows_vt100_support(STDERR)) { + return false; + } + return true; + } + + /** + * Prints a section of the help or usage message to stdout. + * @internal + */ + public static function printHelpSection(string $section, bool $forbid_color = false, bool $toStderr = false): void + { + if (!$forbid_color) { + $section = self::colorizeHelpSectionIfSupported($section); + } + if ($toStderr) { + CLI::printToStderr($section); + } else { + echo $section; + } + } + + /** + * Add ansi color codes to the CLI flags included in the --help or --extended-help message, + * but only if the CLI/config flags and environment supports it. + */ + public static function colorizeHelpSectionIfSupported(string $section): string + { + if (Config::getValue('color_issue_messages') ?? (!self::hasNoColorEnv() && self::supportsColor(\STDOUT))) { + $section = self::colorizeHelpSection($section); + } + return $section; + } + + /** + * Add ansi color codes to the CLI flags included in the --help or --extended-help message. + */ + public static function colorizeHelpSection(string $section): string + { + $colorize_flag_cb = /** @param list $match */ static function (array $match): string { + [$_, $prefix, $cli_flag, $suffix] = $match; + $colorized_cli_flag = Colorizing::colorizeTextWithColorCode(Colorizing::STYLES['green'], $cli_flag); + return $prefix . $colorized_cli_flag . $suffix; + }; + $long_flag_regex = '(()((?:--)(?:' . \implode('|', array_map(static function (string $option): string { + return \preg_quote(\rtrim($option, ':')); + }, self::GETOPT_LONG_OPTIONS)) . '))([^\w-]|$))'; + $section = \preg_replace_callback($long_flag_regex, $colorize_flag_cb, $section); + $short_flag_regex = '((\s|\b|\')(-[' . \str_replace(':', '', self::GETOPT_SHORT_OPTIONS) . '])([^\w-]))'; + + $section = \preg_replace_callback($short_flag_regex, $colorize_flag_cb, $section); + + $colorize_opt_cb = /** @param list $match */ static function (array $match): string { + $cli_flag = $match[0]; + return Colorizing::colorizeTextWithColorCode(Colorizing::STYLES['yellow'], $cli_flag); + }; + $section = \preg_replace_callback('@<\S+>|\{\S+\}@', $colorize_opt_cb, $section); + $section = \preg_replace('@^ERROR:@', Colorizing::colorizeTextWithColorCode(Colorizing::STYLES['light_red'], '\0'), $section); + $section = \preg_replace('@^WARNING:@', Colorizing::colorizeTextWithColorCode(Colorizing::STYLES['yellow'], '\0'), $section); + return $section; + } + + /** + * Finds potentially misspelled flags and returns them as a string + * + * This will use levenshtein distance, showing the first one or two flags + * which match with a distance of <= 5 + * + * @param string $key Misspelled key to attempt to correct + * @internal + */ + public static function getFlagSuggestionString( + string $key + ): string { + $trim = static function (string $s): string { + return \rtrim($s, ':'); + }; + $generate_suggestion = static function (string $suggestion): string { + return (strlen($suggestion) === 1 ? '-' : '--') . $suggestion; + }; + $generate_suggestion_text = static function (string $suggestion, string ...$other_suggestions) use ($generate_suggestion): string { + $suggestions = \array_merge([$suggestion], $other_suggestions); + return ' (did you mean ' . \implode(' or ', array_map($generate_suggestion, $suggestions)) . '?)'; + }; + $short_options = \array_filter(array_map($trim, \str_split(self::GETOPT_SHORT_OPTIONS))); + if (strlen($key) === 1) { + if (in_array($key, $short_options, true)) { + return $generate_suggestion_text($key); + } + $alternate = \ctype_lower($key) ? \strtoupper($key) : \strtolower($key); + if (in_array($alternate, $short_options, true)) { + return $generate_suggestion_text($alternate); + } + return ''; + } elseif ($key === '') { + return ''; + } elseif (strlen($key) > 255) { + // levenshtein refuses to compute for longer strings + return ''; + } + // include short options in case a typo is made like -aa instead of -a + $known_flags = \array_merge(self::GETOPT_LONG_OPTIONS, $short_options); + + $known_flags = array_map($trim, $known_flags); + + $similarities = []; + + $key_lower = \strtolower($key); + foreach ($known_flags as $flag) { + if (strlen($flag) === 1 && \stripos($key, $flag) === false) { + // Skip over suggestions of flags that have no common characters + continue; + } + $distance = \levenshtein($key_lower, \strtolower($flag)); + // distance > 5 is too far off to be a typo + // Make sure that if two flags have the same distance, ties are sorted alphabetically + if ($distance > 5) { + continue; + } + if ($key === $flag) { + if (in_array($key . ':', self::GETOPT_LONG_OPTIONS, true)) { + return " (This option is probably missing the required value. Or this option may not apply to a regular Phan analysis, and/or it may be unintentionally unhandled in \Phan\CLI::__construct())"; + } else { + return " (This option may not apply to a regular Phan analysis, and/or it may be unintentionally unhandled in \Phan\CLI::__construct())"; + } + } + $similarities[$flag] = [$distance, "x" . \strtolower($flag), $flag]; + } + + \asort($similarities); // retain keys and sort descending + $similarity_values = \array_values($similarities); + + if (count($similarity_values) >= 2 && ($similarity_values[1][0] <= $similarity_values[0][0] + 1)) { + // If the next-closest suggestion isn't close to as similar as the closest suggestion, just return the closest suggestion + return $generate_suggestion_text($similarity_values[0][2], $similarity_values[1][2]); + } elseif (count($similarity_values) >= 1) { + return $generate_suggestion_text($similarity_values[0][2]); + } + return ''; + } + + /** + * Checks if a file (not a folder) which has potentially not yet been created on disk should be parsed. + * @param string $file_path a relative path to a file within the project + */ + public static function shouldParse(string $file_path): bool + { + $exclude_file_regex = Config::getValue('exclude_file_regex'); + if ($exclude_file_regex && self::isPathMatchedByRegex($exclude_file_regex, $file_path)) { + return false; + } + $file_extensions = Config::getValue('analyzed_file_extensions'); + + if (!\is_array($file_extensions) || count($file_extensions) === 0) { + return false; + } + $extension = \pathinfo($file_path, \PATHINFO_EXTENSION); + if (!is_string($extension) || !in_array($extension, $file_extensions, true)) { + return false; + } + + $directory_regex = Config::getValue('__directory_regex'); + return $directory_regex && \preg_match($directory_regex, $file_path) > 0; + } + + /** + * @param string $directory_name + * The name of a directory to scan for files ending in `.php`. + * + * @return list + * A list of PHP files in the given directory + * + * @throws InvalidArgumentException + * if there is nothing to analyze + */ + private static function directoryNameToFileList( + string $directory_name + ): array { + $file_list = []; + + try { + $file_extensions = Config::getValue('analyzed_file_extensions'); + + if (!\is_array($file_extensions) || count($file_extensions) === 0) { + throw new InvalidArgumentException( + 'Empty list in config analyzed_file_extensions. Nothing to analyze.' + ); + } + + $exclude_file_regex = Config::getValue('exclude_file_regex'); + $filter_folder_or_file = /** @param mixed $unused_key */ static function (SplFileInfo $file_info, $unused_key, \RecursiveIterator $iterator) use ($file_extensions, $exclude_file_regex): bool { + try { + if (\in_array($file_info->getBaseName(), ['.', '..'], true)) { + // Exclude '.' and '..' + return false; + } + if ($file_info->isDir()) { + if (!$iterator->hasChildren()) { + return false; + } + // Compare exclude_file_regex against the relative path of the folder within the project + // (E.g. src/subfolder/) + if ($exclude_file_regex && self::isPathMatchedByRegex($exclude_file_regex, $file_info->getPathname() . '/')) { + // E.g. for phan itself, excludes vendor/psr/log/Psr/Log/Test and vendor/symfony/console/Tests + return false; + } + + return true; + } + + if (!in_array($file_info->getExtension(), $file_extensions, true)) { + return false; + } + if (!$file_info->isFile()) { + // Handle symlinks to invalid real paths + $file_path = $file_info->getRealPath() ?: $file_info->__toString(); + CLI::printErrorToStderr("Unable to read file $file_path: SplFileInfo->isFile() is false for SplFileInfo->getType() == " . \var_export(self::getSplFileInfoType($file_info), true) . "\n"); + return false; + } + if (!$file_info->isReadable()) { + $file_path = $file_info->getRealPath(); + CLI::printErrorToStderr("Unable to read file $file_path: SplFileInfo->isReadable() is false, getPerms()=" . \sprintf("%o(octal)", @$file_info->getPerms()) . "\n"); + return false; + } + + // Compare exclude_file_regex against the relative path within the project + // (E.g. src/foo.php) + if ($exclude_file_regex && self::isPathMatchedByRegex($exclude_file_regex, $file_info->getPathname())) { + return false; + } + } catch (Exception $e) { + CLI::printErrorToStderr(\sprintf("Unexpected error checking if %s should be parsed: %s %s\n", $file_info->getPathname(), \get_class($e), $e->getMessage())); + return false; + } + + return true; + }; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveCallbackFilterIterator( + new \RecursiveDirectoryIterator( + $directory_name, + \RecursiveDirectoryIterator::FOLLOW_SYMLINKS + ), + $filter_folder_or_file + ) + ); + + $file_list = \array_keys(\iterator_to_array($iterator)); + } catch (Exception $exception) { + CLI::printWarningToStderr("Caught exception while listing files in '$directory_name': {$exception->getMessage()}\n"); + } + + // Normalize leading './' in paths. + $normalized_file_list = []; + foreach ($file_list as $file_path) { + $file_path = \preg_replace('@^(\.[/\\\\]+)+@', '', $file_path); + // Treat src/file.php and src//file.php and src\file.php the same way + $normalized_file_list[\preg_replace("@[/\\\\]+@", "\0", $file_path)] = $file_path; + } + \uksort($normalized_file_list, 'strcmp'); + return \array_values($normalized_file_list); + } + + private static function getSplFileInfoType(SplFileInfo $info): string + { + try { + return @$info->getType(); + } catch (Exception $e) { + return "(unknown: {$e->getMessage()})"; + } + } + + /** + * Returns true if the progress bar was requested and it makes sense to display. + */ + public static function shouldShowProgress(): bool + { + return (Config::getValue('progress_bar') || Config::getValue('debug_output')) && + !Config::getValue('dump_ast') && + !self::isDaemonOrLanguageServer(); + } + + /** + * Returns true if the long version of the progress bar should be shown. + * Precondition: shouldShowProgress is true. + */ + public static function shouldShowLongProgress(): bool + { + return Config::getValue('__long_progress_bar'); + } + + /** + * Returns true if this is a daemon or language server responding to requests + */ + public static function isDaemonOrLanguageServer(): bool + { + return Config::getValue('daemonize_tcp') || + Config::getValue('daemonize_socket') || + Config::getValue('language_server_config'); + } + + /** + * Should this show --debug output + */ + public static function shouldShowDebugOutput(): bool + { + return Config::getValue('debug_output') && !self::isDaemonOrLanguageServer(); + } + + /** + * Check if a path name is excluded by regex, in a platform independent way. + * Normalizes $path_name on Windows so that '/' is always the directory separator. + * + * @param string $exclude_file_regex - PHP regex + * @param string $path_name - path name within project, beginning with user-provided directory name. + * On windows, may contain '\'. + * + * @return bool - True if the user's configured regex is meant to exclude $path_name + */ + public static function isPathMatchedByRegex( + string $exclude_file_regex, + string $path_name + ): bool { + // Make this behave the same way on Linux/Unix and on Windows. + if (DIRECTORY_SEPARATOR === '\\') { + $path_name = \str_replace(DIRECTORY_SEPARATOR, '/', $path_name); + } + return \preg_match($exclude_file_regex, $path_name) > 0; + } + + // Bound the percentage to [0, 1] + private static function boundPercentage(float $p): float + { + return \min(\max($p, 0.0), 1.0); + } + + /** + * Update a progress bar on the screen + * + * @param string $msg + * A short message to display with the progress + * meter + * + * @param float $p + * The percentage to display + * + * @param ?(string|FQSEN|AddressableElement) $details + * Details about what is being analyzed within the phase for $msg + * + * @param ?int $offset + * The index of this event in the list of events that will be emitted + * + * @param ?int $count + * The number of events in the list. + * This is constant for 'parse' and 'analyze' phases, but may change for other phases. + */ + public static function progress( + string $msg, + float $p, + $details = null, + ?int $offset = null, + ?int $count = null + ): void { + if ($msg !== self::$current_progress_state_any) { + self::$current_progress_state_any = $msg; + Type::handleChangeCurrentProgressState($msg); + } + if (self::shouldShowDebugOutput()) { + self::debugProgress($msg, $p, $details); + return; + } + if (!self::shouldShowProgress()) { + return; + } + // Bound the percentage to [0, 1] + $p = self::boundPercentage($p); + + static $previous_update_time = 0.0; + $time = \microtime(true); + + // If not enough time has elapsed, then don't update the progress bar. + // Making the update frequency based on time (instead of the number of files) + // prevents the terminal from rapidly flickering while processing small files. + if ($time - $previous_update_time < Config::getValue('progress_bar_sample_interval')) { + // Make sure to output 100% for all phases, to avoid confusion. + // https://github.com/phan/phan/issues/2694 + // e.g. `tool/phoogle --progress-bar` will stop partially through the 'method' phase otherwise. + if ($p < 1.0) { + return; + } + } + $previous_update_time = $time; + if ($msg === 'analyze' && Writer::isForkPoolWorker()) { + // The original process of the fork pool is responsible for rendering the combined progress. + Writer::recordProgress($p, (int)$offset, (int)$count); + return; + } + $memory = \memory_get_usage() / 1024 / 1024; + $peak = \memory_get_peak_usage() / 1024 / 1024; + + self::outputProgressLine($msg, $p, $memory, $peak, $offset, $count); + } + + /** @var ?string the current state of CLI::progress, with any progress bar */ + private static $current_progress_state_any = null; + + /** @var ?string the state for long progress */ + private static $current_progress_state_long_progress = null; + /** @var int the number of events that were handled */ + private static $current_progress_offset_long_progress = 0; + + // 80 - strlen(' 9999 / 9999 (100%) 9999MB') == 54 + private const PROGRESS_WIDTH = 54; + + /** + * Returns the number of columns in the terminal + */ + private static function getColumns(): int + { + static $columns = null; + if ($columns === null) { + // Only call this once per process, since it can be rather expensive + $columns = (new Terminal())->getWidth(); + } + return $columns; + } + + /** + * @internal + */ + public static function outputProgressLine(string $msg, float $p, float $memory, float $peak, ?int $offset = null, ?int $count = null): void + { + if (self::shouldShowLongProgress()) { + self::showLongProgress($msg, $p, $memory, $offset, $count); + return; + } + + $columns = self::getColumns(); + $left_side = \str_pad($msg, 10, ' ', STR_PAD_LEFT) . ' '; + if ($columns - (60 + 10) > 19) { + $percent_progress = \sprintf('%1$ 5.1f', (int)(1000 * $p) / 10); + } else { + $percent_progress = \sprintf("%1$ 3d", (int)(100 * $p)); + } + // Don't make the current memory usage in the progress bar shorter (avoid showing "MBB") + $width = \max(2, strlen((string)(int)$peak)); + $right_side = + " " . $percent_progress . "%" . + \sprintf(' %' . $width . 'dMB/%' . $width . 'dMB', (int)$memory, (int)$peak); + // @phan-suppress-previous-line PhanPluginPrintfVariableFormatString + + // strlen(" 99% 999MB/999MB") == 17 + $used_length = strlen($left_side) + \max(17, strlen($right_side)); + $remaining_length = $columns - $used_length; + $remaining_length = \min(60, \max(0, $remaining_length)); + if ($remaining_length > 0) { + $progress_bar = self::renderInnerProgressBar($remaining_length, $p); + } else { + $progress_bar = ''; + $right_side = \ltrim($right_side); + } + + // Build up a string, then make a single call to fwrite(). Should be slightly faster and smoother to render to the console. + $msg = "\r" . + $left_side . + $progress_bar . + $right_side . + "\r"; + fwrite(STDERR, $msg); + } + + /** + * Print an end to progress bars or debug output + */ + public static function endProgressBar(): void + { + static $did_end = false; + if ($did_end) { + // Overkill as a sanity check + return; + } + $did_end = true; + if (self::shouldShowDebugOutput()) { + fwrite(STDERR, "Phan's analysis is complete\n"); + return; + } + if (self::shouldShowProgress()) { + // Print a newline to stderr to visuall separate stderr from stdout + fwrite(STDERR, PHP_EOL); + \fflush(\STDOUT); + } + } + + /** + * @param ?(string|FQSEN|AddressableElement) $details + */ + public static function debugProgress(string $msg, float $p, $details): void + { + $pct = \sprintf("%d%%", (int)(100 * self::boundPercentage($p))); + + if ($details === null) { + return; + } + if ($details instanceof AddressableElement) { + $details = $details->getFQSEN(); + } + switch ($msg) { + case 'parse': + case 'analyze': + $line = "Going to $msg '$details' ($pct)"; + break; + case 'method': + case 'function': + $line = "Going to analyze $msg $details() ($pct)"; + break; + default: + $line = "In $msg phase, processing '$details' ($pct)"; + break; + } + self::debugOutput($line); + } + + /** + * Write a line of output for debugging. + */ + public static function debugOutput(string $line): void + { + if (self::shouldShowDebugOutput()) { + fwrite(STDERR, $line . PHP_EOL); + } + } + + /** + * Renders a unicode progress bar that goes from light (left) to dark (right) + * The length in the console is the positive integer $length + * @see https://en.wikipedia.org/wiki/Block_Elements + */ + private static function renderInnerProgressBar(int $length, float $p): string + { + $current_float = $p * $length; + $current = (int)$current_float; + $rest = \max($length - $current, 0); + + if (!self::doesTerminalSupportUtf8()) { + // Show a progress bar of "XXXX>------" in Windows when utf-8 is unsupported. + $progress_bar = str_repeat("X", $current); + $delta = $current_float - $current; + if ($delta > 0.5) { + $progress_bar .= ">" . str_repeat("-", $rest - 1); + } else { + $progress_bar .= str_repeat("-", $rest); + } + return $progress_bar; + } + // The left-most characters are "Light shade" + $progress_bar = str_repeat("\u{2588}", $current); + $delta = $current_float - $current; + if ($delta > 1.0 / 3) { + // The between character is "Full block" or "Medium shade" or "solid shade". + // The remaining characters on the right are "Full block" (darkest) + $first = $delta > 2.0 / 3 ? "\u{2593}" : "\u{2592}"; + $progress_bar .= $first . str_repeat("\u{2591}", $rest - 1); + } else { + $progress_bar .= str_repeat("\u{2591}", $rest); + } + return self::colorizeProgressBarSegment($progress_bar); + } + + private static function colorizeProgressBarSegment(string $segment): string + { + if ($segment === '') { + return ''; + } + $progress_bar_color = $_ENV['PHAN_COLOR_PROGRESS_BAR'] ?? ''; + if ($progress_bar_color !== '' && CLI::supportsColor(STDERR)) { + $progress_bar_color = Colorizing::STYLES[\strtolower($progress_bar_color)] ?? $progress_bar_color; + return Colorizing::colorizeTextWithColorCode($progress_bar_color, $segment); + } + return $segment; + } + + /** + * Shows a long version of the progress bar, suitable for Continuous Integration logs + */ + private static function showLongProgress(string $msg, float $p, float $memory, ?int $offset, ?int $count): void + { + $buf = self::renderLongProgress($msg, $p, $memory, $offset, $count); + // Do a single write call (more efficient than multiple calls) + if (strlen($buf) > 0) { + fwrite(STDERR, $buf); + } + } + + /** + * Reset the long progress state to the initial state. + * + * Useful for --analyze-twice + */ + public static function resetLongProgressState(): void + { + self::$current_progress_offset_long_progress = 0; + self::$current_progress_state_long_progress = null; + } + + private static function renderLongProgress(string $msg, float $p, float $memory, ?int $offset, ?int $count): string + { + $buf = ''; + if ($msg !== self::$current_progress_state_long_progress) { + switch ($msg) { + case 'parse': + $buf = "Parsing files..." . PHP_EOL; + break; + case 'classes': + $buf = "Analyzing classes..." . PHP_EOL; + break; + case 'function': + $buf = "Analyzing functions..." . PHP_EOL; + break; + case 'method': + $buf = "Analyzing methods..." . PHP_EOL; + break; + case 'analyze': + static $did_print = false; + if ($did_print) { + $buf = "Analyzing files a second time..." . PHP_EOL; + } else { + $buf = "Analyzing files..." . PHP_EOL; + $did_print = true; + } + break; + case 'dead code': + $buf = "Checking for dead code..." . PHP_EOL; + break; + default: + $buf = "In '$msg' phase\n"; + } + self::$current_progress_state_long_progress = $msg; + self::$current_progress_offset_long_progress = 0; + } + if (self::doesTerminalSupportUtf8()) { + $chr = "\u{2591}"; + } else { + $chr = "."; + } + if (in_array($msg, ['analyze', 'parse'], true)) { + while (self::$current_progress_offset_long_progress < $offset) { + $old_mod = self::$current_progress_offset_long_progress % self::PROGRESS_WIDTH; + $len = (int) min($offset - self::$current_progress_offset_long_progress, self::PROGRESS_WIDTH - $old_mod); + if (!$len) { + // impossible + break; + } + + $buf .= self::colorizeProgressBarSegment(str_repeat($chr, $len)); + self::$current_progress_offset_long_progress += $len; + $mod = self::$current_progress_offset_long_progress % self::PROGRESS_WIDTH; + if ($mod === 0 || self::$current_progress_offset_long_progress === $count) { + if ($mod) { + $buf .= str_repeat(" ", self::PROGRESS_WIDTH - $mod); + } + // @phan-suppress-next-line PhanPluginPrintfVariableFormatString + $buf .= " " . \sprintf( + "%" . strlen((string)(int)$count) . "d / %d (%3d%%) %.0fMB" . PHP_EOL, + min(self::$current_progress_offset_long_progress, $count), + (int)$count, + 100 * $p, + $memory + ); + } + } + } else { + $offset = (int)($p * self::PROGRESS_WIDTH); + if (self::$current_progress_offset_long_progress < $offset) { + $buf .= self::colorizeProgressBarSegment(str_repeat($chr, $offset - self::$current_progress_offset_long_progress)); + self::$current_progress_offset_long_progress = $offset; + if (self::$current_progress_offset_long_progress === self::PROGRESS_WIDTH) { + $buf .= ' ' . \sprintf("%.0fMB" . PHP_EOL, $memory); + } + } + } + return $buf; + } + + /** + * Guess if the terminal supports utf-8. + * In some locales, windows is set to a non-utf-8 codepoint. + * + * @see https://github.com/phan/phan/issues/2572 + * @see https://en.wikipedia.org/wiki/Code_page#Windows_code_pages + * @suppress PhanUndeclaredFunction, UnusedSuppression the function exists only in Windows. + * @suppress PhanImpossibleTypeComparison, PhanRedundantCondition, PhanImpossibleCondition, PhanSuspiciousValueComparison the value for strtoupper is inferred as a literal. + */ + public static function doesTerminalSupportUtf8(): bool + { + if (getenv('PHAN_NO_UTF8')) { + return false; + } + if (\PHP_OS_FAMILY === 'Windows') { + if (!\function_exists('sapi_windows_cp_is_utf8') || !\sapi_windows_cp_is_utf8()) { + return false; + } + } + return true; + } + + /** + * Look for a `.phan/config` file up to a few directories + * up the hierarchy and apply anything in there to + * the configuration. + * @throws UsageException + */ + private function maybeReadConfigFile(bool $require_config_exists): void + { + + // If the file doesn't exist here, try a directory up + $config_file_name = $this->config_file; + $config_file_name = + StringUtil::isNonZeroLengthString($config_file_name) + ? \realpath($config_file_name) + : \implode(DIRECTORY_SEPARATOR, [ + Config::getProjectRootDirectory(), + '.phan', + 'config.php' + ]); + + // Totally cool if the file isn't there + if ($config_file_name === false || !\file_exists($config_file_name)) { + if ($require_config_exists) { + // But if the CLI option --require-config-exists is provided, exit immediately. + // (Include extended help documenting that option) + if ($config_file_name !== false) { + throw new UsageException("Could not find a config file at '$config_file_name', but --require-config-exists was set", EXIT_FAILURE, UsageException::PRINT_EXTENDED); + } else { + $msg = \sprintf( + "Could not figure out the path for config file %s, but --require-config-exists was set", + StringUtil::encodeValue($this->config_file) + ); + throw new UsageException($msg, EXIT_FAILURE, UsageException::PRINT_EXTENDED); + } + } + return; + } + + // Read the configuration file + $config = require($config_file_name); + + // Write each value to the config + foreach ($config as $key => $value) { + Config::setValue($key, $value); + } + } + + /** + * This will assert that ast\parse_code or a polyfill can be called. + * @throws AssertionError on failure + */ + private static function ensureASTParserExists(): void + { + if (Config::getValue('use_polyfill_parser')) { + return; + } + if (!\extension_loaded('ast')) { + self::printHelpSection( + // phpcs:ignore Generic.Files.LineLength.MaxExceeded + "ERROR: The php-ast extension must be loaded in order for Phan to work. Either install and enable php-ast, or invoke Phan with the CLI option --allow-polyfill-parser (which is noticeably slower)\n", + false, + true + ); + \phan_output_ast_installation_instructions(); + exit(EXIT_FAILURE); + } + self::sanityCheckAstVersion(); + + try { + // Split up the opening PHP tag to fix highlighting in vim. + \ast\parse_code( + '<' . '?php 42;', + Config::AST_VERSION + ); + } catch (\LogicException $_) { + self::printHelpSection( + 'ERROR: Unknown AST version (' + . Config::AST_VERSION + . ') in configuration. ' + . "You may need to rebuild the latest version of the php-ast extension.\n" + . "See https://github.com/phan/phan#getting-started for more details.\n" + . "(You are using php-ast " . (new ReflectionExtension('ast'))->getVersion() . ", but " . Config::MINIMUM_AST_EXTENSION_VERSION . " or newer is required. Alternately, test with --force-polyfill-parser (which is noticeably slower))\n", + false, + true + ); + exit(EXIT_FAILURE); + } + + // Workaround for https://github.com/nikic/php-ast/issues/79 + try { + \ast\parse_code( + '<' . '?php syntaxerror', + Config::AST_VERSION + ); + self::printHelpSection( + 'ERROR: Expected ast\\parse_code to throw ParseError on invalid inputs. Configured AST version: ' + . Config::AST_VERSION + . '. ' + . "You may need to rebuild the latest version of the php-ast extension.\n", + false, + true + ); + exit(EXIT_FAILURE); + } catch (\ParseError $_) { + // error message may validate with locale and version, don't validate that. + } + } + + /** + * This duplicates the check in Bootstrap.php, in case opcache.file_cache has outdated information about whether extension_loaded('ast') is true. exists. + */ + private static function sanityCheckAstVersion(): void + { + $ast_version = (string)\phpversion('ast'); + if (\version_compare($ast_version, '1.0.0') <= 0) { + if ($ast_version === '') { + // Seen in php 7.3 with file_cache when ast is initially enabled but later disabled, due to the result of extension_loaded being assumed to be a constant by opcache. + fwrite(STDERR, "ERROR: extension_loaded('ast') is true, but phpversion('ast') is the empty string. You probably need to clear opcache (opcache.file_cache='" . \ini_get('opcache.file_cache') . "')" . PHP_EOL); + } + // TODO: Change this to a warning for 0.1.5 - 1.0.0. (https://github.com/phan/phan/issues/2954) + // 0.1.5 introduced the ast\Node constructor, which is required by the polyfill + // + // NOTE: We haven't loaded the autoloader yet, so these issue messages can't be colorized. + \fprintf( + STDERR, + "ERROR: Phan 3.x requires php-ast 1.0.1+ because it depends on AST version 70. php-ast '%s' is installed." . PHP_EOL, + $ast_version + ); + require_once __DIR__ . '/Bootstrap.php'; + \phan_output_ast_installation_instructions(); + \fwrite(STDERR, "Exiting without analyzing files." . PHP_EOL); + exit(1); + } + } + + /** + * Returns a string that can be used to check if dev-master versions changed (approximately). + * + * This is useful for checking if caches (e.g. of ASTs) should be invalidated. + */ + public static function getDevelopmentVersionId(): string + { + $news_path = \dirname(__DIR__) . '/NEWS.md'; + $version = self::PHAN_VERSION; + if (\file_exists($news_path)) { + $version .= '-' . \filesize($news_path); + } + return $version; + } + + /** + * If any problematic extensions are installed, then restart without them + * @suppress PhanAccessMethodInternal + */ + public function restartWithoutProblematicExtensions(): void + { + $extensions_to_disable = []; + if (self::shouldRestartToExclude('xdebug')) { + $extensions_to_disable[] = 'xdebug'; + // Restart if Xdebug is loaded, unless the environment variable PHAN_ALLOW_XDEBUG is set. + if (!getenv('PHAN_DISABLE_XDEBUG_WARN')) { + fwrite(STDERR, <<setLogger(new StderrLogger()); + foreach ($extensions_to_disable as $extension) { + $ini_handler->disableExtension($extension); + } + // Automatically restart if problematic extensions are loaded + $ini_handler->check(); + } + } + + private static function shouldRestartToExclude(string $extension): bool + { + return \extension_loaded($extension) && !getenv('PHAN_ALLOW_' . \strtoupper($extension)); + } + + private static function willUseMultipleProcesses(): bool + { + if (Config::getValue('processes') > 1) { + return true; + } + if (Config::getValue('language_server_use_pcntl_fallback')) { + return false; + } + $config = Config::getValue('language_server_config'); + if ($config && !isset($config['stdin'])) { + return true; + } + if (Config::getValue('daemonize_tcp')) { + return true; + } + return false; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/CLIBuilder.php b/bundled-libs/phan/phan/src/Phan/CLIBuilder.php new file mode 100644 index 000000000..e2fc80b0e --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/CLIBuilder.php @@ -0,0 +1,77 @@ + */ + private $opts = []; + /** @var list */ + private $argv = []; + + public function __construct() + { + } + + /** + * Set an option and return $this. + * + * @param string|list|false $value + */ + public function setOption(string $opt, $value = false): self + { + $this->opts[$opt] = $value; + if (!\is_array($value)) { + $value = [$value]; + } + foreach ($value as $element) { + // Hardcode the option that there are no single-letter long options. + $opt_name = \strlen($opt) > 1 ? "--$opt" : "-$opt"; + $this->argv[] = $opt_name; + if (\is_int($element) || \is_string($element)) { + $this->argv[] = (string)$element; + } + } + return $this; + } + + /** + * Create and read command line arguments, configuring + * \Phan\Config as a side effect. + * + * @throws ExitException + * @throws UsageException + */ + public function build(): CLI + { + return CLI::fromRawValues($this->opts, $this->argv); + } + + /** + * Return options in the same format as the getopt() call returns. + * @return associative-array + */ + public function getOpts(): array + { + return $this->opts; + } + + /** + * Return an array of arguments that correspond to what would cause getopt() to return $this->getOpts(). + * @return list + */ + public function getArgv(): array + { + return $this->argv; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/CodeBase.php b/bundled-libs/phan/phan/src/Phan/CodeBase.php new file mode 100644 index 000000000..062fa2f49 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/CodeBase.php @@ -0,0 +1,2380 @@ + + * A map from FQSEN to an internal or user defined class + * + * TODO: Improve Phan's self-analysis, allow the shorthand array access set syntax to be used without making bad inferences + * (e.g. $this->fqsen_class_map[$fqsen] = $clazz; + */ + private $fqsen_class_map; + + /** + * @var Map + * A map from FQSEN to a user defined class + */ + private $fqsen_class_map_user_defined; + + /** + * @var Map + * A map from FQSEN to an internal class + */ + private $fqsen_class_map_internal; + + /** + * @var Map + * A map from FQSEN to a ReflectionClass + */ + private $fqsen_class_map_reflection; + + /** + * @var Map> + * A map from FQSEN to set of ClassAliasRecord objects + */ + private $fqsen_alias_map; + + /** + * @var Map + * A map from FQSEN to a global constant + */ + private $fqsen_global_constant_map; + + /** + * @var Map + * A map from FQSEN to function + */ + private $fqsen_func_map; + + /** + * @var Set + * A set of internal function FQSENs to lazily initialize. + * Entries are removed as new entries get added to fqsen_func_map. + */ + private $internal_function_fqsen_set; + + /** + * @var Set + * The set of all methods + */ + private $method_set; + + /** + * @var Map + * A map from FullyQualifiedClassName to a ClassMap, + * an object that holds properties, methods and class + * constants. + */ + private $class_fqsen_class_map_map; + + /** + * @var array> + * A map from a string method name to a Set of + * Methods + */ + private $name_method_map = []; + + /** + * @var array>>> + * Maps the file and namespace identifier to the use statements found in that namespace + */ + private $parsed_namespace_maps = []; + + /** + * @var array> + * Maps file paths to a set of file-level suppressions (E.g. 'PhanUnreferencedUseNormal', etc.) + * The corresponding value is the number of times the issue was suppressed + */ + private $file_level_suppression_set = []; + + /** + * @var bool + * If true, elements will be ensured to be hydrated + * on demand as they are requested. + */ + private $should_hydrate_requested_elements = false; + + /** + * @var UndoTracker|null - undoes the addition of global constants, classes, functions, and methods. + */ + private $undo_tracker; + + /** + * @var bool is the undo tracker currently enabled? + * + * If the Phan Language Server or Daemon Mode is enabled, + * the undo tracker will be enabled prior to the analysis phase, and disabled afterwards. + */ + private $has_enabled_undo_tracker = false; + + /** + * @var bool should Phan expect files contents for any path to be changed frequently + * (i.e. running as Daemon or the language server) + */ + private $expect_changes_to_file_contents = false; + + /** + * @var ?string (The currently parsed or analyzed file, if any. Used only for the crash reporting output) + */ + private static $current_file = null; + + /** + * Initialize a new CodeBase + * TODO: Remove internal_function_name_list completely? + * @param string[] $internal_class_name_list + * @param string[] $internal_interface_name_list + * @param string[] $internal_trait_name_list + * @param string[] $internal_constant_name_list + * @param string[] $internal_function_name_list + */ + public function __construct( + array $internal_class_name_list, + array $internal_interface_name_list, + array $internal_trait_name_list, + array $internal_constant_name_list, + array $internal_function_name_list + ) { + $this->fqsen_class_map = new Map(); + $this->fqsen_class_map_internal = new Map(); + $this->fqsen_class_map_reflection = new Map(); + $this->fqsen_class_map_user_defined = new Map(); + $this->fqsen_alias_map = new Map(); + $this->fqsen_global_constant_map = new Map(); + $this->fqsen_func_map = new Map(); + $this->class_fqsen_class_map_map = new Map(); + $this->method_set = new Set(); + $this->internal_function_fqsen_set = new Set(); + + // Add any pre-defined internal classes, interfaces, + // constants, traits and functions + $this->addClassesByNames($internal_class_name_list); + $this->addClassesByNames($internal_interface_name_list); + $this->addClassesByNames($internal_trait_name_list); + $this->addGlobalConstantsByNames($internal_constant_name_list); + // These are keywords that Phan expects to always exist - make sure to add them even if they weren't provided. + $this->addGlobalConstantsByNames(['true', 'false', 'null']); + // We initialize the FQSENs early on so that they show up + // in the proper casing. + $this->addInternalFunctionsByNames($internal_function_name_list); + } + + /** + * Start to enable the tracking of closures that can undo adding elements (class declarations, method declarations, etc.) + * to this codebase. + * + * This should only be called once, before the start of the parse phase. + */ + public function enableUndoTracking(): void + { + if ($this->has_enabled_undo_tracker) { + throw new \RuntimeException("Undo tracking already enabled"); + } + $this->has_enabled_undo_tracker = true; + $this->undo_tracker = new UndoTracker(); + } + + /** + * Start to disable the tracking of closures that can undo adding elements (class declarations, method declarations, etc.) + * to this codebase. + */ + public function disableUndoTracking(): void + { + if (!$this->has_enabled_undo_tracker) { + throw new \RuntimeException("Undo tracking was never enabled"); + } + $this->undo_tracker = null; + } + + /** + * @return bool is undo tracking enabled (i.e. are there closures that will revert the side effect of adding a file?) + */ + public function isUndoTrackingEnabled(): bool + { + return $this->undo_tracker !== null; + } + + /** + * Enable hydration of elements. (populating class elements with information from their ancestors) + * + * This is called after the parse phase is finished. + * + * - Prior to the end of the parse phase, ancestors of class elements would be unavailable, + * so hydration would result in an inconsistent state. + */ + public function setShouldHydrateRequestedElements( + bool $should_hydrate_requested_elements + ): void { + $this->should_hydrate_requested_elements = + $should_hydrate_requested_elements; + } + + /** + * Returns true if hydration of elements is enabled. + * This is called after the parse phase is finished. + */ + public function shouldHydrateRequestedElements(): bool + { + return $this->should_hydrate_requested_elements; + } + + /** + * @return list - The list of files which are successfully parsed. + * This changes whenever the file list is reloaded from disk. + * This also includes files which don't declare classes or functions or globals, + * because those files use classes/functions/constants. + * + * (This is the list prior to any analysis exclusion or whitelisting steps) + */ + public function getParsedFilePathList(): array + { + if ($this->undo_tracker) { + return $this->undo_tracker->getParsedFilePathList(); + } + throw new \RuntimeException("Calling getParsedFilePathList without an undo tracker"); + } + + /** + * @return int The size of $this->getParsedFilePathList() + */ + public function getParsedFilePathCount(): int + { + if ($this->undo_tracker) { + return $this->undo_tracker->getParsedFilePathCount(); + } + throw new \RuntimeException("Calling getParsedFilePathCount without an undo tracker"); + } + + /** + * Records the file currently being parsed/analyzed so that crash/error reports + * will indicate the analyzed file causing the error. + */ + public function setCurrentParsedFile(?string $current_parsed_file): void + { + self::$current_file = $current_parsed_file; + if ($this->undo_tracker) { + $this->undo_tracker->setCurrentParsedFile($current_parsed_file); + } + } + + /** + * Record that changes to file contents should be expected from now onwards, e.g. this is running as a language server or in daemon mode. + * + * E.g. this would disable caching ASTs of the polyfill/fallback to disk. + */ + public function setExpectChangesToFileContents(): void + { + $this->expect_changes_to_file_contents = true; + } + + /** + * Returns true if changes to file contents should be expected frequently. + * + * E.g. this is called to check if Phan should disable caching ASTs of the polyfill/fallback to disk. + */ + public function getExpectChangesToFileContents(): bool + { + return $this->expect_changes_to_file_contents; + } + + /** + * Sets the currently analyzed file, to improve Phan's crash reporting. + * @param string|null $current_analyzed_file + */ + public function setCurrentAnalyzedFile(?string $current_analyzed_file): void + { + self::$current_file = $current_analyzed_file; + } + + /** + * Returns the most recently parsed or analyzed file. + * @internal - For use only by the phan error handler, to help with debugging crashes + */ + public static function getMostRecentlyParsedOrAnalyzedFile(): ?string + { + return self::$current_file; + } + + /** + * Called when a file is unparsable. + * Removes the classes and functions, etc. from an older version of the file, if one exists. + */ + public function recordUnparsableFile(string $current_parsed_file): void + { + if ($this->undo_tracker) { + $this->undo_tracker->recordUnparsableFile($this, $current_parsed_file); + } + } + + /** + * @param string[] $class_name_list + * A list of class names to load type information for + */ + private function addClassesByNames(array $class_name_list): void + { + $included_extension_subset = self::getIncludedExtensionSubset(); + foreach ($class_name_list as $class_name) { + $reflection_class = new \ReflectionClass($class_name); + if ($reflection_class->isUserDefined()) { + continue; + } + if (is_array($included_extension_subset) && !isset($included_extension_subset[strtolower($reflection_class->getExtensionName() ?: '')])) { + // Allow preventing Phan from loading type information for a subset of extensions. + // This is useful if you have an extension installed locally (e.g. FFI, ast) but it won't be available in the target environment/php version. + continue; + } + // include internal classes, but not external classes such as composer + $this->addReflectionClass($reflection_class); + } + } + + /** + * @return ?array if non-null, the subset of extensions phan will limit the loading of reflection information to. + */ + private static function getIncludedExtensionSubset(): ?array + { + $included_extension_subset = Config::getValue('included_extension_subset'); + if (!is_array($included_extension_subset)) { + return null; + } + $map = [ + 'core' => true, + 'date' => true, + // 'hash' => true, // always enabled in 7.4.0, too new + // 'json' => true, // always enabled in 8.0.0, too new + 'pcre' => true, + 'reflection' => true, + 'spl' => true, + 'standard' => true, + ]; + foreach ($included_extension_subset as $name) { + if ($name === 'user') { + continue; + } + $map[strtolower($name)] = true; + } + return $map; + } + + /** + * @param string[] $const_name_list + * A list of global constant names to load type information for + */ + private function addGlobalConstantsByNames(array $const_name_list): void + { + $included_extension_subset = self::getIncludedExtensionSubset(); + if (is_array($included_extension_subset)) { + $excluded_constant_set = []; + foreach (get_defined_constants(true) as $ext_name => $constant_values) { + if (isset($included_extension_subset[strtolower($ext_name)])) { + continue; + } + foreach ($constant_values as $constant_name => $_) { + $excluded_constant_set[$constant_name] = true; + } + } + foreach ($const_name_list as $i => $const_name) { + if (isset($excluded_constant_set[$const_name])) { + unset($const_name_list[$i]); + } + } + } + foreach ($const_name_list as $const_name) { + // #1015 workaround for empty constant names ('' and '0'). + if (!\is_string($const_name)) { + // @phan-suppress-next-line PhanPluginRemoveDebugCall + \fprintf(STDERR, "Saw constant with non-string name of %s. There may be a bug in a PECL extension you are using (php -m will list those)\n", \var_export($const_name, true)); + continue; + } + try { + $const_obj = GlobalConstant::fromGlobalConstantName($const_name); + $this->addGlobalConstant($const_obj); + } catch (InvalidArgumentException | FQSENException $e) { + self::handleGlobalConstantException($const_name, $e); + } + } + } + + private static function handleGlobalConstantException(string $const_name, Exception $e): void + { + // Workaround for windows bug in #1011 + if (\strncmp($const_name, "\0__COMPILER_HALT_OFFSET__\0", 26) === 0) { + return; + } + // e.g. "\000apc_register_serializer-" APC_SERIALIZER_ABI + if (\strncmp($const_name, "\x00apc_", 5) === 0) { + return; + } + // @phan-suppress-next-line PhanPluginRemoveDebugCall + \fprintf(STDERR, "Failed to load global constant value for %s, continuing: %s\n", \var_export($const_name, true), $e->getMessage()); + } + + /** + * @param list $new_file_list + * @param array $file_mapping_contents maps relative path to absolute paths + * @param ?(string[]) $reanalyze_files files to re-analyze + * @return list - Subset of $new_file_list which changed on disk and has to be parsed again. Automatically unparses the old versions of files which were modified. + */ + public function updateFileList(array $new_file_list, array $file_mapping_contents = [], array $reanalyze_files = null): array + { + if ($this->undo_tracker) { + $this->invalidateDependentCacheEntries(); + + return $this->undo_tracker->updateFileList($this, $new_file_list, $file_mapping_contents, $reanalyze_files); + } + throw new \RuntimeException("Calling updateFileList without undo tracker"); + } + + /** + * @param string $file_name + * @return bool - true if caller should replace contents + */ + public function beforeReplaceFileContents(string $file_name): bool + { + if ($this->undo_tracker) { + $this->invalidateDependentCacheEntries(); + + return $this->undo_tracker->beforeReplaceFileContents($this, $file_name); + } + throw new \RuntimeException("Calling replaceFileContents without undo tracker"); + } + + /** + * Eagerly load all signatures. + * + * This is useful if we expect Phan to be running for a long time and forking processes (in language server or daemon mode), + * or if we need all of the signatures of functions (e.g. for tools that need all signatures) + */ + public function eagerlyLoadAllSignatures(): void + { + $this->getInternalClassMap(); // Force initialization of remaining internal php classes to reduce latency of future analysis requests. + $this->forceLoadingInternalFunctions(); // Force initialization of internal functions to reduce latency of future analysis requests. + } + + /** + * Load all internal global functions for analysis. + * + * This is useful if we expect Phan to be running for a long time and forking processes (in language server or daemon mode), + * or if we need all of the signatures of functions (e.g. for tools that need all signatures) + */ + public function forceLoadingInternalFunctions(): void + { + $internal_function_fqsen_set = $this->internal_function_fqsen_set; + try { + foreach ($internal_function_fqsen_set as $function_fqsen) { + // hasFunctionWithFQSEN will automatically load $function_name, **unless** we don't have a signature for that function. + if (!$this->hasFunctionWithFQSEN($function_fqsen)) { + // Force loading these even if automatic loading failed. + // (Shouldn't happen, the function list is fetched from reflection by callers. + $function_alternate_generator = FunctionFactory::functionListFromReflectionFunction( + $function_fqsen, + new \ReflectionFunction($function_fqsen->getNamespacedName()) + ); + foreach ($function_alternate_generator as $function) { + $this->addFunction($function); + } + } + } + } finally { + // Don't need to track these any more *afteR* loading everything. + // hasFunctionWithFQSEN calls hasInternalFunctionWithFQSEN, + // which will only load the function if it was in internal_function_fqsen_set + $this->internal_function_fqsen_set = new Set(); + } + } + + /** + * @param string[] $internal_function_name_list + * @suppress PhanThrowTypeAbsentForCall + */ + private function addInternalFunctionsByNames(array $internal_function_name_list): void + { + $included_extension_subset = self::getIncludedExtensionSubset(); + if (is_array($included_extension_subset)) { + $forbidden_function_set = []; + // Forbid functions both from extensions and zend_extensions such as xdebug + foreach (get_loaded_extensions() as $ext_name) { + if (isset($included_extension_subset[strtolower($ext_name)])) { + continue; + } + foreach (get_extension_funcs($ext_name) ?: [] as $function_name) { + $forbidden_function_set[strtolower($function_name)] = true; + } + } + foreach ($internal_function_name_list as $i => $function_name) { + if (isset($forbidden_function_set[$function_name])) { + unset($internal_function_name_list[$i]); + } + } + } + + foreach ($internal_function_name_list as $function_name) { + $this->internal_function_fqsen_set->attach(FullyQualifiedFunctionName::fromFullyQualifiedString($function_name)); + } + } + + /** + * Clone dependent objects when cloning this object. + */ + public function __clone() + { + $this->fqsen_class_map = + $this->fqsen_class_map->deepCopyValues(); + + $this->fqsen_class_map_user_defined = + new Map(); + + $this->fqsen_class_map_internal = + new Map(); + + foreach ($this->fqsen_class_map as $fqsen => $clazz) { + if ($clazz->isPHPInternal()) { + $this->fqsen_class_map_internal->offsetSet($fqsen, $clazz); + } else { + $this->fqsen_class_map_user_defined->offsetSet($fqsen, $clazz); + } + } + + $this->fqsen_class_map_reflection = + clone($this->fqsen_class_map_reflection); + + $this->fqsen_alias_map = + $this->fqsen_alias_map->deepCopyValues(); + + $this->fqsen_global_constant_map = + $this->fqsen_global_constant_map->deepCopyValues(); + + $this->fqsen_func_map = + $this->fqsen_func_map->deepCopyValues(); + + // NOTE: If this were to become a deep copy, this would also have to update class_map. + // (That also has references to Method, which should be shared in the resulting clone) + $this->method_set = clone($this->method_set); + + $this->class_fqsen_class_map_map = + $this->class_fqsen_class_map_map->deepCopyValues(); + + $this->internal_function_fqsen_set = + clone($this->internal_function_fqsen_set); + + $name_method_map = $this->name_method_map; + $this->name_method_map = []; + foreach ($name_method_map as $name => $method_map) { + $this->name_method_map[$name] = $method_map->deepCopy(); + } + } + + /** + * @param array{clone:CodeBase,callbacks:?(Closure():void)[]} $restore_point + */ + public function restoreFromRestorePoint(array $restore_point): void + { + $clone = $restore_point['clone']; + + // TODO: Restore the inner state of Clazz objects as well + // (e.g. memoizations, types added in method/analysis phases, plugin changes, etc. + // NOTE: Type::clearAllMemoizations is called elsewhere already. + // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach this is intentionally iterating over the private properties of $clone + foreach ($clone as $key => $value) { + $this->{$key} = $value; + } + + foreach ($restore_point['callbacks'] as $callback) { + if ($callback) { + $callback(); + } + } + } + + /** + * For use by daemon mode when running without pcntl + * Returns a serialized representation of everything in this CodeBase. + * @internal + * @return array{clone:CodeBase,callbacks:(?Closure():void)[]} + * @suppress PhanAccessMethodInternal + */ + public function createRestorePoint(): array + { + // Create a deep copy of this CodeBase + $clone = clone($this); + // make a deep copy of the NamespaceMapEntry objects within parsed_namespace_maps + // NOTE: It is faster to *create* the clone if this used unserialize(serialize(parsed_namespace_maps). + // However, it is 10 times slower once you include the time needed to garbage collect the data in the copies, because strings in values are brand new copies in unserialize(). + // It is also likely to require more memory. + $clone->parsed_namespace_maps = $this->parsed_namespace_maps; + foreach ($clone->parsed_namespace_maps as &$map_for_file) { + foreach ($map_for_file as &$map_for_namespace_id) { + foreach ($map_for_namespace_id as &$map_for_use_type) { + foreach ($map_for_use_type as &$entry) { + $entry = clone($entry); + } + } + } + } + + /** @var list */ + $callbacks = []; + // Create callbacks to restore classes + foreach ($this->fqsen_class_map as $class) { + $callbacks[] = $class->createRestoreCallback(); + } + // Create callbacks to restore methods and global functions + foreach ($this->fqsen_func_map as $func) { + $callbacks[] = $func->createRestoreCallback(); + } + // Create callbacks to back up global constants + // (They may refer to constants from other files. + // The other files may change.) + foreach ($this->fqsen_global_constant_map as $const) { + $callbacks[] = $const->createRestoreCallback(); + } + // Create callbacks to back up global constants + // (They may refer to constants from other files. + // The other files may change.) + foreach ($this->class_fqsen_class_map_map as $class_map) { + // Create callbacks to back up class constants and properties. + // Methods were already backed up. + foreach ($class_map->getClassConstantMap() as $const) { + $callbacks[] = $const->createRestoreCallback(); + } + foreach ($class_map->getPropertyMap() as $property) { + $callbacks[] = $property->createRestoreCallback(); + } + } + + return [ + 'clone' => $clone, + 'callbacks' => $callbacks, + ]; + } + + /** + * @return CodeBase + * A new code base is returned which is a shallow clone + * of this one, which is to say that the sets and maps + * of elements themselves are cloned, but the keys and + * values within those sets and maps are not cloned. + * + * Updates to elements will bleed through code bases + * with only shallow clones. See + * https://github.com/phan/phan/issues/257 + */ + public function shallowClone(): CodeBase + { + $code_base = new CodeBase([], [], [], [], []); + $code_base->fqsen_class_map = + clone($this->fqsen_class_map); + $code_base->fqsen_class_map_user_defined = + clone($this->fqsen_class_map_user_defined); + $code_base->fqsen_class_map_internal = + clone($this->fqsen_class_map_internal); + $code_base->fqsen_class_map_reflection = + clone($this->fqsen_class_map_reflection); + $code_base->fqsen_alias_map = + clone($this->fqsen_alias_map); + + $code_base->fqsen_global_constant_map = + clone($this->fqsen_global_constant_map); + $code_base->fqsen_func_map = + clone($this->fqsen_func_map); + $code_base->internal_function_fqsen_set = + clone($this->internal_function_fqsen_set); + $code_base->class_fqsen_class_map_map = + clone($this->class_fqsen_class_map_map); + $code_base->method_set = + clone($this->method_set); + return $code_base; + } + + /** + * @param Clazz $class + * A class to add. + */ + public function addClass(Clazz $class): void + { + // Map the FQSEN to the class + $fqsen = $class->getFQSEN(); + $this->fqsen_class_map->offsetSet($fqsen, $class); + $this->fqsen_class_map_user_defined->offsetSet($fqsen, $class); + if ($this->undo_tracker) { + $this->undo_tracker->recordUndo(static function (CodeBase $inner) use ($fqsen): void { + Daemon::debugf("Undoing addClass %s\n", $fqsen); + $inner->fqsen_class_map->offsetUnset($fqsen); + $inner->fqsen_class_map_user_defined->offsetUnset($fqsen); + // unset($inner->fqsen_class_map_reflection[$fqsen]); // should not be necessary + $inner->class_fqsen_class_map_map->offsetUnset($fqsen); + }); + } + } + + /** + * This should be called in the parse phase + * + * @param associative-array> $namespace_map + * @internal + */ + public function addParsedNamespaceMap(string $file, string $namespace, int $id, array $namespace_map): void + { + $key = "$namespace@$id"; + // print("Adding $file $key count=" .count($namespace_map) . "\n"); + // debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $this->parsed_namespace_maps[$file][$key] = $namespace_map; + if ($this->undo_tracker) { + $this->undo_tracker->recordUndo(static function (CodeBase $inner) use ($file, $key): void { + Daemon::debugf("Undoing addParsedNamespaceMap file = %s namespace = %s\n", $file, $key); + unset($inner->parsed_namespace_maps[$file][$key]); + // Hack: addParsedNamespaceMap is called at least once per each file, so unset file-level suppressions at the same time in daemon mode + unset($inner->file_level_suppression_set[$file]); + }); + } + } + + /** + * This should be called in the analysis phase. + * It retrieves the NamespaceMapEntry built in the parse phase + * (This is implemented this way to allow Phan to know if 'use Foo\Bar' was ever used and warn if it wasn't.) + * + * @param string $file the value of $context->getFile() + * @param string $namespace the namespace value. Probably redundant. + * @param int $id (An incrementing counter for namespaces. 0 or 1 in single namespace/absent namespace files) + * @return associative-array> $namespace_map + * @internal + */ + public function getNamespaceMapFromParsePhase(string $file, string $namespace, int $id): array + { + $key = "$namespace@$id"; + + // I'd hope that this is always defined when this is called. + // However, it may not be if files rapidly change and add/remove namespaces? + return $this->parsed_namespace_maps[$file][$key] ?? []; + } + + /** + * Add a class from reflection to the codebase, + * to be analyzed if any part of the analysis uses its fqsen. + * + * @param ReflectionClass $class + * A class to add, lazily. + */ + public function addReflectionClass(ReflectionClass $class): void + { + // Map the FQSEN to the class + try { + $class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString($class->getName()); + $this->fqsen_class_map_reflection->offsetSet($class_fqsen, $class); + } catch (FQSENException $_) { + // Fixes uncaught Phan\Exception\InvalidFQSENException for #2222 + // Just give up on analyzing uses of the class "OCI-Lob" and anything similar - It's invalid because of the hyphen. + } + } + + /** + * Call this to record the existence of a class_alias in the global scope. + * After parse phase is complete (And daemonize has split off a new process), + * call resolveClassAliases() to create FQSEN entries. + * + * @param FullyQualifiedClassName $original + * an existing class to alias to + * + * @param FullyQualifiedClassName $alias + * a name to alias $original to + */ + public function addClassAlias( + FullyQualifiedClassName $original, + FullyQualifiedClassName $alias, + Context $context, + int $lineno + ): void { + if (!$this->fqsen_alias_map->offsetExists($original)) { + $this->fqsen_alias_map->offsetSet($original, new Set()); + } + $alias_record = new ClassAliasRecord($alias, $context, $lineno); + $this->fqsen_alias_map->offsetGet($original)->attach($alias_record); + + if ($this->undo_tracker) { + // TODO: Track a count of aliases instead? This doesn't work in daemon mode if multiple files add the same alias to the same class. + // TODO: Allow .phan/config.php to specify aliases or precedences for aliases? + /** @suppress PhanPluginUnknownObjectMethodCall TODO: Infer types from ArrayAccess->offsetGet in UnionTypeVisitor->visitDim() */ + $this->undo_tracker->recordUndo(static function (CodeBase $inner) use ($original, $alias_record): void { + $fqsen_alias_map = $inner->fqsen_alias_map[$original] ?? null; + if ($fqsen_alias_map) { + $fqsen_alias_map->detach($alias_record); + if ($fqsen_alias_map->count() === 0) { + unset($inner->fqsen_alias_map[$original]); + } + } + }); + } + } + + /** + * Resolve the aliases of class FQSENs to other class FQSENs. + * + * This is called after all calls to class_alias are parsed and all class definitions are parsed + */ + public function resolveClassAliases(): void + { + if ($this->undo_tracker) { + throw new AssertionError('should only call this after daemon mode is finished'); + } + // loop through fqsen_alias_map and add entries to fqsen_class_map. + foreach ($this->fqsen_alias_map as $original_fqsen => $alias_set) { + $this->resolveClassAliasesForAliasSet($original_fqsen, $alias_set); + } + } + + private function resolveClassAliasesForAliasSet(FullyQualifiedClassName $original_fqsen, Set $alias_set): void + { + if (!$this->hasClassWithFQSEN($original_fqsen)) { + // The original class does not exist. + // Emit issues at the point of every single class_alias call with that original class. + foreach ($alias_set as $alias_record) { + if (!($alias_record instanceof ClassAliasRecord)) { + throw new AssertionError("Expected instances of ClassAliasRecord in alias_set"); + } + $suggestion = IssueFixSuggester::suggestSimilarClass($this, $alias_record->context, $original_fqsen); + + Issue::maybeEmitWithParameters( + $this, + $alias_record->context, + Issue::UndeclaredClassAliasOriginal, + $alias_record->lineno, + [$original_fqsen, $alias_record->alias_fqsen], + $suggestion + ); + } + return; + } + // The original class exists. Attempt to create aliases of the original class. + $class = $this->getClassByFQSEN($original_fqsen); + foreach ($alias_set as $alias_record) { + if (!($alias_record instanceof ClassAliasRecord)) { + throw new AssertionError("Expected instances of ClassAliasRecord in alias_set"); + } + $alias_fqsen = $alias_record->alias_fqsen; + // Don't do anything if there is a real class, or if an earlier class_alias created an alias. + if ($this->hasClassWithFQSEN($alias_fqsen)) { + // Emit a different issue type to make filtering out false positives easier. + $clazz = $this->getClassByFQSEN($alias_fqsen); + Issue::maybeEmit( + $this, + $alias_record->context, + Issue::RedefineClassAlias, + $alias_record->lineno, + $alias_fqsen, + $alias_record->context->getFile(), + $alias_record->lineno, + $clazz->getFQSEN(), + $clazz->getFileRef()->getFile(), + $clazz->getFileRef()->getLineNumberStart() + ); + } else { + $this->fqsen_class_map->offsetSet($alias_fqsen, $class); + } + } + } + + /** + * @return bool + * True if a Clazz with the given FQSEN exists + */ + public function hasClassWithFQSEN( + FullyQualifiedClassName $fqsen + ): bool { + if ($this->fqsen_class_map->offsetExists($fqsen)) { + return true; + } + return $this->lazyLoadPHPInternalClassWithFQSEN($fqsen); + } + + /** + * @return bool + * True if a Clazz with the given FQSEN was created + */ + private function lazyLoadPHPInternalClassWithFQSEN( + FullyQualifiedClassName $fqsen + ): bool { + if ($this->fqsen_class_map_reflection->offsetExists($fqsen)) { + $reflection_class = $this->fqsen_class_map_reflection->offsetGet($fqsen); + $this->loadPHPInternalClassWithFQSEN($fqsen, $reflection_class); + return true; + } + return false; + } + + private function loadPHPInternalClassWithFQSEN( + FullyQualifiedClassName $fqsen, + ReflectionClass $reflection_class + ): void { + $class = Clazz::fromReflectionClass($this, $reflection_class); + $this->fqsen_class_map->offsetSet($fqsen, $class); + $this->fqsen_class_map_internal->offsetSet($fqsen, $class); + $this->fqsen_class_map_reflection->offsetUnset($fqsen); + } + + /** + * @param FullyQualifiedClassName $fqsen + * The FQSEN of a class to get + * + * @return Clazz + * A class with the given FQSEN. + * + * If the parse phase has been completed, this will hydrate the returned class. + */ + public function getClassByFQSEN( + FullyQualifiedClassName $fqsen + ): Clazz { + $clazz = $this->fqsen_class_map->offsetGet($fqsen); + + // This is an optimization that saves us a few minutes + // on very large code bases. + // + // Instead of 'hydrating' all classes (expanding their + // types and importing parent methods, properties, etc.) + // all in one go, we just do it on the fly as they're + // requested. When running as multiple processes this + // lets us avoid a significant amount of hydration per + // process. + if ($this->should_hydrate_requested_elements) { + $clazz->hydrate($this); + } + + return $clazz; + } + + /** + * @param FullyQualifiedClassName $fqsen + * The FQSEN of a class to get + * + * @return Clazz + * A class with the given FQSEN (without hydrating the class) + */ + public function getClassByFQSENWithoutHydrating( + FullyQualifiedClassName $fqsen + ): Clazz { + return $this->fqsen_class_map->offsetGet($fqsen); + } + + /** + * @param FullyQualifiedClassName $original + * The FQSEN of class to get aliases of + * + * @return ClassAliasRecord[] + * A list of all aliases of $original (and their definitions) + */ + public function getClassAliasesByFQSEN( + FullyQualifiedClassName $original + ): array { + if ($this->fqsen_alias_map->offsetExists($original)) { + return $this->fqsen_alias_map->offsetGet($original)->toArray(); + } + + return []; + } + + + /** + * @return Map + * A map from FQSENs to classes which are internal. + */ + public function getUserDefinedClassMap(): Map + { + return $this->fqsen_class_map_user_defined; + } + + /** + * @return Map + * A list of all classes which are internal. + */ + public function getInternalClassMap(): Map + { + if (\count($this->fqsen_class_map_reflection) > 0) { + $fqsen_class_map_reflection = $this->fqsen_class_map_reflection; + // Free up memory used by old class map. Prevent it from being freed before we can load it manually. + $this->fqsen_class_map_reflection = new Map(); + foreach ($fqsen_class_map_reflection as $fqsen => $reflection_class) { + $this->loadPHPInternalClassWithFQSEN($fqsen, $reflection_class); + } + } + // TODO: Resolve internal classes and optimize the implementation. + return $this->fqsen_class_map_internal; + } + + /** + * @param Method $method + * A method to add to the code base + */ + public function addMethod(Method $method): void + { + // Add the method to the map + $this->getClassMapByFQSEN( + $method->getFQSEN() + )->addMethod($method); + + $this->method_set->attach($method); + + // If we're doing dead code detection(or something else) and this is a + // method, map the name to the FQSEN so we can do hail- + // mary references. + if (Config::get_track_references()) { + if (!isset($this->name_method_map[$method->getFQSEN()->getNameWithAlternateId()])) { + $this->name_method_map[$method->getFQSEN()->getNameWithAlternateId()] = new Set(); + } + $this->name_method_map[$method->getFQSEN()->getNameWithAlternateId()]->attach($method); + } + if ($this->undo_tracker) { + // The addClass's recordUndo should remove the class map. Only need to remove it from method_set + $this->undo_tracker->recordUndo(static function (CodeBase $inner) use ($method): void { + $inner->method_set->detach($method); + }); + } + } + + /** + * @return bool + * True if an element with the given FQSEN exists + */ + public function hasMethodWithFQSEN( + FullyQualifiedMethodName $fqsen + ): bool { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen->getFullyQualifiedClassName() + )->hasMethodWithName( + $fqsen->getNameWithAlternateId() + ); + } + + /** + * @param FullyQualifiedMethodName $fqsen + * The FQSEN of a method to get + * + * @return Method + * A method with the given FQSEN + */ + public function getMethodByFQSEN( + FullyQualifiedMethodName $fqsen + ): Method { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen->getFullyQualifiedClassName() + )->getMethodByName( + $fqsen->getNameWithAlternateId() + ); + } + + /** + * @return array + * The set of methods associated with the given class + */ + public function getMethodMapByFullyQualifiedClassName( + FullyQualifiedClassName $fqsen + ): array { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen + )->getMethodMap(); + } + + /** + * @return Set + * A set of all known methods with the given name + */ + public function getMethodSetByName(string $name): Set + { + if (!Config::get_track_references()) { + throw new AssertionError( + __METHOD__ . ' can only be called when dead code ' + . ' detection (or force_tracking_references) is enabled.' + ); + } + + return $this->name_method_map[$name] ?? new Set(); + } + + /** + * @return Set + * The set of all methods and functions + * + * This is slow and should be used only for debugging. + */ + private function getFunctionAndMethodSet(): Set + { + $set = clone($this->method_set); + foreach ($this->fqsen_func_map as $value) { + // @phan-suppress-next-line PhanTypeMismatchArgument deliberately adding different class instances to an existing set + $set->attach($value); + } + return $set; + } + + /** + * @return Set + * The set of all methods that Phan is tracking. + */ + public function getMethodSet(): Set + { + return $this->method_set; + } + + /** + * @return Map> + */ + public function getMethodsMapGroupedByDefiningFQSEN(): Map + { + $methods_by_defining_fqsen = new Map(); + '@phan-var Map> $methods_by_defining_fqsen'; + foreach ($this->method_set as $method) { + $defining_fqsen = $method->getDefiningFQSEN(); + $real_defining_fqsen = $method->getRealDefiningFQSEN(); + // Older php versions have issues with ?? on SplObjectStorage + if ($methods_by_defining_fqsen->offsetExists($defining_fqsen)) { + $methods_by_defining_fqsen->offsetGet($defining_fqsen)->append($method); + } else { + $methods_by_defining_fqsen->offsetSet($defining_fqsen, new ArrayObject([$method])); + } + if ($real_defining_fqsen !== $defining_fqsen) { + if ($methods_by_defining_fqsen->offsetExists($real_defining_fqsen)) { + $methods_by_defining_fqsen->offsetGet($real_defining_fqsen)->append($method); + } else { + $methods_by_defining_fqsen->offsetSet($real_defining_fqsen, new ArrayObject([$method])); + } + } + } + return $methods_by_defining_fqsen; + } + + /** + * @return array> + * A human readable encoding of $this->func_and_method_set [string $function_or_method_name => [int|string $pos => string $spec]] + * Excludes internal functions and methods. + * + * This can be used for debugging Phan's inference + */ + public function exportFunctionAndMethodSet(): array + { + $result = []; + foreach ($this->getFunctionAndMethodSet() as $function_or_method) { + if ($function_or_method->isPHPInternal()) { + continue; + } + $fqsen = $function_or_method->getFQSEN(); + $function_or_method_name = (string)$fqsen; + $signature = [(string)$function_or_method->getUnionType()]; + foreach ($function_or_method->getParameterList() as $param) { + $name = $param->getName(); + $param_type = (string)$param->getUnionType(); + if ($param->isVariadic()) { + $name = '...' . $name; + } + if ($param->isPassByReference()) { + $name = '&' . $name; + } + if ($param->isOptional()) { + $name .= '='; + } + $signature[$name] = $param_type; + } + $result[$function_or_method_name] = $signature; + } + \ksort($result); + return $result; + } + + /** + * @param Func $function + * A function to add to the code base + */ + public function addFunction(Func $function): void + { + // Add it to the map of functions + $this->fqsen_func_map[$function->getFQSEN()] = $function; + + if ($this->undo_tracker) { + $this->undo_tracker->recordUndo(static function (CodeBase $inner) use ($function): void { + Daemon::debugf("Undoing addFunction on %s\n", $function->getFQSEN()); + unset($inner->fqsen_func_map[$function->getFQSEN()]); + }); + } + } + + /** + * @return bool + * True if a global function with the given FQSEN exists + */ + public function hasFunctionWithFQSEN( + FullyQualifiedFunctionName $fqsen + ): bool { + $has_function = $this->fqsen_func_map->contains($fqsen); + + if ($has_function) { + return true; + } + + // Make the following checks: + // + // 1. this is an internal function that hasn't been loaded yet. + // 2. Unless 'ignore_undeclared_functions_with_known_signatures' is true, require that the current php binary or its extensions define this function before that. + return $this->hasInternalFunctionWithFQSEN($fqsen); + } + + /** + * @param FullyQualifiedFunctionName $fqsen + * The FQSEN of a function to get + * + * @return Func + * A function with the given FQSEN + */ + public function getFunctionByFQSEN( + FullyQualifiedFunctionName $fqsen + ): Func { + return $this->fqsen_func_map[$fqsen]; + } + + /** + * @return Map + */ + public function getFunctionMap(): Map + { + return $this->fqsen_func_map; + } + + /** + * @param ClassConstant $class_constant + * A class constant to add to the code base + */ + public function addClassConstant(ClassConstant $class_constant): void + { + $this->getClassMapByFullyQualifiedClassName( + $class_constant->getClassFQSEN() + )->addClassConstant($class_constant); + } + + /** + * @return bool + * True if an class constant with the given FQSEN exists + */ + public function hasClassConstantWithFQSEN( + FullyQualifiedClassConstantName $fqsen + ): bool { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen->getFullyQualifiedClassName() + )->hasClassConstantWithName($fqsen->getNameWithAlternateId()); + } + + /** + * @param FullyQualifiedClassConstantName $fqsen + * The FQSEN of a class constant to get + * + * @return ClassConstant + * A class constant with the given FQSEN + */ + public function getClassConstantByFQSEN( + FullyQualifiedClassConstantName $fqsen + ): ClassConstant { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen->getFullyQualifiedClassName() + )->getClassConstantByName($fqsen->getNameWithAlternateId()); + } + + /** + * @return ClassConstant[] + * The set of class constants associated with the given class + */ + public function getClassConstantMapByFullyQualifiedClassName( + FullyQualifiedClassName $fqsen + ): array { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen + )->getClassConstantMap(); + } + + /** + * @param GlobalConstant $global_constant + * A global constant to add to the code base + */ + public function addGlobalConstant(GlobalConstant $global_constant): void + { + $this->fqsen_global_constant_map[ + $global_constant->getFQSEN() + ] = $global_constant; + if ($this->undo_tracker) { + $this->undo_tracker->recordUndo(static function (CodeBase $inner) use ($global_constant): void { + Daemon::debugf("Undoing addGlobalConstant on %s\n", $global_constant->getFQSEN()); + unset($inner->fqsen_global_constant_map[$global_constant->getFQSEN()]); + }); + } + } + + /** + * @return bool + * True if a a global constant with the given FQSEN exists + */ + public function hasGlobalConstantWithFQSEN( + FullyQualifiedGlobalConstantName $fqsen + ): bool { + return $this->fqsen_global_constant_map->offsetExists($fqsen); + } + + /** + * @param FullyQualifiedGlobalConstantName $fqsen + * The FQSEN of a global constant to get + * + * @return GlobalConstant + * A global constant with the given FQSEN + */ + public function getGlobalConstantByFQSEN( + FullyQualifiedGlobalConstantName $fqsen + ): GlobalConstant { + return $this->fqsen_global_constant_map[$fqsen]; + } + + /** + * @return Map + */ + public function getGlobalConstantMap(): Map + { + return $this->fqsen_global_constant_map; + } + + /** + * @param Property $property + * A property to add to the code base + */ + public function addProperty(Property $property): void + { + $this->getClassMapByFullyQualifiedClassName( + $property->getClassFQSEN() + )->addProperty($property); + } + + /** + * @return bool + * True if a property with the given FQSEN exists + */ + public function hasPropertyWithFQSEN( + FullyQualifiedPropertyName $fqsen + ): bool { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen->getFullyQualifiedClassName() + )->hasPropertyWithName($fqsen->getNameWithAlternateId()); + } + + /** + * @param FullyQualifiedPropertyName $fqsen + * The FQSEN of a property to get + * + * @return Property + * A property with the given FQSEN + */ + public function getPropertyByFQSEN( + FullyQualifiedPropertyName $fqsen + ): Property { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen->getFullyQualifiedClassName() + )->getPropertyByName($fqsen->getNameWithAlternateId()); + } + + /** + * @return Property[] + * The set of properties associated with the given class + */ + public function getPropertyMapByFullyQualifiedClassName( + FullyQualifiedClassName $fqsen + ): array { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen + )->getPropertyMap(); + } + + /** + * @param FullyQualifiedClassElement $fqsen + * The FQSEN of a class element + * + * @return ClassMap + * Get the class map for the class of the given class element's fqsen. + */ + private function getClassMapByFQSEN( + FullyQualifiedClassElement $fqsen + ): ClassMap { + return $this->getClassMapByFullyQualifiedClassName( + $fqsen->getFullyQualifiedClassName() + ); + } + + /** + * @param FullyQualifiedClassName $fqsen + * The FQSEN of a class + * + * @return ClassMap + * Get the class map for an FQSEN of the class. + */ + private function getClassMapByFullyQualifiedClassName( + FullyQualifiedClassName $fqsen + ): ClassMap { + $class_fqsen_class_map_map = $this->class_fqsen_class_map_map; + if ($class_fqsen_class_map_map->offsetExists($fqsen)) { + return $class_fqsen_class_map_map->offsetGet($fqsen); + } + $class_fqsen_class_map_map->offsetSet($fqsen, new ClassMap()); + return $class_fqsen_class_map_map->offsetGet($fqsen); + } + + /** + * @return Map + */ + public function getClassMapMap(): Map + { + return $this->class_fqsen_class_map_map; + } + + /** + * @param FullyQualifiedFunctionName $fqsen + * The FQSEN of a function we'd like to look up + * + * @return bool + * If the FQSEN represents an internal function that + * hasn't been loaded yet, true is returned. + */ + private function hasInternalFunctionWithFQSEN( + FullyQualifiedFunctionName $fqsen + ): bool { + $canonical_fqsen = $fqsen->withAlternateId(0); + $found = isset($this->internal_function_fqsen_set[$canonical_fqsen]); + if (!$found) { + // Act as though functions don't exist if they aren't loaded into the php binary + // running phan (or that binary's extensions), even if the signature map contains them. + // (All of the functions were loaded during initialization) + // + // Also, skip over user-defined global functions defined **by Phan** and its dependencies for analysis + if (!Config::getValue('ignore_undeclared_functions_with_known_signatures')) { + return false; + } + // If we already created the alternates, do nothing. + // TODO: This assumes we call hasFunctionWithFQSEN before adding. + if ($this->fqsen_func_map->offsetExists($canonical_fqsen)) { + return false; + } + } + + $name = $canonical_fqsen->getName(); + if ($canonical_fqsen->getNamespace() !== '\\') { + $name = \ltrim($canonical_fqsen->getNamespace(), '\\') . '\\' . $name; + } + + // For elements in the root namespace, check to see if + // there's a static method signature for something that + // hasn't been loaded into memory yet and create a + // method out of it as it's requested + + $function_signature_map = + UnionType::internalFunctionSignatureMap(Config::get_closest_target_php_version_id()); + + // Don't need to track this any more + unset($this->internal_function_fqsen_set[$canonical_fqsen]); + + if (isset($function_signature_map[$name])) { + $signature = $function_signature_map[$name]; + + // Add each method returned for the signature + foreach (FunctionFactory::functionListFromSignature( + $canonical_fqsen, + $signature + ) as $function) { + if ($name === 'each' && Config::get_closest_target_php_version_id() >= 70200) { + $function->setIsDeprecated(true); + } + if ($found) { + $reflection_function = new \ReflectionFunction($name); + if ($reflection_function->isDeprecated()) { + $function->setIsDeprecated(true); + } + $real_return_type = UnionType::fromReflectionType($reflection_function->getReturnType()); + if (Config::getValue('assume_real_types_for_internal_functions')) { + // @phan-suppress-next-line PhanAccessMethodInternal + $real_type_string = UnionType::getLatestRealFunctionSignatureMap(Config::get_closest_target_php_version_id())[$name] ?? null; + if (\is_string($real_type_string)) { + $real_return_type = UnionType::fromStringInContext($real_type_string, new Context(), Type::FROM_TYPE); + } + } + if (!$real_return_type->isEmpty()) { + $real_type_set = $real_return_type->getTypeSet(); + $function->setRealReturnType($real_return_type); + $function->setUnionType(UnionType::of($function->getUnionType()->getTypeSet() ?: $real_type_set, $real_type_set)); + } + + $real_parameter_list = Parameter::listFromReflectionParameterList($reflection_function->getParameters()); + $function->setRealParameterList($real_parameter_list); + // @phan-suppress-next-line PhanAccessMethodInternal + $function->inheritRealParameterDefaults(); + } + $this->addFunction($function); + $this->updatePluginsOnLazyLoadInternalFunction($function); + } + + return true; + } elseif ($found) { + // Phan doesn't have extended information for the signature for this function, but the function exists. + foreach (FunctionFactory::functionListFromReflectionFunction( + $canonical_fqsen, + new \ReflectionFunction($name) + ) as $function) { + $this->addFunction($function); + $this->updatePluginsOnLazyLoadInternalFunction($function); + } + + return true; + } + return false; + } + + /** + * Returns 0 or more stub functions for a FQSEN that wasn't found in stub files (from .phan/config.php) or Reflection. + * + * This is used to warn about invalid argument counts and types when invoking a method, + * to return the type that would exist if the function existed, etc. + * + * NOTE: These placeholders do not get added to the CodeBase instance, + * and are currently different objects every time they get used. + * + * @return list + */ + public function getPlaceholdersForUndeclaredFunction(FullyQualifiedFunctionName $fqsen): array + { + $canonical_fqsen = $fqsen->withAlternateId(0); + if ($this->fqsen_func_map->offsetExists($canonical_fqsen)) { + // Should not be needed + return [$this->fqsen_func_map->offsetGet($canonical_fqsen)]; + } + + $name = $canonical_fqsen->getName(); + if ($canonical_fqsen->getNamespace() !== '\\') { + $name = \ltrim($canonical_fqsen->getNamespace(), '\\') . '\\' . $name; + } + + // For elements in the root namespace, check to see if + // there's a static method signature for something that + // hasn't been loaded into memory yet and create a + // method out of it as it's requested + + $function_signature_map = + UnionType::internalFunctionSignatureMap(Config::get_closest_target_php_version_id()); + + // Don't need to track this any more + unset($this->internal_function_fqsen_set[$canonical_fqsen]); + + if (!isset($function_signature_map[$name])) { + return []; + } + $signature = $function_signature_map[$name]; + return FunctionFactory::functionListFromSignature( + $canonical_fqsen, + $signature + ); + } + + private function updatePluginsOnLazyLoadInternalFunction(Func $function): void + { + ConfigPluginSet::instance()->handleLazyLoadInternalFunction($this, $function); + } + + /** + * @return int + * The total number of elements of all types in the + * code base. + */ + public function totalElementCount(): int + { + $sum = ( + \count($this->fqsen_func_map) + + \count($this->fqsen_global_constant_map) + + \count($this->fqsen_class_map_user_defined) + + \count($this->fqsen_class_map_internal) // initialized internal classes + + \count($this->fqsen_class_map_reflection) // uninitialized internal classes + ); + + foreach ($this->class_fqsen_class_map_map as $class_map) { + $sum += ( + \count($class_map->getClassConstantMap()) + + \count($class_map->getPropertyMap()) + + \count($class_map->getMethodMap()) + ); + } + + return $sum; + } + + /** + * @param string $file_path @phan-unused-param + * @suppress PhanPluginUseReturnValueNoopVoid + */ + public function flushDependenciesForFile(string $file_path): void + { + // TODO: ... + } + + /** + * @param string $file_path @phan-unused-param + * @return string[] + * The list of files that depend on the code in the given + * file path + */ + public function dependencyListForFile(string $file_path): array + { + // TODO: ... + return []; + } + + /** + * @return non-empty-list every constant name except user-defined constants. + */ + public static function getPHPInternalConstantNameList(): array + { + // Unit tests call this on every test case. Cache the **internal** constants in a static variable for efficiency; those won't change. + static $constant_name_list = null; + if ($constant_name_list === null) { + // 'true', 'false', and 'null' aren't actually defined constants, they're keywords? Add them because anything using AST_CONST would expect them to exist. + $constant_name_list = \array_keys(\array_merge(['true' => true, 'false' => false, 'null' => null], ...\array_values( + \array_diff_key(\get_defined_constants(true), ['user' => []]) + ))); + } + return $constant_name_list; + } + + /** + * @param string $file path to a file + * @param string $issue_type (e.g. 'PhanUnreferencedUseNormal') + */ + public function addFileLevelSuppression(string $file, string $issue_type): void + { + // TODO: Modify the implementation so that it can be checked by UnusedSuppressionPlugin. + if (!isset($this->file_level_suppression_set[$file][$issue_type])) { + $this->file_level_suppression_set[$file][$issue_type] = 0; + } + } + + /** + * @param string $file path to a file + * @param string $issue_type (e.g. 'PhanUnreferencedUseNormal') + */ + public function hasFileLevelSuppression(string $file, string $issue_type): bool + { + // TODO: Modify the implementation so that it can be checked by UnusedSuppressionPlugin. + if (isset($this->file_level_suppression_set[$file][$issue_type])) { + ++$this->file_level_suppression_set[$file][$issue_type]; + return true; + } + return false; + } + + /** + * @var array>|null + * Maps lowercase class name to (lowercase namespace => namespace) + */ + private $namespaces_for_class_names = null; + + /** + * @var array>|null + * Maps lowercase function name to (lowercase namespace => namespace) + */ + private $namespaces_for_function_names = null; + + /** + * @var array>|null + * Maps lowercase class name to (lowercase class => class) + */ + private $class_names_in_namespace = null; + + /** + * @var array>|null + * Maps lowercase function name to (lowercase function => function) + */ + private $function_names_in_namespace = null; + + /** + * @var array>|null + * Maps lowercase function name to (lowercase constant => constant) + */ + private $constant_names_in_namespace = null; + + /** + * @var array + * Maps namespace to an object suggesting class names in that namespace + */ + private $class_names_suggester_in_namespace = []; + + /** + * @var array + * Maps namespace to an object suggesting function names in that namespace + */ + private $function_names_suggester_in_namespace = []; + + /** + * @var array + * Maps namespace to an object suggesting constant names in that namespace + */ + private $constant_names_suggester_in_namespace = []; + + private function invalidateDependentCacheEntries(): void + { + // TODO: Should refactor suggestions logic into a separate class + $this->namespaces_for_class_names = null; + $this->namespaces_for_function_names = null; + $this->class_names_in_namespace = null; + $this->function_names_in_namespace = null; + $this->constant_names_in_namespace = null; + $this->class_names_suggester_in_namespace = []; + $this->function_names_suggester_in_namespace = []; + $this->constant_names_suggester_in_namespace = []; + $this->constant_lookup_map_for_name = null; + } + + /** + * @return array> + */ + private function getNamespacesForClassNames(): array + { + return $this->namespaces_for_class_names ?? ($this->namespaces_for_class_names = $this->computeNamespacesForClassNames()); + } + + /** + * @return array> + */ + private function getNamespacesForFunctionNames(): array + { + return $this->namespaces_for_function_names ?? ($this->namespaces_for_function_names = $this->computeNamespacesForFunctionNames()); + } + + /** + * @return array> a newly computed list of namespaces which have each class name + */ + private function computeNamespacesForClassNames(): array + { + $class_fqsen_list = []; + // NOTE: This helper performs shallow clones to avoid interfering with the iteration pointer + // in other iterations over these class maps + foreach (clone($this->fqsen_class_map_user_defined) as $class_fqsen => $_) { + $class_fqsen_list[] = $class_fqsen; + } + foreach (clone($this->fqsen_class_map_internal) as $class_fqsen => $_) { + $class_fqsen_list[] = $class_fqsen; + } + + $suggestion_set = []; + foreach ($class_fqsen_list as $class_fqsen) { + $namespace = $class_fqsen->getNamespace(); + $suggestion_set[strtolower($class_fqsen->getName())][strtolower($namespace)] = $namespace; + } + foreach (clone($this->fqsen_class_map_reflection) as $reflection_class) { + $namespace = '\\' . $reflection_class->getNamespaceName(); + // https://secure.php.net/manual/en/reflectionclass.getnamespacename.php + $suggestion_set[strtolower($reflection_class->getShortName())][strtolower($namespace)] = $namespace; + } + return $suggestion_set; + } + + /** + * @return array> a newly computed list of namespaces which have each function name + */ + private function computeNamespacesForFunctionNames(): array + { + $function_fqsen_list = []; + // NOTE: This helper performs shallow clones to avoid interfering with the iteration pointer + // in other iterations over these function maps + foreach (clone($this->fqsen_func_map) as $function_fqsen => $_) { + $function_fqsen_list[] = $function_fqsen; + } + foreach (clone($this->internal_function_fqsen_set) as $function_fqsen) { + $function_fqsen_list[] = $function_fqsen; + } + + $suggestion_set = []; + foreach ($function_fqsen_list as $function_fqsen) { + $namespace = $function_fqsen->getNamespace(); + $suggestion_set[strtolower($function_fqsen->getName())][strtolower($namespace)] = $namespace; + } + return $suggestion_set; + } + + /** + * @return list (Don't rely on unique names) + */ + private function getClassFQSENList(): array + { + $class_fqsen_list = []; + // NOTE: This helper performs shallow clones to avoid interfering with the iteration pointer + // in other iterations over these class maps + foreach (clone($this->fqsen_class_map_user_defined) as $class_fqsen => $_) { + $class_fqsen_list[] = $class_fqsen; + } + foreach (clone($this->fqsen_class_map_internal) as $class_fqsen => $_) { + $class_fqsen_list[] = $class_fqsen; + } + return $class_fqsen_list; + } + + /** + * @return array> a list of namespaces which have each class name + */ + private function getClassNamesInNamespaceMap(): array + { + return $this->class_names_in_namespace ?? ($this->class_names_in_namespace = $this->computeClassNamesInNamespace()); + } + + /** + * @return array> a list of namespaces which have each function name + */ + private function getFunctionNamesInNamespaceMap(): array + { + return $this->function_names_in_namespace ?? ($this->function_names_in_namespace = $this->computeFunctionNamesInNamespace()); + } + + /** + * @return array> a list of namespaces which have each constant name + */ + private function getConstantNamesInNamespaceMap(): array + { + return $this->constant_names_in_namespace ?? ($this->constant_names_in_namespace = $this->computeConstantNamesInNamespace()); + } + + /** + * @return array a list of class names in $namespace + */ + public function getClassNamesOfNamespace(string $namespace): array + { + $namespace = strtolower($namespace); + if (\substr($namespace, 0, 1) !== '\\') { + $namespace = "\\$namespace"; + } + return $this->getClassNamesInNamespaceMap()[$namespace] ?? []; + } + + /** + * @return array a list of function names in $namespace + */ + public function getFunctionNamesOfNamespace(string $namespace): array + { + $namespace = strtolower($namespace); + if (\substr($namespace, 0, 1) !== '\\') { + $namespace = "\\$namespace"; + } + return $this->getFunctionNamesInNamespaceMap()[$namespace] ?? []; + } + + /** + * @return array a list of constant names in $namespace + */ + public function getConstantNamesOfNamespace(string $namespace): array + { + $namespace = strtolower($namespace); + if (\substr($namespace, 0, 1) !== '\\') { + $namespace = "\\$namespace"; + } + return $this->getConstantNamesInNamespaceMap()[$namespace] ?? []; + } + + /** + * This limits the suggested class names from getClassNamesOfNamespace for $namespace_lower to + * the names which are similar enough in length to be a potential suggestion, + * or those which have the requested name as a prefix + */ + private function getClassNameSuggesterForNamespace(string $namespace): StringSuggester + { + $namespace = strtolower($namespace); + return $this->class_names_suggester_in_namespace[$namespace] + ?? ($this->class_names_suggester_in_namespace[$namespace] = new StringSuggester($this->getClassNamesOfNamespace($namespace))); + } + + /** + * This limits the suggested function names from getFunctionNamesOfNamespace for $namespace_lower to + * the names which are similar enough in length to be a potential suggestion, + * or those which have the requested name as a prefix + */ + private function getFunctionNameSuggesterForNamespace(string $namespace): StringSuggester + { + $namespace = strtolower($namespace); + return $this->function_names_suggester_in_namespace[$namespace] + ?? ($this->function_names_suggester_in_namespace[$namespace] = new StringSuggester($this->getFunctionNamesOfNamespace($namespace))); + } + + /** + * This limits the suggested constant names from getConstantNamesOfNamespace for $namespace_lower to + * the names which are similar enough in length to be a potential suggestion, + * or those which have the requested name as a prefix + */ + private function getConstantNameSuggesterForNamespace(string $namespace): StringSuggester + { + $namespace = strtolower($namespace); + return $this->constant_names_suggester_in_namespace[$namespace] + ?? ($this->constant_names_suggester_in_namespace[$namespace] = new StringSuggester($this->getConstantNamesOfNamespace($namespace))); + } + + /** + * @return array> maps namespace name to unique classes in that namespace. + */ + private function computeClassNamesInNamespace(): array + { + $class_fqsen_list = $this->getClassFQSENList(); + + $suggestion_set = []; + foreach ($class_fqsen_list as $class_fqsen) { + $namespace = $class_fqsen->getNamespace(); + $name = $class_fqsen->getName(); + $suggestion_set[strtolower($namespace)][strtolower($name)] = $name; + } + foreach (clone($this->fqsen_class_map_reflection) as $reflection_class) { + $namespace = '\\' . $reflection_class->getNamespaceName(); + $name = '\\' . $reflection_class->getName(); + // https://secure.php.net/manual/en/reflectionclass.getnamespacename.php + $suggestion_set[strtolower($namespace)][strtolower($name)] = $name; + } + return $suggestion_set; + } + + /** + * @return array> maps namespace name to unique functions in that namespace. + */ + private function computeFunctionNamesInNamespace(): array + { + $suggestion_set = []; + foreach (clone($this->fqsen_func_map) as $function_fqsen => $_) { + $namespace = $function_fqsen->getNamespace(); + $name = $function_fqsen->getName(); + $suggestion_set[strtolower($namespace)][strtolower($name)] = $name; + } + foreach (clone($this->internal_function_fqsen_set) as $function_fqsen) { + $namespace = $function_fqsen->getNamespace(); + $name = $function_fqsen->getName(); + $suggestion_set[strtolower($namespace)][strtolower($name)] = $name; + } + return $suggestion_set; + } + + /** + * @return array> maps namespace name to unique constants in that namespace. + */ + private function computeConstantNamesInNamespace(): array + { + $suggestion_set = []; + foreach (clone($this->fqsen_global_constant_map) as $fqsen => $_) { + $namespace = $fqsen->getNamespace(); + $name = $fqsen->getName(); + $suggestion_set[strtolower($namespace)][$name] = $name; + } + foreach (['TRUE', 'FALSE', 'NULL'] as $redundant) { + unset($suggestion_set['\\'][$redundant]); + } + return $suggestion_set; + } + + /** + * @unused-param $context + * @return list 0 or more namespaced class names found in this code base + */ + public function suggestSimilarClassInOtherNamespace( + FullyQualifiedClassName $missing_class, + Context $context + ): array { + $class_name = $missing_class->getName(); + $class_name_lower = strtolower($class_name); + $namespaces_for_class_names = $this->getNamespacesForClassNames(); + + $namespaces_for_class = $namespaces_for_class_names[$class_name_lower] ?? []; + if (count($namespaces_for_class) === 0) { + return []; + } + // We're looking for similar names, not identical ones + unset($namespaces_for_class[strtolower($missing_class->getNamespace())]); + $namespaces_for_class = \array_values($namespaces_for_class); + + \usort($namespaces_for_class, 'strcmp'); + + /** @suppress PhanThrowTypeAbsentForCall */ + return \array_map(static function (string $namespace_name) use ($class_name): FullyQualifiedClassName { + return FullyQualifiedClassName::make($namespace_name, $class_name); + }, $namespaces_for_class); + } + + /** + * @unused-param $context + * @return list 0 or more namespaced function names found in this code base with the same name but different namespaces + */ + public function suggestSimilarGlobalFunctionInOtherNamespace( + string $namespace, + string $function_name, + Context $context, + bool $include_same_namespace = false + ): array { + $function_name_lower = strtolower($function_name); + $namespaces_for_function_names = $this->getNamespacesForFunctionNames(); + + $namespaces_for_function = $namespaces_for_function_names[$function_name_lower] ?? []; + if (count($namespaces_for_function) === 0) { + return []; + } + if (!$include_same_namespace) { + // We're looking for similar names, not identical ones + unset($namespaces_for_function[strtolower($namespace)]); + } + $namespaces_for_function = \array_values($namespaces_for_function); + + \usort($namespaces_for_function, 'strcmp'); + + /** @suppress PhanThrowTypeAbsentForCall */ + return \array_map(static function (string $namespace_name) use ($function_name): FullyQualifiedFunctionName { + return FullyQualifiedFunctionName::make($namespace_name, $function_name); + }, $namespaces_for_function); + } + + private static function phpVersionIdToString(int $php_version_id): string + { + return \sprintf('%d.%d', $php_version_id / 10000, ($php_version_id / 100) % 100); + } + + /** + * @unused-param $context + * @return list 0 or more namespaced function names found in this code base for newer php versions + */ + public function suggestSimilarGlobalFunctionInNewerVersion( + string $namespace, + string $function_name, + Context $context, + bool $suggest_in_global_namespace + ): array { + if (!$suggest_in_global_namespace && $namespace !== '\\') { + return []; + } + $target_php_version_config = Config::get_closest_target_php_version_id(); + $target_php_version = (int)\floor(\min($target_php_version_config, \PHP_VERSION_ID) / 100) * 100; + $targets = [50600, 70000, 70100, 70200, 70300, 70400, 80000]; + $function_name_lower = strtolower($function_name); + foreach ($targets as $i => $target) { + // If $target_php_version is 7.1 only check for functions added in 7.2 or newer that weren't in the previous version. + // Don't suggest functions added in 7.1 + $next_target = $targets[$i + 1] ?? 0; + if (!$next_target || $next_target <= $target_php_version) { + continue; + } + $signature_map = UnionType::internalFunctionSignatureMap($next_target); + if (isset($signature_map[$function_name_lower])) { + $old_signature_map = UnionType::internalFunctionSignatureMap($target); + if (!isset($old_signature_map[$function_name_lower])) { + $details = \sprintf('target_php_version=%s run with PHP %s', self::phpVersionIdToString($target_php_version_config), self::phpVersionIdToString(\PHP_VERSION_ID)); + $suggestion = \sprintf( + '(to use the function %s() added in PHP %s in a project with %s without a polyfill parsed by Phan)', + $function_name, + self::phpVersionIdToString($next_target), + $details + ); + return [$suggestion]; + } + } + } + return []; + } + + /** + * @internal + */ + protected const _NON_CLASS_TYPE_SUGGESTION_SET = [ + 'array' => 'array', + 'bool' => 'bool', + 'callable' => 'callable', + 'false' => 'false', + 'float' => 'float', + 'int' => 'int', + 'iterable' => 'iterable', + 'mixed' => 'mixed', + 'null' => 'null', + 'object' => 'object', + 'resource' => 'resource', + 'scalar' => 'scalar', + 'self' => 'self', + 'static' => 'static', + 'string' => 'string', + 'true' => 'true', + // 'void' only makes sense for return type suggestions + ]; + + /** + * @return list 0 or more namespaced function names found in this code base in $namespace + */ + public function suggestSimilarGlobalFunctionInSameNamespace( + string $namespace, + string $name, + Context $context, + bool $suggest_in_global_namespace + ): array { + $suggestions = $this->suggestSimilarGlobalFunctionForNamespaceAndName($namespace, $name, $context); + if ($namespace !== "\\" && $suggest_in_global_namespace) { + $suggestions = \array_merge( + $suggestions, + $this->suggestSimilarGlobalFunctionForNamespaceAndName("\\", $name, $context) + ); + } + return $suggestions; + } + + /** + * @throws FQSENException + */ + private function getClassIfConstructorAccessible(string $namespace, string $name, Context $context): ?FullyQualifiedClassName + { + $fqsen = FullyQualifiedClassName::makeIfLoaded($namespace, $name); + if (!$fqsen || !$this->hasClassWithFQSEN($fqsen)) { + return null; + } + $class = $this->getClassByFQSEN($fqsen); + if (!$class->isClass()) { + return null; + } + if (!$class->hasMethodWithName($this, '__construct', true)) { + // Allow both the constructor and the absence of a constructor + return $fqsen; + } + $class_fqsen_in_current_scope = IssueFixSuggester::maybeGetClassInCurrentScope($context); + if ($class->getMethodByName($this, '__construct')->isAccessibleFromClass($this, $class_fqsen_in_current_scope)) { + return $fqsen; + } + return null; + } + + /** + * @return list 0 or more namespaced class names found in this code base in $namespace + */ + public function suggestSimilarNewInAnyNamespace( + string $namespace, + string $name, + Context $context, + bool $suggest_in_global_namespace + ): array { + try { + $suggestions = []; + $fqsen = $this->getClassIfConstructorAccessible($namespace, $name, $context); + if ($fqsen && $this->hasClassWithFQSEN($fqsen)) { + $suggestions[] = $fqsen; + } + if ($namespace !== "\\" && $suggest_in_global_namespace) { + $fqsen = $this->getClassIfConstructorAccessible('\\', $name, $context); + if ($fqsen && $this->hasClassWithFQSEN($fqsen)) { + $suggestions[] = $fqsen; + } + } + } catch (Exception $_) { + // ignore + } + return $suggestions; + } + + /** + * @return list an array of constants similar to the missing constant. + */ + public function suggestSimilarConstantsToConstant(string $name): array + { + $map = $this->getConstantLookupMapForName(); + $results = $map[strtolower($name)] ?? []; + return \array_values($results); + } + + /** + * @var ?array> maps lowercase name to FQSEN to constant + */ + private $constant_lookup_map_for_name; + + /** @return array> maps constant name to namespace to constant (cached) */ + private function getConstantLookupMapForName(): array + { + return $this->constant_lookup_map_for_name ?? ($this->constant_lookup_map_for_name = $this->computeConstantLookupMapForName()); + } + + /** @return array> maps constant name to namespace to constant */ + private function computeConstantLookupMapForName(): array + { + $result = []; + foreach ($this->fqsen_global_constant_map as $fqsen => $_) { + $result[strtolower($fqsen->getName())][$fqsen->__toString()] = $fqsen; + } + return $result; + } + + /** + * @unused-param $context + * @return list 0 or more namespaced function names found in this code base, from various namespaces + */ + public function suggestSimilarGlobalFunctionForNamespaceAndName( + string $namespace, + string $name, + Context $context + ): array { + $suggester = $this->getFunctionNameSuggesterForNamespace($namespace); + $suggested_function_names = $suggester->getSuggestions($name); + + /** + * @suppress PhanThrowTypeAbsentForCall + */ + return \array_values(\array_map(static function (string $function_name) use ($namespace): FullyQualifiedFunctionName { + return FullyQualifiedFunctionName::make($namespace, $function_name); + }, $suggested_function_names)); + } + + /** + * @return list 0 or more namespaced constant names found in this code base, from various namespaces + */ + public function suggestSimilarGlobalConstantForNamespaceAndName( + string $namespace, + string $name + ): array { + $suggester = $this->getConstantNameSuggesterForNamespace($namespace); + $suggested_constant_names = $suggester->getSuggestions($name); + + /** + * @suppress PhanThrowTypeAbsentForCall + */ + return \array_values(\array_map(static function (string $constant_name): FullyQualifiedGlobalConstantName { + return FullyQualifiedGlobalConstantName::fromFullyQualifiedString($constant_name); + }, $suggested_constant_names)); + } + + /** + * @param int $class_suggest_type value from IssueFixSuggester::CLASS_SUGGEST_* + * @unused-param $context + * + * @return list 0 or more namespaced class names found in this code base + * + * NOTE: Non-classes are always represented as strings (and will be suggested even if there is a namespace), + * classes are always represented as FullyQualifiedClassName + */ + public function suggestSimilarClassInSameNamespace( + FullyQualifiedClassName $missing_class, + Context $context, + int $class_suggest_type = IssueFixSuggester::CLASS_SUGGEST_ONLY_CLASSES + ): array { + $namespace = $missing_class->getNamespace(); + $class_name = $missing_class->getName(); + $class_name_lower = strtolower($class_name); + + $suggester = $this->getClassNameSuggesterForNamespace($namespace); + $class_names_in_namespace = $suggester->getSuggestions($class_name); + + if (count($class_names_in_namespace) > Config::getValue('suggestion_check_limit')) { + return []; + } + + $suggestion_set = $class_names_in_namespace; + if ($class_suggest_type !== IssueFixSuggester::CLASS_SUGGEST_ONLY_CLASSES) { + // TODO: Could limit earlier here and precompute (based on similar string length) + $suggestion_set += self::_NON_CLASS_TYPE_SUGGESTION_SET; + if ($class_suggest_type === IssueFixSuggester::CLASS_SUGGEST_CLASSES_AND_TYPES_AND_VOID) { + $suggestion_set['void'] = 'void'; + } + } + unset($suggestion_set[$class_name_lower]); + if (count($suggestion_set) === 0) { + return []; + } + + // We're looking for similar names, not identical names + $suggested_class_names = \array_keys( + IssueFixSuggester::getSuggestionsForStringSet($class_name_lower, $suggestion_set) + ); + + if (\count($suggested_class_names) === 0) { + return []; + } + \usort($suggested_class_names, 'strcmp'); + + /** + * @return string|FullyQualifiedClassName + * @suppress PhanThrowTypeAbsentForCall + */ + return \array_map(static function (string $class_name_lower) use ($namespace, $class_names_in_namespace) { + if (!\array_key_exists($class_name_lower, $class_names_in_namespace)) { + // This is a builtin type + return $class_name_lower; + } + $class_name = $class_names_in_namespace[$class_name_lower]; + return FullyQualifiedClassName::make($namespace, $class_name); + }, $suggested_class_names); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/CodeBase/ClassMap.php b/bundled-libs/phan/phan/src/Phan/CodeBase/ClassMap.php new file mode 100644 index 000000000..ac92db853 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/CodeBase/ClassMap.php @@ -0,0 +1,136 @@ + + * A map from name to ClassConstant + */ + private $class_constant_map = []; + + /** + * @var array + * A map from name to Property + */ + private $property_map = []; + + /** + * @var array + * A map from name to Method + */ + private $method_map = []; + + /** + * Record that the class this represents has the provided class constant. + */ + public function addClassConstant(ClassConstant $constant): void + { + $this->class_constant_map[ + $constant->getFQSEN()->getNameWithAlternateId() + ] = $constant; + } + + /** + * @return bool does this class have a constant with name $name + */ + public function hasClassConstantWithName(string $name): bool + { + return isset($this->class_constant_map[$name]); + } + + /** + * Gets the class constant $name of this class + */ + public function getClassConstantByName(string $name): ClassConstant + { + return $this->class_constant_map[$name]; + } + + /** + * @return array + */ + public function getClassConstantMap(): array + { + return $this->class_constant_map; + } + + /** + * Record that the class this represents has the provided property information + */ + public function addProperty(Property $property): void + { + $this->property_map[ + $property->getFQSEN()->getNameWithAlternateId() + ] = $property; + } + + /** + * Checks if the class this represents has a property with name $name + */ + public function hasPropertyWithName(string $name): bool + { + return isset($this->property_map[$name]); + } + + /** + * Fetch information about the property (of the class this represents) with name $name + */ + public function getPropertyByName(string $name): Property + { + return $this->property_map[$name]; + } + + /** + * @return array + */ + public function getPropertyMap(): array + { + return $this->property_map; + } + + /** + * Records that the class that this represents has the provided method. + */ + public function addMethod(Method $method): void + { + $this->method_map[\strtolower( + $method->getFQSEN()->getNameWithAlternateId() + )] = $method; + } + + /** + * Checks if the class that this represents has a method with name $name. + */ + public function hasMethodWithName(string $name): bool + { + return isset($this->method_map[\strtolower($name)]); + } + + /** + * Fetches the method signature with name $name of the class that this represents. + */ + public function getMethodByName(string $name): Method + { + return $this->method_map[\strtolower($name)]; + } + + /** + * @return array + */ + public function getMethodMap(): array + { + return $this->method_map; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/CodeBase/UndoTracker.php b/bundled-libs/phan/phan/src/Phan/CodeBase/UndoTracker.php new file mode 100644 index 000000000..9067da3bb --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/CodeBase/UndoTracker.php @@ -0,0 +1,219 @@ +getFile() make keeping this redundant? + */ + private $current_parsed_file; + + /** + * @var array> + */ + private $undo_operations_for_path = []; + + /** + * @var array Maps file paths to the modification dates and file size of the paths. - On ext4, milliseconds are available, but php APIs all return seconds. + */ + private $file_modification_state = []; + + public function __construct() + { + } + + /** + * @return list - The list of files which are successfully parsed. + * This changes whenever the file list is reloaded from disk. + * This also includes files which don't declare classes or functions or globals, + * because those files use classes/functions/constants. + * + * (This is the list prior to any analysis exclusion or whitelisting steps) + */ + public function getParsedFilePathList(): array + { + return \array_keys($this->file_modification_state); + } + + /** + * @return int The size of $this->getParsedFilePathList() + */ + public function getParsedFilePathCount(): int + { + return count($this->file_modification_state); + } + + /** + * Record that Phan has started parsing $current_parsed_file. + * + * This allows us to track which changes need to be undone when that file's contents change or the file gets removed. + */ + public function setCurrentParsedFile(?string $current_parsed_file): void + { + if (\is_string($current_parsed_file)) { + Daemon::debugf("Recording file modification state for %s", $current_parsed_file); + // This shouldn't be null. TODO: Figure out what to do if it is. + $this->file_modification_state[$current_parsed_file] = self::getFileState($current_parsed_file); + } + $this->current_parsed_file = $current_parsed_file; + } + + + /** + * @return ?string - This string should change when the file is modified. Returns null if the file somehow doesn't exist + */ + public static function getFileState(string $path): ?string + { + \clearstatcache(true, $path); // TODO: does this work properly with symlinks? seems to. + $real = \realpath($path); + if (!\is_string($real)) { + return null; + } + if (!\file_exists($real)) { + return null; + } + $stat = @\stat($real); // Double check: suppress to prevent Phan's error_handler from terminating on error. + if (!$stat) { + return null; // It was missing or unreadable. + } + return \sprintf('%d_%d', $stat['mtime'], $stat['size']); + } + + /** + * Called when a file is unparsable. + * Removes the classes and functions, etc. from an older version of the file, if one exists. + */ + public function recordUnparsableFile(CodeBase $code_base, string $current_parsed_file): void + { + Daemon::debugf("%s was unparsable, had a syntax error", $current_parsed_file); + Phan::getIssueCollector()->removeIssuesForFiles([$current_parsed_file]); + $this->undoFileChanges($code_base, $current_parsed_file); + unset($this->file_modification_state[$current_parsed_file]); + } + + /** + * Undoes all of the changes for the relative path at $path + */ + private function undoFileChanges(CodeBase $code_base, string $path): void + { + Daemon::debugf("Undoing file changes for $path"); + foreach ($this->undo_operations_for_path[$path] ?? [] as $undo_operation) { + $undo_operation($code_base); + } + unset($this->undo_operations_for_path[$path]); + } + + /** + * @param \Closure $undo_operation - a closure expecting 1 param - inner. It undoes a change caused by a parsed file. + * Ideally, this would extend to all changes. (e.g. including dead code detection) + */ + public function recordUndo(Closure $undo_operation): void + { + $file = $this->current_parsed_file; + if (!\is_string($file)) { + throw new \RuntimeException("Called recordUndo in CodeBaseMutable, but not parsing a file"); + } + if (!isset($this->undo_operations_for_path[$file])) { + $this->undo_operations_for_path[$file] = []; + } + $this->undo_operations_for_path[$file][] = $undo_operation; + } + + /** + * @param CodeBase $code_base - code base owning this tracker + * @param list $new_file_list + * @param array $file_mapping_contents + * @param ?(string[]) $reanalyze_files files to re-parse before re-running analysis. + * This fixes #1921 + * @return list - Subset of $new_file_list which changed on disk and has to be parsed again. Automatically unparses the old versions of files which were modified. + */ + public function updateFileList(CodeBase $code_base, array $new_file_list, array $file_mapping_contents, array $reanalyze_files = null): array + { + $new_file_set = []; + foreach ($new_file_list as $path) { + $new_file_set[$path] = true; + } + foreach ($file_mapping_contents as $path => $_) { + $new_file_set[$path] = true; + } + unset($new_file_list); + $removed_file_list = []; + $changed_or_added_file_list = []; + foreach ($new_file_set as $path => $_) { + if (!isset($this->file_modification_state[$path])) { + $changed_or_added_file_list[] = $path; + } + } + foreach ($this->file_modification_state as $path => $state) { + if (!isset($new_file_set[$path])) { + $this->undoFileChanges($code_base, $path); + $removed_file_list[] = $path; + unset($this->file_modification_state[$path]); + continue; + } + // TODO: Always invalidate the parsed file if we're about to analyze it? + if (isset($file_mapping_contents[$path])) { + // TODO: Move updateFileList to be called before fork()? + $new_state = 'daemon:' . \sha1($file_mapping_contents[$path]); + } else { + $new_state = self::getFileState($path); + } + if ($new_state !== $state || in_array($path, $reanalyze_files ?? [], true)) { + $removed_file_list[] = $path; + $this->undoFileChanges($code_base, $path); + // TODO: This will call stat() twice as much as necessary for the modified files. Not important. + unset($this->file_modification_state[$path]); + if ($new_state !== null) { + $changed_or_added_file_list[] = $path; + } + } + } + if (count($removed_file_list) > 0) { + Phan::getIssueCollector()->removeIssuesForFiles($removed_file_list); + } + return $changed_or_added_file_list; + } + + /** + * @param CodeBase $code_base - code base owning this tracker + * @param string $file_path + * @return bool - true if the file existed + */ + public function beforeReplaceFileContents(CodeBase $code_base, string $file_path): bool + { + if (!isset($this->file_modification_state[$file_path])) { + Daemon::debugf("Tried to replace contents of '$file_path', but that path does not yet exist"); + return false; + } + Phan::getIssueCollector()->removeIssuesForFiles([$file_path]); + $this->undoFileChanges($code_base, $file_path); + unset($this->file_modification_state[$file_path]); + return true; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Config.php b/bundled-libs/phan/phan/src/Phan/Config.php new file mode 100644 index 000000000..59454b08a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Config.php @@ -0,0 +1,1695 @@ + null, + + // The PHP version that will be used for feature/syntax compatibility warnings. + // + // Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`, `null`. + // If this is set to `null`, Phan will first attempt to infer the value from + // the project's composer.json's `{"require": {"php": "version range"}}` if possible. + // If that could not be determined, then Phan assumes `target_php_version`. + 'minimum_target_php_version' => null, + + // Default: true. If this is set to true, + // and `target_php_version` is newer than the version used to run Phan, + // Phan will act as though functions added in newer PHP versions exist. + // + // NOTE: Currently, this only affects `Closure::fromCallable()` + 'pretend_newer_core_methods_exist' => true, + + // Make the tolerant-php-parser polyfill generate doc comments + // for all types of elements, even if php-ast wouldn't (for an older PHP version) + 'polyfill_parse_all_element_doc_comments' => true, + + // A list of individual files to include in analysis + // with a path relative to the root directory of the + // project. + 'file_list' => [], + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in `exclude_analysis_directory_list`, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [], + + // For internal use by Phan to quickly check for membership in directory_list. + '__directory_regex' => null, + + // Whether to enable debugging output to stderr + 'debug_output' => false, + + // List of case-insensitive file extensions supported by Phan. + // (e.g. `['php', 'html', 'htm']`) + 'analyzed_file_extensions' => ['php'], + + // A regular expression to match files to be excluded + // from parsing and analysis and will not be read at all. + // + // This is useful for excluding groups of test or example + // directories/files, unanalyzable files, or files that + // can't be removed for whatever reason. + // (e.g. `'@Test\.php$@'`, or `'@vendor/.*/(tests|Tests)/@'`) + 'exclude_file_regex' => '', + + // A list of files that will be excluded from parsing and analysis + // and will not be read at all. + // + // This is useful for excluding hopelessly unanalyzable + // files that can't be removed for whatever reason. + 'exclude_file_list' => [], + + // Enable this to enable checks of require/include statements referring to valid paths. + // The settings `include_paths` and `warn_about_relative_include_statement` affect the checks. + 'enable_include_path_checks' => false, + + // A list of [include paths](https://secure.php.net/manual/en/ini.core.php#ini.include-path) to check when checking if `require_once`, `include`, etc. are pointing to valid files. + // + // To refer to the directory of the file being analyzed, use `'.'` + // To refer to the project root directory, use \Phan\Config::getProjectRootDirectory() + // + // (E.g. `['.', \Phan\Config::getProjectRootDirectory() . '/src/folder-added-to-include_path']`) + // + // This is ignored if `enable_include_path_checks` is not `true`. + 'include_paths' => ['.'], + + // Enable this to warn about the use of relative paths in `require_once`, `include`, etc. + // Relative paths are harder to reason about, and opcache may have issues with relative paths in edge cases. + // + // This is ignored if `enable_include_path_checks` is not `true`. + 'warn_about_relative_include_statement' => false, + + // A directory list that defines files that will be excluded + // from static analysis, but whose class and method + // information should be included. + // + // Generally, you'll want to include the directories for + // third-party code (such as "vendor/") in this list. + // + // n.b.: If you'd like to parse but not analyze 3rd + // party code, directories containing that code + // should be added to the `directory_list` as well as + // to `exclude_analysis_directory_list`. + 'exclude_analysis_directory_list' => [], + + // This is set internally by Phan based on exclude_analysis_directory_list + '__exclude_analysis_regex' => null, + + // A list of files that will be included in static analysis, + // **to the exclusion of others.** + // + // This typically should not get put in your Phan config file. + // It gets set by `--include-analysis-file-list`. + // + // Use `directory_list` and `file_list` instead to add files + // to be parsed and analyzed, and `exclude_*` to exclude files + // and folders from analysis. + 'include_analysis_file_list' => [], + + // Backwards Compatibility Checking. This is slow + // and expensive, but you should consider running + // it before upgrading your version of PHP to a + // new version that has backward compatibility + // breaks. + // + // If you are migrating from PHP 5 to PHP 7, + // you should also look into using + // [php7cc (no longer maintained)](https://github.com/sstalle/php7cc) + // and [php7mar](https://github.com/Alexia/php7mar), + // which have different backwards compatibility checks. + // + // If you are still using versions of php older than 5.6, + // `PHP53CompatibilityPlugin` may be worth looking into if you are not running + // syntax checks for php 5.3 through another method such as + // `InvokePHPNativeSyntaxCheckPlugin` (see .phan/plugins/README.md). + 'backward_compatibility_checks' => true, + + // A set of fully qualified class-names for which + // a call to `parent::__construct()` is required. + 'parent_constructor_required' => [], + + // If true, this runs a quick version of checks that takes less + // time at the cost of not running as thorough + // of an analysis. You should consider setting this + // to true only when you wish you had more **undiagnosed** issues + // to fix in your code base. + // + // In quick-mode the scanner doesn't rescan a function + // or a method's code block every time a call is seen. + // This means that the problem here won't be detected: + // + // ```php + // false, + + // The maximum recursion depth that can be reached when analyzing the code. + // This setting only takes effect when quick_mode is disabled. + // A higher limit will make the analysis more accurate, but could possibly + // make it harder to track the code bit where a detected issue originates. + // As long as this is kept relatively low, performance is usually not affected + // by changing this setting. + 'maximum_recursion_depth' => 2, + + // If enabled, check all methods that override a + // parent method to make sure its signature is + // compatible with the parent's. + // + // This check can add quite a bit of time to the analysis. + // + // This will also check if final methods are overridden, etc. + 'analyze_signature_compatibility' => true, + + // Set this to true to allow contravariance in real parameter types of method overrides + // (Users may enable this if analyzing projects that support only php 7.2+) + // + // See [this note about PHP 7.2's new features](https://secure.php.net/manual/en/migration72.new-features.php#migration72.new-features.param-type-widening). + // This is false by default. (By default, Phan will warn if real parameter types are omitted in an override) + // + // If this is null, this will be inferred from `target_php_version`. + 'allow_method_param_type_widening' => null, + + // Set this to true to make Phan guess that undocumented parameter types + // (for optional parameters) have the same type as default values + // (Instead of combining that type with `mixed`). + // + // E.g. `function my_method($x = 'val')` would make Phan infer that `$x` had a type of `string`, not `string|mixed`. + // Phan will not assume it knows specific types if the default value is `false` or `null`. + 'guess_unknown_parameter_type_using_default' => false, + + // Allow adding types to vague return types such as @return object, @return ?mixed in function/method/closure union types. + // Normally, Phan only adds inferred returned types when there is no `@return` type or real return type signature.. + // This setting can be disabled on individual methods by adding `@phan-hardcode-return-type` to the doc comment. + // + // Disabled by default. This is more useful with `--analyze-twice`. + 'allow_overriding_vague_return_types' => false, + + // When enabled, infer that the types of the properties of `$this` are equal to their default values at the start of `__construct()`. + // This will have some false positives due to Phan not checking for setters and initializing helpers. + // This does not affect inherited properties. + // + // Set to true to enable. + 'infer_default_properties_in_construct' => false, + + // If enabled, inherit any missing phpdoc for types from + // the parent method if none is provided. + // + // NOTE: This step will only be performed if `analyze_signature_compatibility` is also enabled. + 'inherit_phpdoc_types' => true, + + // The minimum severity level to report on. This can be + // set to `Issue::SEVERITY_LOW`, `Issue::SEVERITY_NORMAL` or + // `Issue::SEVERITY_CRITICAL`. Setting it to only + // critical issues is a good place to start on a big + // sloppy mature code base. + 'minimum_severity' => Issue::SEVERITY_LOW, + + // If enabled, missing properties will be created when + // they are first seen. If false, we'll report an + // error message if there is an attempt to write + // to a class property that wasn't explicitly + // defined. + 'allow_missing_properties' => false, + + // If enabled, allow null to be cast as any array-like type. + // + // This is an incremental step in migrating away from `null_casts_as_any_type`. + // If `null_casts_as_any_type` is true, this has no effect. + 'null_casts_as_array' => false, + + // If enabled, allow any array-like type to be cast to null. + // This is an incremental step in migrating away from `null_casts_as_any_type`. + // If `null_casts_as_any_type` is true, this has no effect. + 'array_casts_as_null' => false, + + // If enabled, null can be cast to any type and any + // type can be cast to null. Setting this to true + // will cut down on false positives. + 'null_casts_as_any_type' => false, + + // If enabled, Phan will warn if **any** type in a method invocation's object + // is definitely not an object, + // or if **any** type in an invoked expression is not a callable. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_method_checking' => false, + + // If enabled, Phan will warn if **any** type in the argument's union type + // cannot be cast to a type in the parameter's expected union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_param_checking' => false, + + // If enabled, Phan will warn if **any** type in a property assignment's union type + // cannot be cast to a type in the property's declared union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_property_checking' => false, + + // If enabled, Phan will warn if **any** type in a returned value's union type + // cannot be cast to the declared return type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_return_checking' => false, + + // If enabled, Phan will warn if **any** type of the object expression for a property access + // does not contain that property. + 'strict_object_checking' => false, + + // If enabled, Phan will act as though it's certain of real return types of a subset of internal functions, + // even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version). + // + // Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect. + // As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x. + 'assume_real_types_for_internal_functions' => false, + + // If enabled, scalars (int, float, bool, string, null) + // are treated as if they can cast to each other. + // This does not affect checks of array keys. See `scalar_array_key_cast`. + 'scalar_implicit_cast' => false, + + // If enabled, any scalar array keys (int, string) + // are treated as if they can cast to each other. + // E.g. `array` can cast to `array` and vice versa. + // Normally, a scalar type such as int could only cast to/from int and mixed. + 'scalar_array_key_cast' => false, + + // If this has entries, scalars (int, float, bool, string, null) + // are allowed to perform the casts listed. + // + // E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]` + // allows casting null to a string, but not vice versa. + // (subset of `scalar_implicit_cast`) + 'scalar_implicit_partial' => [], + + // If true, Phan will convert the type of a possibly undefined array offset to the nullable, defined equivalent. + // If false, Phan will convert the type of a possibly undefined array offset to the defined equivalent (without converting to nullable). + 'convert_possibly_undefined_offset_to_nullable' => false, + + // If true, seemingly undeclared variables in the global + // scope will be ignored. + // + // This is useful for projects with complicated cross-file + // globals that you have no hope of fixing. + 'ignore_undeclared_variables_in_global_scope' => false, + + // If true, check to make sure the return type declared + // in the doc-block (if any) matches the return type + // declared in the method signature. + 'check_docblock_signature_return_type_match' => true, + + // If true, check to make sure the param types declared + // in the doc-block (if any) matches the param types + // declared in the method signature. + 'check_docblock_signature_param_type_match' => true, + + // If true, make narrowed types from phpdoc params override + // the real types from the signature, when real types exist. + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // + // Affects analysis of the body of the method and the param types passed in by callers. + // + // (*Requires `check_docblock_signature_param_type_match` to be true*) + 'prefer_narrowed_phpdoc_param_type' => true, + + // (*Requires `check_docblock_signature_return_type_match` to be true*) + // + // If true, make narrowed types from phpdoc returns override + // the real types from the signature, when real types exist. + // + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // + // This setting affects the analysis of return statements in the body of the method and the return types passed in by callers. + 'prefer_narrowed_phpdoc_return_type' => true, + + // Set to true in order to attempt to detect dead + // (unreferenced) code. Keep in mind that the + // results will only be a guess given that classes, + // properties, constants and methods can be referenced + // as variables (like `$class->$property` or + // `$class->$method()`) in ways that we're unable + // to make sense of. + // + // To more aggressively detect dead code, + // you may want to set `dead_code_detection_prefer_false_negative` to `false`. + 'dead_code_detection' => false, + + // Set to true in order to attempt to detect unused variables. + // `dead_code_detection` will also enable unused variable detection. + // + // This has a few known false positives, e.g. for loops or branches. + 'unused_variable_detection' => false, + + // Set to true in order to attempt to detect redundant and impossible conditions. + // + // This has some false positives involving loops, + // variables set in branches of loops, and global variables. + 'redundant_condition_detection' => false, + + // Set to true in order to attempt to detect error-prone truthiness/falsiness checks. + // + // This is not suitable for all codebases. + 'error_prone_truthy_condition_detection' => false, + + // Set to true in order to attempt to detect variables that could be replaced with constants or literals. + // (i.e. they are declared once (as a constant expression) and never modified) + // This is almost entirely false positives for most coding styles. + // + // This is intended to be used to check for bugs where a variable such as a boolean was declared but is no longer (or was never) modified. + 'constant_variable_detection' => false, + + // Set to true in order to emit issues such as `PhanUnusedPublicMethodParameter` instead of `PhanUnusedPublicNoOverrideMethodParameter` + // (i.e. assume any non-final non-private method can have overrides). + // This is useful in situations when parsing only a subset of the available files. + 'unused_variable_detection_assume_override_exists' => false, + + // Set this to true in order to aggressively assume class elements aren't overridden when analyzing uses of classes. + // This is useful for standalone applications which have all code analyzed by Phan. + // + // Currently, this just affects inferring that methods without return statements have type `void` + 'assume_no_external_class_overrides' => false, + + // Set to true in order to force tracking references to elements + // (functions/methods/consts/protected). + // + // `dead_code_detection` is another option which also causes references + // to be tracked. + 'force_tracking_references' => false, + + // If true, the dead code detection rig will + // prefer false negatives (not report dead code) to + // false positives (report dead code that is not + // actually dead). + // + // In other words, the graph of references will have + // too many edges rather than too few edges when guesses + // have to be made about what references what. + 'dead_code_detection_prefer_false_negative' => true, + + // If true, then before analysis, try to simplify AST into a form + // which improves Phan's type inference in edge cases. + // + // This may conflict with `dead_code_detection`. + // When this is true, this slows down analysis slightly. + // + // E.g. rewrites `if ($a = value() && $a > 0) {...}` + // into `$a = value(); if ($a) { if ($a > 0) {...}}` + // + // Defaults to true as of Phan 3.0.3. + // This still helps with some edge cases such as assignments in compound conditions. + 'simplify_ast' => true, + + // Enable this to warn about harmless redundant use for classes and namespaces such as `use Foo\bar` in namespace Foo. + // + // Note: This does not affect warnings about redundant uses in the global namespace. + 'warn_about_redundant_use_namespaced_class' => false, + + // If true, Phan will read `class_alias()` calls in the global scope, then + // + // 1. create aliases from the *parsed* files if no class definition was found, and + // 2. emit issues in the global scope if the source or target class is invalid. + // (If there are multiple possible valid original classes for an aliased class name, + // the one which will be created is unspecified.) + // + // NOTE: THIS IS EXPERIMENTAL, and the implementation may change. + 'enable_class_alias_support' => false, + + // If disabled, Phan will not read docblock type + // annotation comments for `@property`. + // + // - When enabled, in addition to inferring existence of magic properties, + // Phan will also warn when writing to `@property-read` and reading from `@property-read`. + // Phan will warn when writing to read-only properties and reading from write-only properties. + // + // Note: `read_type_annotations` must also be enabled. + 'read_magic_property_annotations' => true, + + // If disabled, Phan will not read docblock type + // annotation comments for `@method`. + // + // Note: `read_type_annotations` must also be enabled. + 'read_magic_method_annotations' => true, + + // If disabled, Phan will not read docblock type + // annotation comments for `@mixin`. + // + // Note: `read_type_annotations` must also be enabled. + 'read_mixin_annotations' => true, + + // If disabled, Phan will not read docblock type + // annotation comments (such as for `@return`, `@param`, + // `@var`, `@suppress`, `@deprecated`) and only rely on + // types expressed in code. + 'read_type_annotations' => true, + + // If enabled, Phan will cache ASTs generated by the polyfill/fallback to disk + // (except when running in the background as a language server/daemon) + // + // ASTs generated by the native AST library (php-ast) are never cached, + // because php-ast is faster than loading and unserializing data from the cache. + // + // Disabling this is faster when the cache won't be reused, + // e.g. if this would be run in a docker image without mounting the cache as a volume. + // + // The cache can be found at `sys_get_tmp_dir() . "/phan-$USERNAME"`. + 'cache_polyfill_asts' => true, + + // If enabled, warn about throw statement where the exception types + // are not documented in the PHPDoc of functions, methods, and closures. + 'warn_about_undocumented_throw_statements' => false, + + // If enabled (and `warn_about_undocumented_throw_statements` is enabled), + // Phan will warn about function/closure/method invocations that have `@throws` + // that aren't caught or documented in the invoking method. + + 'warn_about_undocumented_exceptions_thrown_by_invoked_functions' => false, + + // Phan will not warn about lack of documentation of `@throws` for any of the configured classes or their subclasses. + // This only matters when `warn_about_undocumented_throw_statements` is true. + // The default is the empty array (Don't suppress any warnings) + // + // (E.g. `['RuntimeException', 'AssertionError', 'TypeError']`) + 'exception_classes_with_optional_throws_phpdoc' => [ ], + + // This setting maps case-insensitive strings to union types. + // + // This is useful if a project uses phpdoc that differs from the phpdoc2 standard. + // + // If the corresponding value is the empty string, + // then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`) + // + // If the corresponding value is not empty, + // then Phan will act as though it saw the corresponding UnionTypes(s) + // when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc. + // + // This matches the **entire string**, not parts of the string. + // (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting) + // + // (These are not aliases, this setting is ignored outside of doc comments). + // (Phan does not check if classes with these names exist) + // + // Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']` + 'phpdoc_type_mapping' => [ ], + + // Set to true in order to ignore issue suppression. + // This is useful for testing the state of your code, but + // unlikely to be useful outside of that. + 'disable_suppression' => false, + + // Set to true in order to ignore line-based issue suppressions. + // Disabling both line and file-based suppressions is mildly faster. + 'disable_line_based_suppression' => false, + + // Set to true in order to ignore file-based issue suppressions. + 'disable_file_based_suppression' => false, + + // If set to true, we'll dump the AST instead of + // analyzing files + 'dump_ast' => false, + + // If set to a string, we'll dump the fully qualified lowercase + // function and method signatures instead of analyzing files. + 'dump_signatures_file' => null, + + // If set to true, we'll dump the list of files to parse + // to stdout instead of parsing and analyzing files. + 'dump_parsed_file_list' => false, + + // Include a progress bar in the output. + 'progress_bar' => false, + + // When true, use a different version of the progress bar + // that's suitable for Continuous Integration logs. + '__long_progress_bar' => false, + + // If this much time (in seconds) has passed since the last update, + // then update the progress bar. + 'progress_bar_sample_interval' => 0.1, + + // The number of processes to fork off during the analysis + // phase. + 'processes' => 1, + + // Set to true to emit profiling data on how long various + // parts of Phan took to run. You likely don't care to do + // this. + 'profiler_enabled' => false, + + // Phan will give up on suggesting a different name in issue messages + // if the number of candidates (for a given suggestion category) is greater than `suggestion_check_limit`. + // + // Set this to `0` to disable most suggestions for similar names, and only suggest identical names in other namespaces. + // Set this to `PHP_INT_MAX` (or other large value) to always suggest similar names and identical names in other namespaces. + // + // Phan will be a bit slower when this config setting is large. + // A lower value such as 50 works for suggesting misspelled classes/constants in namespaces, + // but won't give you suggestions for globally namespaced functions. + 'suggestion_check_limit' => 1000, + + // Set this to true to disable suggestions for what to use instead of undeclared variables/classes/etc. + 'disable_suggestions' => false, + + // Add any issue types (such as `'PhanUndeclaredMethod'`) + // to this list to inhibit them from being reported. + 'suppress_issue_types' => [ + // 'PhanUndeclaredMethod', + ], + + // If this list is empty, no filter against issues types will be applied. + // If this list is non-empty, only issues within the list + // will be emitted by Phan. + // + // See https://github.com/phan/phan/wiki/Issue-Types-Caught-by-Phan + // for the full list of issues that Phan detects. + // + // Phan is capable of detecting hundreds of types of issues. + // Projects should almost always use `suppress_issue_types` instead. + 'whitelist_issue_types' => [ + // 'PhanUndeclaredClass', + ], + + // A custom list of additional superglobals and their types. **Only needed by projects using runkit/runkit7.** + // + // (Corresponding keys are declared in `runkit.superglobal` ini directive) + // + // `globals_type_map` should be set for setting the types of these superglobals. + // E.g `['_FOO']`; + 'runkit_superglobals' => [], + + // Override to hardcode existence and types of (non-builtin) globals in the global scope. + // Class names should be prefixed with `\`. + // + // (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`) + 'globals_type_map' => [], + + // Enable this to emit issue messages with markdown formatting. + 'markdown_issue_messages' => false, + + // Enable this with `--absolute-path-issue-messages` to use absolute paths in issue messages + 'absolute_path_issue_messages' => false, + + // If true, then hide the issue's column in plaintext and pylint output printers. + // Note that phan only knows the column for a tiny subset of issues. + 'hide_issue_column' => false, + + // Enable this to automatically use colorized phan output for the 'text' output format if the terminal supports it. + // Alternately, set PHAN_ENABLE_COLOR_OUTPUT=1. + // This config setting can be overridden with NO_COLOR=1 or PHAN_DISABLE_COLOR_OUTPUT=1. + 'color_issue_messages_if_supported' => false, + + // Emit colorized issue messages for the 'text' output mode (false by default with the 'text' output mode to supported terminals). + // NOTE: it is strongly recommended to enable this via other methods, + // since this is incompatible with most output formatters. + // + // This can be enabled by setting PHAN_ENABLE_COLOR_OUTPUT=1 or passing `--color` or by setting `color_issue_messages_if_supported` + 'color_issue_messages' => null, + + // In `--output-mode=verbose`, refuse to print lines of context that exceed this limit. + 'max_verbose_snippet_length' => 1000, + + // Allow overriding color scheme in `.phan/config.php` for printing issues, for individual types. + // + // See the keys of `Phan\Output\Colorizing::STYLES` for valid color names, + // and the keys of `Phan\Output\Colorizing::DEFAULT_COLOR_FOR_TEMPLATE` for valid color names. + // + // E.g. to change the color for the file (of an issue instance) to red, set this to `['FILE' => 'red']` + // + // E.g. to use the terminal's default color for the line (of an issue instance), set this to `['LINE' => 'none']` + 'color_scheme' => [], + + // Enable or disable support for generic templated + // class types. + 'generic_types_enabled' => true, + + // Assign files to be analyzed on random processes + // in random order. You very likely don't want to + // set this to true. This is meant for debugging + // and fuzz testing purposes only. + 'randomize_file_order' => false, + + // Setting this to true makes the process assignment for file analysis + // as predictable as possible, using consistent hashing. + // + // Even if files are added or removed, or process counts change, + // relatively few files will move to a different group. + // (use when the number of files is much larger than the process count) + // + // NOTE: If you rely on Phan parsing files/directories in the order + // that they were provided in this config, don't use this. + // See [this note in Phan's wiki](https://github.com/phan/phan/wiki/Different-Issue-Sets-On-Different-Numbers-of-CPUs). + 'consistent_hashing_file_order' => false, + + // Set by `--print-memory-usage-summary`. Prints a memory usage summary to stderr after analysis. + 'print_memory_usage_summary' => false, + + // By default, Phan will log error messages to stdout if PHP is using options that slow the analysis. + // (e.g. PHP is compiled with `--enable-debug` or when using Xdebug) + 'skip_slow_php_options_warning' => false, + + // By default, Phan will warn if the 'tokenizer' module isn't installed and enabled. + 'skip_missing_tokenizer_warning' => false, + + // This is the maximum frame length for crash reports + 'debug_max_frame_length' => 1000, + + // You can put paths to stubs of internal extensions in this config option. + // If the corresponding extension is **not** loaded, then Phan will use the stubs instead. + // Phan will continue using its detailed type annotations, + // but load the constants, classes, functions, and classes (and their Reflection types) + // from these stub files (doubling as valid php files). + // Use a different extension from php to avoid accidentally loading these. + // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now) + // + // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`) + 'autoload_internal_extension_signatures' => [ + ], + + // This can be set to a list of extensions to limit Phan to using the reflection information of. + // If this is a list, then Phan will not use the reflection information of extensions outside of this list. + // The extensions loaded for a given php installation can be seen with `php -m` or `get_loaded_extensions(true)`. + // + // Note that this will only prevent Phan from loading reflection information for extensions outside of this set. + // If you want to add stubs, see `autoload_internal_extension_signatures`. + // + // If this is used, 'core', 'date', 'pcre', 'reflection', 'spl', and 'standard' will be automatically added. + // + // When this is an array, `ignore_undeclared_functions_with_known_signatures` will always be set to false. + // (because many of those functions will be outside of the configured list) + // + // Also see `ignore_undeclared_functions_with_known_signatures` to warn about using unknown functions. + 'included_extension_subset' => null, + + // Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for, + // but aren't available in the codebase, or from Reflection. + // (may lead to false positives if an extension isn't loaded) + // + // If this is true(default), then Phan will not warn. + // + // Even when this is false, Phan will still infer return values and check parameters of internal functions + // if Phan has the signatures. + 'ignore_undeclared_functions_with_known_signatures' => true, + + // If a file to be analyzed can't be parsed, + // then use a slower PHP substitute for php-ast to try to parse the files. + // This setting is ignored if a file is excluded from analysis. + // + // NOTE: it is strongly recommended to enable this via the `--use-fallback-parser` CLI flag instead, + // since this may result in strange error messages for invalid files (e.g. if parsed but not analyzed). + 'use_fallback_parser' => false, + + // Use the polyfill parser based on tolerant-php-parser instead of the possibly missing native implementation + // + // NOTE: This makes parsing several times slower than the native implementation. + // + // NOTE: it is strongly recommended to enable this via the `--use-polyfill-parser` or `--force-polyfill-parser` + // since this may result in strange error messages for invalid files (e.g. if parsed but not analyzed). + 'use_polyfill_parser' => false, + + // Keep a reference to the original tolerant-php-parser node in the generated php-ast Node. + // This is extremely memory intensive, and only recommended if a Phan plugin is used for code reformatting, style checks, etc. + '__parser_keep_original_node' => false, + + // Path to a Unix socket for a daemon to listen to files to analyze. Use command line option instead. + 'daemonize_socket' => false, + + // If a daemon should listen to files to analyze over TCP. + // This setting is mutually exclusive with `daemonize_socket`. + 'daemonize_tcp' => false, + + // TCP host for a daemon to listen to files to analyze. + 'daemonize_tcp_host' => '127.0.0.1', + + // TCP port (from 1024 to 65535) for a daemon to listen to files to analyze. + 'daemonize_tcp_port' => 4846, + + // If this is an array, it configures the way clients will communicate with the Phan language server. + // Possibilities: Exactly one of + // + // 1. `['stdin' => true]` + // 2. `['tcp-server' => string (address this server should listen on)]` + // 3. `['tcp' => string (address client is listening on)]` + 'language_server_config' => false, + + // Valid values: false, true. Should only be set via CLI (`--language-server-analyze-only-on-save`) + 'language_server_analyze_only_on_save' => false, + + // Valid values: null, 'info'. Used when developing or debugging a language server client of Phan. + 'language_server_debug_level' => null, + + // Set this to true to emit all issues detected from the language server (e.g. invalid phpdoc in parsed files), + // not just issues in files currently open in the editor/IDE. + // This can be very verbose and has more false positives. + 'language_server_disable_output_filter' => false, + + // This should only be set by CLI (`--language-server-force-missing-pcntl` or `language-server-require-pcntl`), which will set this to true for debugging. + // When true, this will manually back up the state of the PHP process and restore it. + 'language_server_use_pcntl_fallback' => false, + + // This should only be set via CLI (`--language-server-disable-go-to-definition` to disable) + // Affects "go to definition" and "go to type definition" of LSP. + 'language_server_enable_go_to_definition' => true, + + // This should only be set via CLI (`--language-server-disable-hover` to disable) + // Affects "hover" of LSP. + 'language_server_enable_hover' => true, + + // This should only be set via CLI (`--language-server-disable-completion` to disable) + // Affects "completion" of LSP. + 'language_server_enable_completion' => true, + + // Don't show the category name in issue messages. + // This makes error messages slightly shorter. + // Use `--language-server-hide-category` if you want to enable this. + 'language_server_hide_category_of_issues' => false, + + // Should be configured by --language-server-min-diagnostic-delay-ms. + // Use this for language clients that have race conditions processing diagnostics. + // Max value is 1000 ms. + 'language_server_min_diagnostics_delay_ms' => 0, + + // Set this to false to disable the plugins that Phan uses to infer more accurate return types of `array_map`, `array_filter`, and many other functions. + // + // Phan is slightly faster when these are disabled. + 'enable_internal_return_type_plugins' => true, + + // Set this to true to enable the plugins that Phan uses to infer more accurate return types of `implode`, `json_decode`, and many other functions. + // + // Phan is slightly faster when these are disabled. + 'enable_extended_internal_return_type_plugins' => false, + + // Set this to true to make Phan store a full Context inside variables, instead of a FileRef. This could provide more useful info to plugins, + // but will increase the memory usage by roughly 2.5%. + 'record_variable_context_and_scope' => false, + + // If a literal string type exceeds this length, + // then Phan converts it to a regular string type. + // This setting cannot be less than 50. + // + // This setting can be overridden if users wish to store strings that are even longer than 50 bytes. + 'max_literal_string_type_length' => 200, + + // internal + 'dump_matching_functions' => false, + + // This is the path to a file containing a list of pre-existing issues to ignore, on a per-file basis. + // It's recommended to set this with `--load-baseline=path/to/baseline.php`. + // A baseline file can be created or updated with `--save-baseline=path/to/baseline.php`. + 'baseline_path' => null, + + // For internal use only. + '__save_baseline_path' => null, + + // This is the type of summary comment that will be generated when `--save-baseline=path/to/baseline.php` is used. + // Supported values: 'ordered_by_count' (default), 'ordered_by_type', 'none'. + // (The first type makes it easier to see uncommon issues when reading the code but is more prone to merge conflicts in version control) + // (Does not affect analysis) + 'baseline_summary_type' => 'ordered_by_count', + + // A list of plugin files to execute. + // + // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`) + // + // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/master/.phan/plugins). + // + // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`) + 'plugins' => [ + ], + + // This can be used by third-party plugins that expect configuration. + // + // E.g. this is used by `InvokePHPNativeSyntaxCheckPlugin` + 'plugin_config' => [ + ], + + // This should only be set with `--analyze-twice`. + '__analyze_twice' => false, + + // This should only be set with `--always-exit-successfully-after-analysis` + '__always_exit_successfully_after_analysis' => false, + ]; + + public const COMPLETION_VSCODE = 'vscode'; + + /** + * Disallow the constructor. + */ + private function __construct() + { + } + + /** + * @return string + * Get the root directory of the project that we're + * scanning + * @suppress PhanPossiblyFalseTypeReturn getcwd() can technically be false, but we should have checked earlier + */ + public static function getProjectRootDirectory(): string + { + return self::$project_root_directory ?? \getcwd(); + } + + /** + * @param string $project_root_directory + * Set the root directory of the project that we're + * scanning + */ + public static function setProjectRootDirectory( + string $project_root_directory + ): void { + self::$project_root_directory = $project_root_directory; + } + + /** + * Initializes the configuration used for analysis. + * + * This is automatically called with the defaults, to set any derived configuration and static properties as side effects. + */ + public static function init(): void + { + static $did_init = false; + if ($did_init) { + return; + } + $did_init = true; + self::initOnce(); + } + + private static function initOnce(): void + { + // Trigger magic setters + foreach (self::$configuration as $name => $v) { + self::setValue($name, $v); + } + } + + /** + * @return array + * A map of configuration keys and their values + * + * @suppress PhanUnreferencedPublicMethod useful for plugins, testing, etc. + */ + public static function toArray(): array + { + return self::$configuration; + } + + // method naming is deliberate to make these getters easier to search. + // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps + + /** + * Allow null to be cast as any type and for any + * type to be cast to null. + */ + public static function get_null_casts_as_any_type(): bool + { + return self::$null_casts_as_any_type; + } + + /** + * If enabled, Phan will warn if **any** type in a method's object expression + * is definitely not an object, + * or if **any** type in an invoked expression is not a callable. + */ + public static function get_strict_method_checking(): bool + { + return self::$strict_method_checking; + } + + /** + * If enabled, Phan will warn if **any** type in the argument's type + * cannot be cast to a type in the parameter's expected type. + */ + public static function get_strict_param_checking(): bool + { + return self::$strict_param_checking; + } + + /** + * If enabled, Phan will warn if **any** type in a property assignment's type + * cannot be cast to a type in the property's expected type. + */ + public static function get_strict_property_checking(): bool + { + return self::$strict_property_checking; + } + + /** + * If enabled, Phan will warn if **any** type in the return statement's union type + * cannot be cast to a type in the method's declared return type. + */ + public static function get_strict_return_checking(): bool + { + return self::$strict_return_checking; + } + + /** + * If enabled, Phan will warn if **any** type in the object expression for a property + * does not contain that property. + */ + public static function get_strict_object_checking(): bool + { + return self::$strict_object_checking; + } + + /** If enabled, allow null to cast to any array-like type. */ + public static function get_null_casts_as_array(): bool + { + return self::$null_casts_as_array; + } + + /** If enabled, allow any array-like type to be cast to null. */ + public static function get_array_casts_as_null(): bool + { + return self::$array_casts_as_null; + } + + /** If true, then Phan tracks references to elements */ + public static function get_track_references(): bool + { + return self::$track_references; + } + + /** If true, then Phan enables backwards compatibility checking. */ + public static function get_backward_compatibility_checks(): bool + { + return self::$backward_compatibility_checks; + } + + /** + * If true, then Phan runs a quick version of checks that takes less + * time at the cost of not running as thorough + * of an analysis. + */ + public static function get_quick_mode(): bool + { + return self::$quick_mode; + } + + /** @return int the 5-digit PHP version id which is closest to matching the PHP_VERSION_ID for the 'target_php_version' string */ + public static function get_closest_target_php_version_id(): int + { + return self::$closest_target_php_version_id; + } + + /** @return int the 5-digit PHP version id which is closest to matching the PHP_VERSION_ID for the 'minimum_target_php_version' string */ + public static function get_closest_minimum_target_php_version_id(): int + { + return self::$closest_minimum_target_php_version_id; + } + // phpcs:enable PSR1.Methods.CamelCapsMethodName.NotCamelCaps + + /** + * @return mixed + * @phan-hardcode-return-type + */ + public static function getValue(string $name) + { + return self::$configuration[$name]; + } + + /** + * Resets the configuration to the initial state, prior to parsing config files and CLI arguments. + * @internal - this should only be used in unit tests. + */ + public static function reset(): void + { + self::$configuration = self::DEFAULT_CONFIGURATION; + // Trigger magic behavior + self::initOnce(); + } + + /** + * @param string $name + * @param mixed $value + */ + public static function setValue(string $name, $value): void + { + self::$configuration[$name] = $value; + switch ($name) { + case 'ignore_undeclared_functions_with_known_signatures': + case 'included_extension_subset': + if (is_array(self::$configuration['included_extension_subset'])) { + self::$configuration['ignore_undeclared_functions_with_known_signatures'] = false; + } + break; + case 'null_casts_as_any_type': + self::$null_casts_as_any_type = $value; + break; + case 'null_casts_as_array': + self::$null_casts_as_array = $value; + break; + case 'array_casts_as_null': + self::$array_casts_as_null = $value; + break; + case 'strict_method_checking': + self::$strict_method_checking = $value; + break; + case 'strict_param_checking': + self::$strict_param_checking = $value; + break; + case 'strict_property_checking': + self::$strict_property_checking = $value; + break; + case 'strict_return_checking': + self::$strict_return_checking = $value; + break; + case 'strict_object_checking': + self::$strict_object_checking = $value; + break; + case 'dead_code_detection': + case 'force_tracking_references': + self::$track_references = self::getValue('dead_code_detection') || self::getValue('force_tracking_references'); + break; + case 'backward_compatibility_checks': + self::$backward_compatibility_checks = $value; + break; + case 'quick_mode': + self::$quick_mode = $value; + break; + case 'allow_method_param_type_widening': + self::$configuration['allow_method_param_type_widening_original'] = $value; + if ($value === null) { + // If this setting is set to null, infer it based on the closest php version id. + self::$configuration[$name] = self::$closest_minimum_target_php_version_id >= 70200; + } + break; + case 'target_php_version': + case 'minimum_target_php_version': + self::$configuration[$name] = $value; + self::updateClosestTargetPHPVersion(); + break; + case 'exclude_analysis_directory_list': + self::$configuration['__exclude_analysis_regex'] = self::generateDirectoryListRegex($value); + break; + case 'directory_list': + self::$configuration['__directory_regex'] = self::generateDirectoryListRegex($value); + break; + case 'scalar_implicit_partial': + self::$configuration[$name] = self::normalizeScalarImplicitPartial($value); + break; + } + } + + private const TRUTHY_SCALAR_EQUIVALENTS = [ + 'int' => 'non-zero-int', + 'string' => 'non-empty-string', + ]; + + private static function updateClosestTargetPHPVersion(): void + { + $value = self::$configuration['target_php_version']; + if (is_int($value) || is_float($value)) { + $value = \sprintf("%.1f", $value); + } + // @phan-suppress-next-line PhanSuspiciousTruthyString, PhanSuspiciousTruthyCondition + $value = (string) ($value ?: PHP_VERSION); + if (\strtolower($value) === 'native') { + $value = PHP_VERSION; + } + + self::$closest_target_php_version_id = self::computeClosestTargetPHPVersionId($value); + + $min_value = self::$configuration['minimum_target_php_version']; + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $min_value_id = StringUtil::isNonZeroLengthString($min_value) ? self::computeClosestTargetPHPVersionId($min_value) : null; + + // @phan-suppress-next-line PhanSuspiciousTruthyString, PhanSuspiciousTruthyCondition + if (!$min_value_id) { + $min_value_id = self::determineMinimumPHPVersionFromComposer() ?? $min_value_id; + } + if (!$min_value_id) { + $min_value_id = self::computeClosestTargetPHPVersionId(PHP_VERSION); + } + self::$closest_minimum_target_php_version_id = (int) \min(self::$closest_target_php_version_id, $min_value_id); + if (!isset(self::$configuration['allow_method_param_type_widening_original'])) { + self::$configuration['allow_method_param_type_widening'] = self::$closest_minimum_target_php_version_id >= 70200; + } + } + + /** + * Guess minimum_target_php_version based on composer.json supported versions + */ + private static function determineMinimumPHPVersionFromComposer(): ?int + { + $settings = self::readComposerSettings(); + [$version, $_] = Initializer::determineTargetPHPVersion($settings); + + if (is_string($version)) { + return self::computeClosestTargetPHPVersionId($version); + } + return null; + } + + /** + * Read the composer settings if this phan project is a composer project. + * + * @return array + */ + private static function readComposerSettings(): array + { + static $contents = null; + if (is_array($contents)) { + return $contents; + } + $path_to_composer_json = \getcwd() . "/composer.json"; + if (!\file_exists($path_to_composer_json)) { + return $contents = []; + } + // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal + $composer_json_contents = @\file_get_contents($path_to_composer_json); + if (!is_string($composer_json_contents)) { + return $contents = []; + } + $library_composer_settings = @\json_decode($composer_json_contents, true); + if (!is_array($library_composer_settings)) { + CLI::printWarningToStderr("Saw invalid composer.json file contents when reading project settings: " . \json_last_error_msg() . "\n"); + return $contents = []; + } + return $library_composer_settings; + } + + /** + * If int can cast to/from T, where T is possibly not falsey, + * then allow non-zero-int to cast to/from T. + * + * @param array> $value + * @return array> + * @suppress PhanPluginCanUseParamType + */ + private static function normalizeScalarImplicitPartial($value): array + { + if (!is_array($value)) { + return []; + } + foreach (self::TRUTHY_SCALAR_EQUIVALENTS as $scalar => $non_falsey_scalar) { + if (isset($value[$scalar]) && !isset($value[$non_falsey_scalar])) { + $value[$non_falsey_scalar] = \array_values(\array_filter( + $value[$scalar], + static function (string $type): bool { + return !in_array($type, ['null', 'false'], true); + } + )); + } + } + foreach ($value as $key => &$allowed_casts) { + if (in_array($key, ['null', 'false'], true)) { + continue; + } + foreach (self::TRUTHY_SCALAR_EQUIVALENTS as $scalar => $non_falsey_scalar) { + if (in_array($scalar, $allowed_casts, true) && !in_array($non_falsey_scalar, $allowed_casts, true)) { + $allowed_casts[] = $non_falsey_scalar; + } + } + } + return $value; + } + + /** + * @param string[] $value + */ + private static function generateDirectoryListRegex(array $value): ?string + { + if (!$value) { + return null; + } + $parts = \array_map(static function (string $path): string { + $path = \str_replace('\\', '/', $path); // Normalize \\ to / in configs + $path = \rtrim($path, '\//'); // remove trailing / from directory + $path = \preg_replace('@^(\./)+@', '', $path); // Remove any number of leading ./ sections + return \preg_quote($path, '@'); // Quote this + }, $value); + + return '@^(\./)*(' . \implode('|', $parts) . ')([/\\\\]|$)@'; + } + + private static function computeClosestTargetPHPVersionId(string $version): int + { + if (\version_compare($version, '6.0') < 0) { + return 50600; + } elseif (\version_compare($version, '7.1') < 0) { + return 70000; + } elseif (\version_compare($version, '7.2') < 0) { + return 70100; + } elseif (\version_compare($version, '7.3') < 0) { + return 70200; + } elseif (\version_compare($version, '7.4') < 0) { + return 70300; + } elseif (\version_compare($version, '8.0') < 0) { + return 70400; + } else { + return 80000; + } + } + + /** + * @return string + * The relative path appended to the project root directory. (i.e. the absolute path) + * + * @suppress PhanUnreferencedPublicMethod + * @see FileRef::getProjectRelativePathForPath() for converting to relative paths + * NOTE: This deliberately does not support phar:// URLs, because those evaluate php code when the phar is first loaded. + */ + public static function projectPath(string $relative_path): string + { + return Paths::toAbsolutePath(self::getProjectRootDirectory(), $relative_path); + } + + /** + * @param mixed $value + */ + private static function errSuffixGotType($value): string + { + return ", but got type '" . gettype($value) . "'"; + } + + /** + * @param array $configuration + * @return list a list of 0 or more error messages for invalid config settings + */ + public static function getConfigErrors(array $configuration): array + { + /** + * @param mixed $value + */ + $is_scalar = static function ($value): ?string { + if (is_null($value) || \is_scalar($value)) { + return null; + } + return 'Expected a scalar' . self::errSuffixGotType($value); + }; + /** + * @param mixed $value + */ + $is_bool = static function ($value): ?string { + if (is_bool($value)) { + return null; + } + return 'Expected a boolean' . self::errSuffixGotType($value); + }; + /** + * @param mixed $value + */ + $is_bool_or_null = static function ($value): ?string { + if (is_bool($value) || is_null($value)) { + return null; + } + return 'Expected a boolean' . self::errSuffixGotType($value); + }; + /** + * @param mixed $value + */ + $is_string_or_null = static function ($value): ?string { + if (is_null($value) || is_string($value)) { + return null; + } + return 'Expected a string' . self::errSuffixGotType($value); + }; + /** + * @param mixed $value + */ + $is_string = static function ($value): ?string { + if (is_string($value)) { + return null; + } + return 'Expected a string' . self::errSuffixGotType($value); + }; + /** + * @param mixed $value + */ + $is_array = static function ($value): ?string { + if (is_array($value)) { + return null; + } + return 'Expected an array' . self::errSuffixGotType($value); + }; + /** + * @param mixed $value + */ + $is_int_strict = static function ($value): ?string { + if (is_int($value)) { + return null; + } + return 'Expected an integer' . self::errSuffixGotType($value); + }; + /** + * @param mixed $value + */ + $is_string_list = static function ($value): ?string { + if (!is_array($value)) { + return 'Expected a list of strings' . self::errSuffixGotType($value); + } + foreach ($value as $i => $element) { + if (!is_string($element)) { + return "Expected a list of strings: index $i is type '" . gettype($element) . "'"; + } + } + return null; + }; + /** + * @param mixed $value + */ + $is_string_list_or_null = static function ($value): ?string { + if (is_null($value)) { + return null; + } + if (!is_array($value)) { + return 'Expected null or a list of strings' . self::errSuffixGotType($value); + } + foreach ($value as $i => $element) { + if (!is_string($element)) { + return "Expected null or a list of strings: index $i is type '" . gettype($element) . "'"; + } + } + return null; + }; + /** + * @param mixed $value + */ + $is_associative_string_array = static function ($value): ?string { + if (!is_array($value)) { + return 'Expected an associative array mapping strings to strings' . self::errSuffixGotType($value); + } + foreach ($value as $i => $element) { + if (!is_string($element)) { + return "Expected an associative array mapping strings to strings: index $i is '" . gettype($element) . "'"; + } + } + return null; + }; + $config_checks = [ + 'absolute_path_issue_messages' => $is_bool, + 'allow_method_param_type_widening' => $is_bool_or_null, + 'allow_missing_properties' => $is_bool, + 'analyzed_file_extensions' => $is_string_list, + 'analyze_signature_compatibility' => $is_bool, + 'array_casts_as_null' => $is_bool, + 'autoload_internal_extension_signatures' => $is_associative_string_array, + 'included_extension_subset' => $is_string_list_or_null, + 'backward_compatibility_checks' => $is_bool, + 'baseline_path' => $is_string_or_null, + 'baseline_summary_type' => $is_string, + 'cache_polyfill_asts' => $is_bool, + 'check_docblock_signature_param_type_match' => $is_bool, + 'check_docblock_signature_return_type_match' => $is_bool, + 'color_issue_messages' => $is_bool_or_null, + 'color_scheme' => $is_associative_string_array, + 'consistent_hashing_file_order' => $is_bool, + 'daemonize_socket' => $is_scalar, + 'daemonize_tcp_host' => $is_string, + 'daemonize_tcp' => $is_bool, + 'daemonize_tcp_port' => $is_int_strict, + 'dead_code_detection' => $is_bool, + 'dead_code_detection_prefer_false_negative' => $is_bool, + 'directory_list' => $is_string_list, + 'disable_line_based_suppression' => $is_bool, + 'disable_suggestions' => $is_bool, + 'disable_suppression' => $is_bool, + 'dump_ast' => $is_bool, + 'dump_matching_functions' => $is_bool, + 'dump_parsed_file_list' => $is_bool, + 'dump_signatures_file' => $is_string_or_null, + 'enable_class_alias_support' => $is_bool, + 'enable_include_path_checks' => $is_bool, + 'enable_internal_return_type_plugins' => $is_bool, + 'exception_classes_with_optional_throws_phpdoc' => $is_string_list, + 'exclude_analysis_directory_list' => $is_string_list, + 'exclude_file_list' => $is_string_list, + 'exclude_file_regex' => $is_string_or_null, + 'file_list' => $is_string_list, + 'force_tracking_references' => $is_bool, + 'generic_types_enabled' => $is_bool, + 'globals_type_map' => $is_associative_string_array, + 'guess_unknown_parameter_type_using_default' => $is_bool, + 'hide_issue_column' => $is_bool, + 'infer_default_properties_in_construct' => $is_bool, + 'ignore_undeclared_functions_with_known_signatures' => $is_bool, + 'ignore_undeclared_variables_in_global_scope' => $is_bool, + 'include_analysis_file_list' => $is_string_list, + 'include_paths' => $is_string_list, + 'inherit_phpdoc_types' => $is_bool, + 'language_server_analyze_only_on_save' => $is_bool, + // 'language_server_config' => array|false, // should not be set directly + 'language_server_debug_level' => $is_string_or_null, + 'language_server_disable_output_filter' => $is_bool, + 'language_server_enable_completion' => $is_scalar, + 'language_server_enable_go_to_definition' => $is_bool, + 'language_server_enable_hover' => $is_bool, + 'language_server_hide_category_of_issues' => $is_bool, + 'language_server_use_pcntl_fallback' => $is_bool, + 'long_progress_bar' => $is_bool, + 'markdown_issue_messages' => $is_bool, + 'max_literal_string_type_length' => $is_int_strict, + 'max_verbose_snippet_length' => $is_int_strict, + 'minimum_severity' => $is_int_strict, + 'null_casts_as_any_type' => $is_bool, + 'null_casts_as_array' => $is_bool, + 'parent_constructor_required' => $is_string_list, + 'phpdoc_type_mapping' => $is_associative_string_array, + 'plugin_config' => $is_array, + 'plugins' => $is_string_list, + 'polyfill_parse_all_element_doc_comments' => $is_bool, + 'prefer_narrowed_phpdoc_param_type' => $is_bool, + 'prefer_narrowed_phpdoc_return_type' => $is_bool, + 'pretend_newer_core_methods_exist' => $is_bool, + 'print_memory_usage_summary' => $is_bool, + 'processes' => $is_int_strict, + 'profiler_enabled' => $is_bool, + 'progress_bar' => $is_bool, + 'progress_bar_sample_interval' => $is_scalar, + 'quick_mode' => $is_bool, + 'randomize_file_order' => $is_bool, + 'read_magic_method_annotations' => $is_bool, + 'read_magic_property_annotations' => $is_bool, + 'read_type_annotations' => $is_bool, + 'runkit_superglobals' => $is_string_list, + 'scalar_array_key_cast' => $is_bool, + 'scalar_implicit_cast' => $is_bool, + 'scalar_implicit_partial' => $is_array, + 'simplify_ast' => $is_bool, + 'skip_missing_tokenizer_warning' => $is_bool, + 'skip_slow_php_options_warning' => $is_bool, + 'strict_method_checking' => $is_bool, + 'strict_param_checking' => $is_bool, + 'strict_property_checking' => $is_bool, + 'strict_return_checking' => $is_bool, + 'strict_object_checking' => $is_bool, + 'suggestion_check_limit' => $is_int_strict, + 'suppress_issue_types' => $is_string_list, + 'target_php_version' => $is_scalar, + 'unused_variable_detection' => $is_bool, + 'redundant_condition_detection' => $is_bool, + 'assume_real_types_for_internal_functions' => $is_bool, + 'use_fallback_parser' => $is_bool, + 'use_polyfill_parser' => $is_bool, + 'warn_about_redundant_use_namespaced_class' => $is_bool, + 'warn_about_relative_include_statement' => $is_bool, + 'warn_about_undocumented_exceptions_thrown_by_invoked_functions' => $is_bool, + 'warn_about_undocumented_throw_statements' => $is_bool, + 'whitelist_issue_types' => $is_string_list, + ]; + $result = []; + foreach ($config_checks as $config_name => $check_closure) { + if (!array_key_exists($config_name, $configuration)) { + continue; + } + $value = $configuration[$config_name]; + $error = $check_closure($value); + if (StringUtil::isNonZeroLengthString($error)) { + $result[] = "Invalid config value for '$config_name': $error"; + } + } + return $result; + } + + /** + * Prints errors to stderr if any config options are definitely invalid. + */ + public static function warnIfInvalid(): void + { + $errors = self::getConfigErrors(self::$configuration); + foreach ($errors as $error) { + // @phan-suppress-next-line PhanPluginRemoveDebugCall + \fwrite(STDERR, $error . PHP_EOL); + } + } + + /** + * Check if the issue fixing plugin (from --automatic-fix) is enabled. + */ + public static function isIssueFixingPluginEnabled(): bool + { + return \in_array(__DIR__ . '/Plugin/Internal/IssueFixingPlugin.php', Config::getValue('plugins'), true); + } + + /** + * Fetches the value of language_server_min_diagnostics_delay_ms, constrained to 0..1000ms + */ + public static function getMinDiagnosticsDelayMs(): float + { + $delay = Config::getValue('language_server_min_diagnostics_delay_ms'); + if (\is_numeric($delay) && $delay > 0) { + return \min((float)$delay, 1000); + } + return 0; + } +} + +// Call init() to trigger the magic setters. +Config::init(); diff --git a/bundled-libs/phan/phan/src/Phan/Config/InitializedSettings.php b/bundled-libs/phan/phan/src/Phan/Config/InitializedSettings.php new file mode 100644 index 000000000..0e453d564 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Config/InitializedSettings.php @@ -0,0 +1,36 @@ + the values for setting names*/ + public $settings; + + /** @var array> comments for settings */ + public $comment_lines; + + /** @var int the init-level CLI option used to generate the settings. Smaller numbers mean a stricter config. */ + public $init_level; + + /** + * @param array $data + * @param array> $comment_lines + */ + public function __construct( + array $data, + array $comment_lines, + int $init_level + ) { + $this->settings = $data; + $this->comment_lines = $comment_lines; + $this->init_level = $init_level; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Config/Initializer.php b/bundled-libs/phan/phan/src/Phan/Config/Initializer.php new file mode 100644 index 000000000..f737fdc6c --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Config/Initializer.php @@ -0,0 +1,599 @@ + maps a config name to a list of comment lines about that config + */ + public static function computeCommentNameDocumentationMap(): array + { + // Hackish way of extracting comment lines from Config::DEFAULT_CONFIGURATION + // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal + $config_file_lines = \explode("\n", \file_get_contents(\dirname(__DIR__) . '/Config.php')); + $prev_lines = []; + $result = []; + foreach ($config_file_lines as $line) { + if (\preg_match("/^ (['\"])([a-z0-9A-Z_]+)\\1\s*=>/", $line, $matches)) { + $config_name = $matches[2]; + if (count($prev_lines) > 0) { + $result[$config_name] = $prev_lines; + } + $prev_lines = []; + continue; + } + if (\preg_match('@^\s*//@', $line)) { + $prev_lines[] = \trim($line); + } else { + $prev_lines = []; + } + } + return $result; + } + + /** + * Returns indented PHP comment lines to use for the comment on $setting_name. + * Returns the empty string if nothing could be generated. + */ + public static function generateCommentForSetting(string $setting_name): string + { + static $comment_source = null; + if (is_null($comment_source)) { + $comment_source = self::computeCommentNameDocumentationMap(); + } + $lines = $comment_source[$setting_name] ?? null; + if ($lines === null) { + return ''; + } + return \implode('', \array_map(static function (string $line): string { + return " $line\n"; + }, $lines)); + } + + /** + * @param string $setting_name + * @param string|int|float|bool|array|null $setting_value + * @param list $additional_comment_lines + */ + public static function generateEntrySnippetForSetting(string $setting_name, $setting_value, array $additional_comment_lines): string + { + $source = self::generateCommentForSetting($setting_name); + foreach ($additional_comment_lines as $line) { + $source .= " // $line\n"; + } + $source .= ' '; + $source .= \var_export($setting_name, true) . ' => '; + if (is_array($setting_value)) { + if (count($setting_value) > 0) { + $source .= "[\n"; + foreach ($setting_value as $key => $element) { + if (!is_int($key)) { + throw new TypeError("Expected setting default for $setting_name to have consecutive integer keys"); + } + $source .= ' ' . StringUtil::varExportPretty($element) . ",\n"; + } + $source .= " ],\n"; + } else { + $source .= "[],\n"; + } + } else { + $encoded_value = StringUtil::varExportPretty($setting_value); + if ($setting_name === 'minimum_severity') { + switch ($setting_value) { + case Issue::SEVERITY_LOW: + $encoded_value = 'Issue::SEVERITY_LOW'; + break; + case Issue::SEVERITY_NORMAL: + $encoded_value = 'Issue::SEVERITY_NORMAL'; + break; + case Issue::SEVERITY_CRITICAL: + $encoded_value = 'Issue::SEVERITY_CRITICAL'; + break; + } + } + + $source .= "$encoded_value,\n"; + } + return $source; + } + + /** + * Returns a string containing the full source to use for the generated `.phan/config.php` + */ + public static function generatePhanConfigFileContents(InitializedSettings $settings_object): string + { + $phan_settings = $settings_object->settings; + $init_level = $settings_object->init_level; + $comment_lines = $settings_object->comment_lines; + + $source = << $setting_value) { + $source .= "\n"; + $source .= self::generateEntrySnippetForSetting($setting_name, $setting_value, $comment_lines[$setting_name] ?? []); + } + $source .= "];\n"; + return $source; + } + + public const LEVEL_MAP = [ + 'strict' => 1, + 'strong' => 2, + 'average' => 3, + 'normal' => 3, + 'weak' => 4, + 'weakest' => 5, + ]; + + /** + * @param array $composer_settings (can be empty for --init-no-composer) + * @param ?string $vendor_path (can be null for --init-no-composer) + * @param array{init-analyze-file?:string,init-overwrite?:mixed,init-no-composer?:mixed,init-level?:(int|string)} $opts parsed from getopt + * @throws UsageException if provided settings are invalid + * @internal + */ + public static function createPhanSettingsForComposerSettings(array $composer_settings, ?string $vendor_path, array $opts): InitializedSettings + { + $level = $opts['init-level'] ?? 3; + $level = self::LEVEL_MAP[\strtolower((string)$level)] ?? $level; + if (\filter_var($level, FILTER_VALIDATE_INT) === false) { + throw new UsageException("Invalid --init-level=$level", EXIT_FAILURE, UsageException::PRINT_INIT_ONLY); + } + $level = \max(1, \min(5, (int)$level)); + $is_strongest_level = $level === 1; + $is_strong_or_weaker_level = $level >= 2; + $is_average_level = $level >= 3; + $is_weak_level = $level >= 4; + $is_weakest_level = $level >= 5; + + $cwd = \getcwd(); + [$project_directory_list, $project_file_list] = self::extractAutoloadFilesAndDirectories('', $composer_settings); + $minimum_severity = $is_weak_level ? Issue::SEVERITY_NORMAL : Issue::SEVERITY_LOW; + if ($is_weakest_level) { + $plugins = []; + } elseif ($is_average_level) { + $plugins = [ + 'AlwaysReturnPlugin', + 'PregRegexCheckerPlugin', + 'UnreachableCodePlugin', + ]; + } else { + $plugins = [ + 'AlwaysReturnPlugin', + 'DollarDollarPlugin', + 'DuplicateArrayKeyPlugin', + 'DuplicateExpressionPlugin', + 'PregRegexCheckerPlugin', + 'PrintfCheckerPlugin', + 'SleepCheckerPlugin', + 'UnreachableCodePlugin', + 'UseReturnValuePlugin', + 'EmptyStatementListPlugin', + ]; + } + if ($is_strongest_level) { + $plugins[] = 'StrictComparisonPlugin'; + $plugins[] = 'LoopVariableReusePlugin'; + } + + $comments = []; + [$target_php_version, $comments['target_php_version']] = self::determineTargetPHPVersion($composer_settings); + + $phan_settings = [ + 'target_php_version' => $target_php_version, + 'allow_missing_properties' => $is_weak_level, + 'null_casts_as_any_type' => $is_weak_level, + 'null_casts_as_array' => $is_average_level, + 'array_casts_as_null' => $is_average_level, + 'scalar_implicit_cast' => $is_weak_level, + 'scalar_array_key_cast' => $is_average_level, + // TODO: Migrate to a smaller subset scalar_implicit_partial as analysis gets stricter? + 'scalar_implicit_partial' => [], + 'strict_method_checking' => !$is_average_level, + // strict param/return checking has a lot of false positives. Limit it to the strongest analysis level. + 'strict_object_checking' => $is_strongest_level, + 'strict_param_checking' => $is_strongest_level, + 'strict_property_checking' => $is_strongest_level, + 'strict_return_checking' => $is_strongest_level, + 'ignore_undeclared_variables_in_global_scope' => $is_average_level, + 'ignore_undeclared_functions_with_known_signatures' => $is_strong_or_weaker_level, + 'backward_compatibility_checks' => false, // this is only useful for migrating from php5 + 'check_docblock_signature_return_type_match' => !$is_average_level, + 'phpdoc_type_mapping' => [], + 'dead_code_detection' => false, // this is slow + 'unused_variable_detection' => !$is_average_level, + 'redundant_condition_detection' => !$is_average_level, + 'assume_real_types_for_internal_functions' => !$is_average_level, + 'quick_mode' => $is_weakest_level, + 'globals_type_map' => [], + 'minimum_severity' => $minimum_severity, + 'suppress_issue_types' => [], + 'exclude_file_regex' => $vendor_path !== null ? '@^vendor/.*/(tests?|Tests?)/@' : null, + 'exclude_file_list' => [], + 'exclude_analysis_directory_list' => $vendor_path !== null ? [ + 'vendor/' + ] : [], + 'enable_include_path_checks' => !$is_weak_level, + 'processes' => 1, + 'analyzed_file_extensions' => ['php'], + 'autoload_internal_extension_signatures' => [], + 'plugins' => $plugins, + ]; + + $phan_directory_list = $project_directory_list; + $phan_file_list = $project_file_list; + + // TODO: Figure out which require-dev directories can be skipped + $require_directories = $composer_settings['require'] ?? []; + $require_dev_directories = $composer_settings['require-dev'] ?? []; + foreach (\array_merge($require_directories, $require_dev_directories) as $requirement => $_) { + if (\substr_count($requirement, '/') !== 1) { + // e.g. ext-ast, php >= 7.0, etc. + continue; + } + $path_to_require = "$vendor_path/$requirement"; + if (!\is_dir($path_to_require)) { + $requirement = \strtolower($requirement); + $path_to_require = "$vendor_path/$requirement"; + if (!\is_dir($path_to_require)) { + echo CLI::colorizeHelpSectionIfSupported("WARNING: ") . "Directory $path_to_require does not exist, continuing\n"; + continue; + } + } + $path_to_composer_json = "$path_to_require/composer.json"; + if (!\file_exists($path_to_composer_json)) { + echo CLI::colorizeHelpSectionIfSupported("WARNING: ") . "$path_to_composer_json does not exist, continuing\n"; + continue; + } + // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal + $library_composer_settings = \json_decode(\file_get_contents($path_to_composer_json), true); + if (!is_array($library_composer_settings)) { + echo CLI::colorizeHelpSectionIfSupported("WARNING: ") . "$path_to_composer_json contains invalid JSON, continuing\n"; + continue; + } + + [$library_directory_list, $library_file_list] = self::extractAutoloadFilesAndDirectories("vendor/$requirement", $library_composer_settings); + $phan_directory_list = \array_merge($phan_directory_list, $library_directory_list); + $phan_file_list = \array_merge($phan_file_list, $library_file_list); + } + foreach (self::getArrayOption($opts, 'init-analyze-dir') as $extra_dir) { + $path_to_require = "$cwd/$extra_dir"; + if (!\is_dir($path_to_require)) { + throw new UsageException("phan --init-analyze-dir was given a missing/invalid relative directory '$extra_dir'", EXIT_FAILURE, UsageException::PRINT_INIT_ONLY); + } + $phan_directory_list[] = $extra_dir; + } + + foreach ($composer_settings['bin'] ?? [] as $relative_path_to_binary) { + if (self::isPHPBinary($relative_path_to_binary)) { + $phan_file_list[] = $relative_path_to_binary; + } + } + foreach (self::getArrayOption($opts, 'init-analyze-file') as $extra_file) { + $path_to_require = "$cwd/$extra_file"; + if (!\is_file($path_to_require)) { + throw new UsageException("phan --init-analyze-file was given a missing/invalid relative file '$extra_file'", EXIT_FAILURE, UsageException::PRINT_INIT_ONLY); + } + $phan_file_list[] = $extra_file; + } + if ($vendor_path !== null && count($project_directory_list) === 0 && count($project_file_list) === 0 && count($phan_file_list) === 0 && count($phan_directory_list) === 0) { + throw new UsageException('phan --init expects composer.json to contain "autoload" psr-4 directories (and could not determine any directories or files to analyze)', EXIT_FAILURE, UsageException::PRINT_INIT_ONLY); + } + + if (count($phan_file_list) === 0 && count($phan_directory_list) === 0) { + throw new UsageException("phan --init failed to find any directories or files to analyze, giving up.", EXIT_FAILURE, UsageException::PRINT_INIT_ONLY); + } + \sort($phan_directory_list); + \sort($phan_file_list); + + $phan_settings['directory_list'] = \array_unique($phan_directory_list); + $phan_settings['file_list'] = \array_unique($phan_file_list); + return new InitializedSettings($phan_settings, $comments, $level); + } + + /** + * @param array $composer_settings parsed from composer.json + * @return array{0:?string,1:list} + */ + public static function determineTargetPHPVersion(array $composer_settings): array + { + $php_version_constraint = $composer_settings['require']['php'] ?? null; + if (!$php_version_constraint || !is_string($php_version_constraint)) { + return [null, ['TODO: Choose a target_php_version for this project, or leave as null and remove this comment']]; + } + try { + $version_constraint = self::parseConstraintsForRange($php_version_constraint); + } catch (\UnexpectedValueException $_) { + return [null, ['TODO: Choose a target_php_version for this project, or leave as null and remove this comment']]; + } + // Not going to suggest 5.6 - analyzing with 7.0 might detect some functions that were removed + if ($version_constraint->matches(self::parseConstraintsForRange('<7.1-dev'))) { + $version_guess = '7.0'; + } elseif ($version_constraint->matches(self::parseConstraintsForRange('<7.2-dev'))) { + $version_guess = '7.1'; + } elseif ($version_constraint->matches(self::parseConstraintsForRange('<7.3-dev'))) { + $version_guess = '7.2'; + } elseif ($version_constraint->matches(self::parseConstraintsForRange('<7.4-dev'))) { + $version_guess = '7.3'; + } elseif ($version_constraint->matches(self::parseConstraintsForRange('<8.0-dev'))) { + $version_guess = '7.4'; + } elseif ($version_constraint->matches(self::parseConstraintsForRange('>=8.0-dev'))) { + $version_guess = '8.0'; + } else { + return [null, ['TODO: Choose a target_php_version for this project, or leave as null and remove this comment']]; + } + return [$version_guess, ['Automatically inferred from composer.json requirement for "php" of ' . \json_encode($php_version_constraint)]]; + } + + private static function parseConstraintsForRange(string $constraints): ConstraintInterface + { + return (new VersionParser())->parseConstraints($constraints); + } + + /** + * @param array $composer_settings settings parsed from composer.json + * @return list> [$directory_list, $file_list] + */ + private static function extractAutoloadFilesAndDirectories(string $relative_dir, array $composer_settings): array + { + $directory_list = []; + $file_list = []; + $autoload_setting = $composer_settings['autoload'] ?? []; + $autoload_directories = \array_merge( + $autoload_setting['psr-4'] ?? [], + $autoload_setting['psr-0'] ?? [], + $autoload_setting['classmap'] ?? [] + ); + + foreach ($autoload_directories as $lib_list) { + if (is_string($lib_list)) { + $lib_list = [$lib_list]; + } + foreach ($lib_list as $lib) { + if (!is_string($lib)) { + echo CLI::colorizeHelpSectionIfSupported("WARNING: ") . "unexpected autoload field in '$relative_dir/composer.json'\n"; + continue; + } + $composer_lib_relative_path = "$relative_dir/$lib"; + $composer_lib_absolute_path = \getcwd() . "/$composer_lib_relative_path"; + if (!\file_exists($composer_lib_absolute_path)) { + echo CLI::colorizeHelpSectionIfSupported("WARNING: ") . "could not find '$composer_lib_relative_path'\n"; + continue; + } + $composer_lib_relative_path = \trim(\str_replace(\DIRECTORY_SEPARATOR, '/', $composer_lib_relative_path), '/'); + + $composer_lib_relative_path = \preg_replace('@(/+\.)+$@D', '', $composer_lib_relative_path); + if (\is_dir($composer_lib_absolute_path)) { + $directory_list[] = \trim($composer_lib_relative_path, '/'); + } elseif (\is_file($composer_lib_relative_path)) { + $file_list[] = \trim($composer_lib_relative_path, '/'); + } + } + } + return self::filterDirectoryAndFileList($directory_list, $file_list); + } + + /** + * Sort and return the unique directories and files to be added to the Phan config. + * (don't return directories/files within other directories) + * + * @param list $directory_list + * @param list $file_list + * @return list> [$directory_list, $file_list] + */ + public static function filterDirectoryAndFileList(array $directory_list, array $file_list): array + { + \sort($directory_list); + \sort($file_list); + if (count($directory_list) > 0) { + $filter = self::createNotInDirectoryFilter($directory_list); + $directory_list = \array_filter($directory_list, $filter); + $file_list = \array_filter($file_list, $filter); + } + return [ + \array_values(\array_unique($directory_list)), + \array_values(\array_unique($file_list)) + ]; + } + + /** + * @param string[] $directory_list + * @return Closure(string):bool a closure that returns true if the passed in file is not within any folders in $directory_list + */ + private static function createNotInDirectoryFilter(array $directory_list): Closure + { + $parts = \array_map(static function (string $path): string { + if ($path === '.') { + // Probably unnecessary to try to handle absolute paths and ../ in composer libraries. + return '((?!(/|\.\.[/\\\\]|\w:\\\\)).*)'; + } + return \preg_quote($path, '@'); + }, $directory_list); + $prefix_filter = '@^(' . \implode('|', $parts) . ')[\\\\/]@'; + return static function (string $path) use ($prefix_filter): bool { + return !\preg_match($prefix_filter, $path); + }; + } + + /** + * @param array $opts + * @return list + */ + private static function getArrayOption(array $opts, string $key): array + { + $values = $opts[$key] ?? []; + if (is_string($values)) { + return [$values]; + } + return is_array($values) ? $values : []; + } + + /** + * Returns true if there is at least one statement that is parseable and not an inline HTML echo statement. + * + * This indicates that $relative_path points to a PHP binary file that should be analyzed. + */ + public static function isPHPBinary(string $relative_path): bool + { + $cwd = \getcwd(); + $absolute_path = "$cwd/$relative_path"; + if (!\file_exists($absolute_path)) { + \printf("Failed to find '%s', continuing\n", $absolute_path); + return false; + } + $contents = \file_get_contents($absolute_path); + if (!is_string($contents)) { + \printf("Failed to read '%s', continuing\n", $absolute_path); + return false; + } + try { + // PHP binaries can have many forms, may begin with #/usr/bin/env php. + // We assume that if it's parsable and contains at least one PHP executable line, it's valid. + $ast = Parser::parseCode( + new CodeBase([], [], [], [], []), + new Context(), + null, + $relative_path, + $contents, + true + ); + $child_nodes = $ast->children; + if (count($child_nodes) !== 1) { + return true; + } + $node = $child_nodes[0]; + if (!$node instanceof Node) { + // e.g. kind !== \ast\AST_ECHO || !is_string($node->children['expr']); + } catch (ParseError | CompileError | ParseException $_) { + return false; + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Daemon.php b/bundled-libs/phan/phan/src/Phan/Daemon.php new file mode 100644 index 000000000..15d25d48b --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Daemon.php @@ -0,0 +1,261 @@ +isUndoTrackingEnabled()) { + throw new AssertionError("Expected undo tracking to be enabled when starting daemon mode"); + } + + // example requests over TCP + // Assumes that clients send and close the their requests quickly, then wait for a response. + + // {"method":"analyze","files":["/path/to/file1.php","/path/to/file2.php"]} + + $socket_server = self::createDaemonStreamSocketServer(); + // TODO: Limit the maximum number of active processes to a small number(4?) + try { + $got_signal = false; + + if (\function_exists('pcntl_signal')) { + \pcntl_signal( + \SIGCHLD, + /** @param ?(int|array) $status */ + static function (int $signo, $status = null, ?int $pid = null) use (&$got_signal): void { + $got_signal = true; + Request::childSignalHandler($signo, $status, $pid); + } + ); + } + while (true) { + $got_signal = false; // reset this. + // We get an error from stream_socket_accept. After the RuntimeException is thrown, pcntl_signal is called. + /** + * @param int $severity + * @param string $message + * @param string $file + * @param int $line + * @return bool + */ + $previous_error_handler = \set_error_handler(static function (int $severity, string $message, string $file, int $line) use (&$previous_error_handler): bool { + self::debugf("In new error handler '$message'"); + if (!\preg_match('/stream_socket_accept/i', $message)) { + return $previous_error_handler($severity, $message, $file, $line); + } + throw new RuntimeException("Got signal"); + }); + + $conn = false; + try { + $conn = \stream_socket_accept($socket_server, -1); + } catch (RuntimeException $_) { + self::debugf("Got signal"); + \pcntl_signal_dispatch(); + self::debugf("done processing signals"); + if ($got_signal) { + continue; // Ignore notices from stream_socket_accept if it's due to being interrupted by a child process terminating. + } + } finally { + \restore_error_handler(); + } + + if (!\is_resource($conn)) { + // If we didn't get a connection, and it wasn't due to a signal from a child process, then stop the daemon. + break; + } + $request = Request::accept( + $code_base, + $file_path_lister, + new StreamResponder($conn, true), + true + ); + if ($request instanceof Request) { + return $request; // We forked off a worker process successfully, and this is the worker process + } + } + \error_log("Stopped accepting connections"); + } finally { + \restore_error_handler(); + } + return null; + } + + /** + * @return void - A writable request, which has been fully read from. + * Callers should close after they are finished writing. + * + * @throws Exception if analysis failed in an unexpected way + */ + private static function runWithoutPcntl(CodeBase $code_base, Closure $file_path_lister): void + { + // This is a single threaded server, it only analyzes one TCP request at a time + $socket_server = self::createDaemonStreamSocketServer(); + try { + while (true) { + // We get an error from stream_socket_accept. After the RuntimeException is thrown, pcntl_signal is called. + $previous_error_handler = \set_error_handler( + static function (int $severity, string $message, string $file, int $line) use (&$previous_error_handler): bool { + self::debugf("In new error handler '$message'"); + if (!\preg_match('/stream_socket_accept/i', $message)) { + return $previous_error_handler($severity, $message, $file, $line); + } + throw new RuntimeException("Got signal"); + } + ); + + $conn = false; + try { + $conn = \stream_socket_accept($socket_server, -1); + } catch (RuntimeException $_) { + self::debugf("Got signal"); + \pcntl_signal_dispatch(); + self::debugf("done processing signals"); + } finally { + \restore_error_handler(); + } + + if (!\is_resource($conn)) { + // If we didn't get a connection, and it wasn't due to a signal from a child process, then stop the daemon. + break; + } + // We **are** the only process. Imitate the worker process + $request = Request::accept( + $code_base, + $file_path_lister, + new StreamResponder($conn, true), + false // This is not a fork, do not call exit($status) + ); + if ($request instanceof Request) { + self::debugf("Calling analyzeDaemonRequestOnMainThread\n"); + // This did not fork, and will not fork (Unless --processes N was used) + self::analyzeDaemonRequestOnMainThread($code_base, $request); + // Force garbage collection in case it didn't respond + $request = null; + self::debugf("Finished call to analyzeDaemonRequestOnMainThread\n"); + // We did not terminate, we keep accepting + } + } + \error_log("Stopped accepting connections"); + } finally { + \restore_error_handler(); + } + } + + /** + * @throws Exception if analysis throws + */ + private static function analyzeDaemonRequestOnMainThread(CodeBase $code_base, Request $request): void + { + // Stop tracking undo operations, now that the parse phase is done. + // TODO: Save and reset $code_base in place + $analyze_file_path_list = $request->filterFilesToAnalyze($code_base->getParsedFilePathList()); + Phan::setPrinter($request->getPrinter()); + if (\count($analyze_file_path_list) === 0) { + // Nothing to do, don't start analysis + $request->respondWithNoFilesToAnalyze(); // respond and exit. + return; + } + $restore_point = $code_base->createRestorePoint(); + $code_base->disableUndoTracking(); + + $temporary_file_mapping = $request->getTemporaryFileMapping(); + + try { + Phan::finishAnalyzingRemainingStatements($code_base, $request, $analyze_file_path_list, $temporary_file_mapping); + } catch (ExitException $_) { + // This is normal and expected, do nothing + } finally { + $code_base->restoreFromRestorePoint($restore_point); + } + } + + /** + * @return resource (resource is not a reserved keyword) + * @throws InvalidArgumentException if the config does not specify a method. (should not happen) + */ + private static function createDaemonStreamSocketServer() + { + if (Config::getValue('daemonize_socket')) { + $listen_url = 'unix://' . Config::getValue('daemonize_socket'); + } elseif (Config::getValue('daemonize_tcp')) { + $listen_url = \sprintf('tcp://%s:%d', Config::getValue('daemonize_tcp_host'), Config::getValue('daemonize_tcp_port')); + } else { + throw new InvalidArgumentException("Should not happen, no port/socket for daemon to listen on."); + } + // @phan-suppress-next-line PhanPluginRemoveDebugCall this is deliberate output. + \printf( + "Listening for Phan analysis requests at %s\nAwaiting analysis requests for directory %s\n", + $listen_url, + \var_export(Config::getProjectRootDirectory(), true) + ); + $socket_server = \stream_socket_server($listen_url, $errno, $errstr); + if (!$socket_server) { + \error_log("Failed to create Unix socket server $listen_url: $errstr ($errno)\n"); + exit(1); + } + return $socket_server; + } + + /** + * Debug (non-error) statement related to the daemon. + * Set PHAN_DAEMON_ENABLE_DEBUG=1 when debugging. + * + * @param string $format - printf style format string + * @param mixed ...$args - printf args + * @suppress PhanPluginPrintfVariableFormatString + */ + public static function debugf(string $format, ...$args): void + { + if (\getenv('PHAN_DAEMON_ENABLE_DEBUG')) { + if (\count($args) > 0) { + $message = \sprintf($format, ...$args); + } else { + $message = $format; + } + // @phan-suppress-next-line PhanPluginRemoveDebugCall printing to stderr is deliberate + \fwrite(\STDERR, $message . "\n"); + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Daemon/ExitException.php b/bundled-libs/phan/phan/src/Phan/Daemon/ExitException.php new file mode 100644 index 000000000..c6395cbee --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Daemon/ExitException.php @@ -0,0 +1,17 @@ +,format:string,temporary_file_mapping_contents:array} + * + * The configuration passed in with the request to the daemon. + */ + private $request_config; + + /** @var BufferedOutput this collects the serialized issues emitted by this worker to be sent back to the master process */ + private $buffered_output; + + /** @var string the method of the daemon being invoked */ + private $method; + + /** @var list|null the list of files the client has requested to be analyzed */ + private $files = null; + + /** @var IssuePrinterInterface possibly a CapturingJSONPrinter, to avoid json_encode+json_decode overhead when there's a lot of issues in language server mode. */ + private $raw_printer; + + /** + * A set of process ids of child processes + * @var associative-array + */ + private static $child_pids = []; + + /** + * A map from process ids of exited child processes to their exit status. + * @var associative-array + */ + private static $exited_pid_status = []; + + + /** + * The most recent Language Server Protocol request to look up what an element is + * (e.g. "go to definition", "go to type definition", "hover") + * + * @var ?NodeInfoRequest + */ + private $most_recent_node_info_request; + + /** + * If true, this process will exit() after finishing. + * If false, this class will instead throw ExitException to be caught by the caller + * (E.g. if pcntl is unavailable) + * + * @var bool + */ + private $should_exit; + + /** + * @param array{method:string,files:list,format:string,temporary_file_mapping_contents:array} $config + * @param ?NodeInfoRequest $most_recent_node_info_request + */ + private function __construct(Responder $responder, array $config, $most_recent_node_info_request, bool $should_exit) + { + $this->responder = $responder; + $this->request_config = $config; + $this->buffered_output = new BufferedOutput(); + $this->method = $config[self::PARAM_METHOD]; + if ($this->method === self::METHOD_ANALYZE_FILES) { + $this->files = $config[self::PARAM_FILES]; + } + $this->most_recent_node_info_request = $most_recent_node_info_request; + $this->should_exit = $should_exit; + } + + /** + * @param string $file_path an absolute or relative path to be analyzed + */ + public function shouldUseMappingPolyfill(string $file_path): bool + { + if ($this->most_recent_node_info_request) { + return $this->most_recent_node_info_request->getPath() === Config::projectPath($file_path); + } + return false; + } + + /** + * @param string $file_path an absolute or relative path to be analyzed + */ + public function shouldAddPlaceholdersForPath(string $file_path): bool + { + if ($this->most_recent_node_info_request instanceof CompletionRequest) { + return $this->most_recent_node_info_request->getPath() === Config::projectPath($file_path); + } + return false; + } + + /** + * Computes the byte offset of the node targeted by a language client's request (e.g. for a "Go to definition" request) + */ + public function getTargetByteOffset(string $file_contents): int + { + if ($this->most_recent_node_info_request) { + $position = $this->most_recent_node_info_request->getPosition(); + return $position->toOffset($file_contents); + } + return -1; + } + + /** + * @return void (unreachable) + * @throws ExitException to imitate an exit without actually exiting + */ + public function exit(int $exit_code): void + { + if ($this->should_exit) { + Daemon::debugf("Exiting"); + exit($exit_code); + } + throw new ExitException("done", $exit_code); + } + + /** + * @param Responder $responder (e.g. a socket to write a response on) + * @param list $file_names absolute path of file(s) to analyze + * @param CodeBase $code_base (for refreshing parse state) + * @param Closure $file_path_lister (for refreshing parse state) + * @param FileMapping $file_mapping object tracking the overrides made by a client. + * @param ?NodeInfoRequest $most_recent_node_info_request contains a promise that we want the resolution of + * @param bool $should_exit - If this is true, calling $this->exit() will terminate the program. If false, ExitException will be thrown. + */ + public static function makeLanguageServerAnalysisRequest( + Responder $responder, + array $file_names, + CodeBase $code_base, + Closure $file_path_lister, + FileMapping $file_mapping, + ?NodeInfoRequest $most_recent_node_info_request, + bool $should_exit + ): Request { + FileCache::clear(); + $file_mapping_contents = self::normalizeFileMappingContents($file_mapping->getOverrides(), $error_message); + if ($most_recent_node_info_request instanceof CompletionRequest) { + $file_mapping_contents = self::adjustFileMappingContentsForCompletionRequest($file_mapping_contents, $most_recent_node_info_request); + } + // Use the temporary contents if they're available + Request::reloadFilePathListForDaemon($code_base, $file_path_lister, $file_mapping_contents, $file_names); + if ($error_message !== null) { + Daemon::debugf($error_message); + } + $result = new self( + $responder, + [ + self::PARAM_FORMAT => 'json', + self::PARAM_METHOD => self::METHOD_ANALYZE_FILES, + self::PARAM_FILES => $file_names, + self::PARAM_TEMPORARY_FILE_MAPPING_CONTENTS => $file_mapping_contents, + ], + $most_recent_node_info_request, + $should_exit + ); + return $result; + } + + /** + * When a user types :: or -> and requests code completion at the end of a line, + * then add __INCOMPLETE_PROPERTY__ or __INCOMPLETE_CLASS_CONST__ so that this + * can get parsed and completed. + * @param array $file_mapping_contents old map from relative file paths to contents. + * @return array + */ + private static function adjustFileMappingContentsForCompletionRequest( + array $file_mapping_contents, + CompletionRequest $completion_request + ): array { + $file = FileRef::getProjectRelativePathForPath($completion_request->getPath()); + // fwrite(STDERR, "\nSaw $file in " . json_encode(array_keys($file_mapping_contents)) . "\n"); + $contents = $file_mapping_contents[$file] ?? null; + if (is_string($contents)) { + $position = $completion_request->getPosition(); + $lines = \explode("\n", $contents); + $line = $lines[$position->line] ?? null; + // $len = strlen($line ?? ''); fwrite(STDERR, "Looking at $line : $position of $len\n"); + if (is_string($line) && strlen($line) === $position->character + 1 && $position->character > 0) { + // fwrite(STDERR, "cursor at the end of the line\n"); + if (\preg_match('/(::|->)$/D', $line, $matches)) { + // fwrite(STDERR, "Updating the file\n"); + if ($matches[1] === '::') { + $addition = TolerantASTConverter::INCOMPLETE_CLASS_CONST; + } else { + $addition = TolerantASTConverter::INCOMPLETE_PROPERTY; + } + $lines[$position->line] .= $addition; + $new_contents = \implode("\n", $lines); + $file_mapping_contents[$file] = $new_contents; + // fwrite(STDERR, "Going to complete\n$new_contents\n====\nA"); + } + } + } + return $file_mapping_contents; + } + + /** + * Returns a printer that will be used to send JSON serialized data to the daemon client (i.e. `phan_client`). + */ + public function getPrinter(): IssuePrinterInterface + { + $this->handleClientColorOutput(); + + $factory = new PrinterFactory(); + $format = $this->request_config[self::PARAM_FORMAT] ?? 'json'; + if (!in_array($format, $factory->getTypes(), true)) { + $this->sendJSONResponse([ + "status" => self::STATUS_INVALID_FORMAT, + ]); + exit(0); + } + // In both the Language Server and the Daemon, + // this deliberately sends only analysis results of the files that are currently open. + // + // Otherwise, there might be an overwhelming number of issues to solve in some projects before using this in the IDE (e.g. PhanUnreferencedUseNormal) + if (($this->request_config[self::PARAM_FORMAT] ?? null) === 'json') { + $printer = new CapturingJSONPrinter(); + } else { + $printer = $factory->getPrinter($format, $this->buffered_output); + } + $this->raw_printer = $printer; + $files = $this->request_config[self::PARAM_FILES] ?? null; + if (is_array($files) && count($files) > 0 && !Config::getValue('language_server_disable_output_filter')) { + return new FilteringPrinter($files, $printer); + } + return $printer; + } + + /** + * Handle a request created by the client with `phan_client --color` + */ + private function handleClientColorOutput(): void + { + // Back up the original state: If pcntl isn't used, we don't want subsequent requests to be accidentally colorized. + static $original_color = null; + if ($original_color === null) { + $original_color = (bool)Config::getValue('color_issue_messages'); + } + $new_color = $this->request_config[self::PARAM_COLOR] ?? $original_color; + Config::setValue('color_issue_messages', $new_color); + } + + /** + * Respond with issues in the requested format + * @see LanguageServer::handleJSONResponseFromWorker() for one possible usage of this + */ + public function respondWithIssues(int $issue_count): void + { + if ($this->raw_printer instanceof CapturingJSONPrinter) { + // Optimization: Avoid json_encode+json_decode overhead and just take the raw array that was built. + // This slightly speeds up responses with a lot of issues (e.g. due to unmatched quotes in strings). + $issues = $this->raw_printer->getIssues(); + } else { + $issues = $this->buffered_output->fetch(); + } + $response = [ + "status" => self::STATUS_OK, + "issue_count" => $issue_count, + "issues" => $issues, + ]; + $most_recent_node_info_request = $this->most_recent_node_info_request; + if ($most_recent_node_info_request instanceof GoToDefinitionRequest) { + $response['definitions'] = $most_recent_node_info_request->getDefinitionLocations(); + $response['hover_response'] = $most_recent_node_info_request->getHoverResponse(); + } elseif ($most_recent_node_info_request instanceof CompletionRequest) { + $response['completions'] = $most_recent_node_info_request->getCompletions(); + } + $this->sendJSONResponse($response); + } + + /** + * Sends a response to the client indicating that + * the requested file wasn't in .phan/config.php's list of files to analyze. + */ + public function respondWithNoFilesToAnalyze(): void + { + $this->sendJSONResponse([ + "status" => self::STATUS_NO_FILES, + ]); + } + + /** + * @param list $analyze_file_path_list + * @return list + */ + public function filterFilesToAnalyze(array $analyze_file_path_list): array + { + if (\is_null($this->files)) { + Daemon::debugf("No files to filter in filterFilesToAnalyze"); + return $analyze_file_path_list; + } + + $analyze_file_path_set = \array_flip($analyze_file_path_list); + $filtered_files = []; + foreach ($this->files as $file) { + // Must be relative to project, allow absolute paths to be passed in. + $file = FileRef::getProjectRelativePathForPath($file); + + if (\array_key_exists($file, $analyze_file_path_set)) { + $filtered_files[] = $file; + } else { + // TODO: Reload file list once before processing request? + // TODO: Change this to also support analyzing files that would normally be parsed but not analyzed? + Daemon::debugf("Failed to find requested file '%s' in parsed file list", $file, StringUtil::jsonEncode($analyze_file_path_list)); + } + } + Daemon::debugf("Returning file set: %s", StringUtil::jsonEncode($filtered_files)); + return $filtered_files; + } + + /** + * TODO: convert absolute path to file contents + * @return array - Maps original relative file paths to contents. + */ + public function getTemporaryFileMapping(): array + { + $mapping = $this->request_config[self::PARAM_TEMPORARY_FILE_MAPPING_CONTENTS] ?? []; + if (!is_array($mapping)) { + $mapping = []; + } + Daemon::debugf("Have the following files in mapping: %s", StringUtil::jsonEncode(\array_keys($mapping))); + return $mapping; + } + + /** + * Fetches the most recently made request for information about a node of the file. + * (e.g. for "go to definition") + */ + public function getMostRecentNodeInfoRequest(): ?NodeInfoRequest + { + return $this->most_recent_node_info_request; + } + + /** + * Send null responses for any open requests so that clients won't hang + * or encounter errors. + * + * (e.g. if we encountered a newer request before that request could be processed) + */ + public function rejectLanguageServerRequestsRequiringAnalysis(): void + { + if ($this->most_recent_node_info_request) { + $this->most_recent_node_info_request->finalize(); + $this->most_recent_node_info_request = null; + } + } + + /** + * Send a response and close the connection, for the given socket's protocol. + * Currently supports only JSON. + * TODO: HTTP protocol. + * + * @param array $response + */ + public function sendJSONResponse(array $response): void + { + if (!$this->responder) { + Daemon::debugf("Already sent response"); + return; + } + $this->responder->sendResponseAndClose($response); + $this->responder = null; + } + + public function __destruct() + { + if ($this->responder) { + $this->responder->sendResponseAndClose([ + 'status' => self::STATUS_ERROR_UNKNOWN, + 'message' => 'failed to send a response - Possibly encountered an exception. See daemon output: ' . StringUtil::jsonEncode(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)), + ]); + $this->responder = null; + } + } + + /** + * @param ?(int|array) $status + */ + public static function childSignalHandler(int $signo, $status = null, ?int $pid = null): void + { + // test + if ($signo !== SIGCHLD) { + return; + } + if (!$pid) { + $pid = \pcntl_waitpid(-1, $status, WNOHANG); + } + Daemon::debugf("Got signal pid=%s", StringUtil::jsonEncode($pid)); + + // Add additional check for Phan - pid > 0 implies status is non-null and an integer + while ($pid > 0 && $status !== null) { + if (\array_key_exists($pid, self::$child_pids)) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal + $exit_code = \pcntl_wexitstatus($status); + if ($exit_code !== 0) { + \error_log(\sprintf("child process %d exited with status %d\n", $pid, $exit_code)); + } else { + Daemon::debugf("child process %d completed successfully", $pid); + } + unset(self::$child_pids[$pid]); + } elseif ($pid > 0) { + self::$exited_pid_status[$pid] = $status; + } + $pid = \pcntl_waitpid(-1, $status, WNOHANG); + } + } + + /** + * @param array $file_mapping_contents + * @param ?string &$error_message @phan-output-reference + * @return array + */ + public static function normalizeFileMappingContents(array $file_mapping_contents, ?string &$error_message): array + { + $error_message = null; + $new_file_mapping_contents = []; + foreach ($file_mapping_contents as $file => $contents) { + if (!\is_string($file)) { + $error_message = 'Passed non-string in list of files to map'; + return []; + } elseif (!\is_string($contents)) { + $error_message = 'Passed non-string in as new file contents'; + return []; + } + $new_file_mapping_contents[FileRef::getProjectRelativePathForPath($file)] = $contents; + } + return $new_file_mapping_contents; + } + /** + * @param CodeBase $code_base + * @param \Closure $file_path_lister lists all files that will be parsed by Phan + * @param Responder $responder + * @return ?Request - non-null if this is a worker process with work to do. null if request failed or this is the master. + */ + public static function accept(CodeBase $code_base, Closure $file_path_lister, Responder $responder, bool $fork): ?Request + { + FileCache::clear(); + + $request = $responder->getRequestData(); + + if (!\is_array($request)) { + $responder->sendResponseAndClose([ + 'status' => self::STATUS_INVALID_REQUEST, + 'message' => 'malformed JSON', + ]); + return null; + } + $new_file_mapping_contents = []; + $method = $request['method'] ?? ''; + $files = null; + switch ($method) { + case 'analyze_all': + // Analyze the default list of files. No expected params. + break; + case 'analyze_file': + // Override some parameters and keep other parameters such as temporary_file_mapping_contents + $request[self::PARAM_FILES] = [$request['file']]; + $request[self::PARAM_METHOD] = 'analyze_files'; + $request[self::PARAM_FORMAT] = $request[self::PARAM_FORMAT] ?? 'json'; + // Fall through, this is an alias of analyze_files + case 'analyze_files': + // Analyze the list of strings provided in "files" + $files = $request[self::PARAM_FILES] ?? null; + $request[self::PARAM_FORMAT] = $request[self::PARAM_FORMAT] ?? 'json'; + $error_message = null; + if (\is_array($files) && count($files)) { + foreach ($files as $file) { + if (!\is_string($file)) { + $error_message = 'Passed non-string in list of files'; + break; + } + } + } else { + $error_message = 'Must pass a non-empty array of file paths for field files'; + } + if (\is_null($error_message)) { + $file_mapping_contents = $request[self::PARAM_TEMPORARY_FILE_MAPPING_CONTENTS] ?? []; + if (is_array($file_mapping_contents)) { + // @phan-suppress-next-line PhanPartialTypeMismatchArgument false positive due to bad inference after unset field of array shape. + $new_file_mapping_contents = self::normalizeFileMappingContents($file_mapping_contents, $error_message); + $request[self::PARAM_TEMPORARY_FILE_MAPPING_CONTENTS] = $new_file_mapping_contents; + } else { + $error_message = 'Must pass an optional array or null for temporary_file_mapping_contents'; + } + } + if ($error_message !== null) { + Daemon::debugf($error_message); + $responder->sendResponseAndClose([ + 'status' => self::STATUS_INVALID_FILES, + 'message' => $error_message, + ]); + return null; + } + break; + // TODO(optional): add APIs to resolve types of variables/properties/etc (e.g. accept byte offset or line/column offset) + default: + $message = \sprintf("expected method to be analyze_all or analyze_files, got %s", StringUtil::jsonEncode($method)); + Daemon::debugf($message); + $responder->sendResponseAndClose([ + 'status' => self::STATUS_INVALID_METHOD, + 'message' => $message, + ]); + return null; + } + + // Re-parse the file list + self::reloadFilePathListForDaemon($code_base, $file_path_lister, $new_file_mapping_contents, $files); + + // Analyze the files that are open in the IDE (If pcntl is available, the analysis is done in a forked process) + + if (!$fork) { + Daemon::debugf("This is the main process pretending to be the fork"); + self::$child_pids = []; + // This is running on the only thread, so configure $request_obj to throw ExitException instead of calling exit() + $request_obj = new self($responder, $request, null, false); + $temporary_file_mapping = $request_obj->getTemporaryFileMapping(); + if (count($temporary_file_mapping) > 0) { + self::applyTemporaryFileMappingForParsePhase($code_base, $temporary_file_mapping); + } + return $request_obj; + } + + $fork_result = \pcntl_fork(); + if ($fork_result < 0) { + \error_log("The daemon failed to fork. Going to terminate"); + } elseif ($fork_result === 0) { + Daemon::debugf("This is the fork"); + self::handleBecomingChildAnalysisProcess(); + $request_obj = new self($responder, $request, null, true); + $temporary_file_mapping = $request_obj->getTemporaryFileMapping(); + if (count($temporary_file_mapping) > 0) { + self::applyTemporaryFileMappingForParsePhase($code_base, $temporary_file_mapping); + } + return $request_obj; + } else { + $pid = $fork_result; + self::handleBecomingParentOfChildAnalysisProcess($pid); + } + return null; + } + + /** + * Handle becoming a parent of a forked process $pid. + * + * This tracks the information needed for the + * main process of the daemon to properly clean up + * after $pid once it exits. (to avoid leaving zombie processes) + * + * @param int $pid the child PID of this process that is performing analysis + */ + public static function handleBecomingParentOfChildAnalysisProcess(int $pid): void + { + $status = self::$exited_pid_status[$pid] ?? null; + if (isset($status)) { + Daemon::debugf("child process %d already exited", $pid); + self::childSignalHandler(SIGCHLD, $status, $pid); + unset(self::$exited_pid_status[$pid]); + } else { + self::$child_pids[$pid] = true; + } + + // TODO: Use http://php.net/manual/en/book.inotify.php if available, watch all directories if available. + // Daemon continues to execute. + Daemon::debugf("Created a child pid %d", $pid); + } + + /** + * Handle becoming a child analysis process - this should no longer be waiting to clean up previously forked child processes. + */ + public static function handleBecomingChildAnalysisProcess(): void + { + self::$child_pids = []; + } + + /** + * Reloads the file path list. + * @param array $file_mapping_contents maps relative paths to file contents + * @param ?list $file_names + */ + public static function reloadFilePathListForDaemon(CodeBase $code_base, Closure $file_path_lister, array $file_mapping_contents, array $file_names = null): void + { + $old_count = $code_base->getParsedFilePathCount(); + + $file_list = $file_path_lister(true); + + if (Config::getValue('consistent_hashing_file_order')) { + // Parse the files in lexicographic order. + // If there are duplicate class/function definitions, + // this ensures they are added to the maps in the same order. + \sort($file_list, SORT_STRING); + } + + $changed_or_added_files = $code_base->updateFileList($file_list, $file_mapping_contents, $file_names); + // Daemon::debugf("Parsing modified files: New files = %s", StringUtil::jsonEncode($changed_or_added_files)); + if (count($changed_or_added_files) > 0 || $code_base->getParsedFilePathCount() !== $old_count) { + // Only clear memoizations if it is determined at least one file to parse was added/removed/modified. + // - file path count changes if files were deleted or added + // - changed_or_added_files has an entry for every added/modified file. + // (would be 0 if a client analyzes one file, then analyzes a different file) + Type::clearAllMemoizations(); + } + // A progress bar doesn't make sense in a daemon which can theoretically process multiple requests at once. + foreach ($changed_or_added_files as $file_path) { + // Kick out anything we read from the former version + // of this file + $code_base->flushDependenciesForFile($file_path); + + // If we have an override for the contents of this file, assume it's open in the IDE. + // (even if it doesn't exist on disk) + $file_contents_override = $file_mapping_contents[$file_path] ?? null; + if (!is_string($file_contents_override)) { + // If the file is gone, no need to continue + $real = \realpath($file_path); + if ($real === false || !\file_exists($real)) { + Daemon::debugf("file $file_path does not exist"); + continue; + } + } + Daemon::debugf("Parsing %s yet again", $file_path); + try { + // Parse the file + Analysis::parseFile($code_base, $file_path, false, $file_contents_override, false, new ParseRequest()); + } catch (\Throwable $throwable) { + \error_log(\sprintf("Analysis::parseFile threw %s for %s: %s\n%s", get_class($throwable), $file_path, $throwable->getMessage(), $throwable->getTraceAsString())); + } + } + Daemon::debugf("Done parsing modified files"); + } + + /** + * Substitutes files. We assume that the original file path exists already, and reject it if it doesn't. + * (i.e. it was returned by $file_path_lister in the past) + * + * @param array $temporary_file_mapping_contents + */ + private static function applyTemporaryFileMappingForParsePhase(CodeBase $code_base, array $temporary_file_mapping_contents): void + { + if (count($temporary_file_mapping_contents) === 0) { + return; + } + + // too verbose + Daemon::debugf("Parsing temporary file mapping contents: New contents = %s", StringUtil::jsonEncode($temporary_file_mapping_contents)); + + $changes_to_add = []; + foreach ($temporary_file_mapping_contents as $file_name => $contents) { + if ($code_base->beforeReplaceFileContents($file_name)) { + $changes_to_add[$file_name] = $contents; + } + } + Daemon::debugf("Done setting temporary file contents: Will replace contents of the following files: %s", StringUtil::jsonEncode(\array_keys($changes_to_add))); + if (count($changes_to_add) === 0) { + return; + } + Type::clearAllMemoizations(); + + foreach ($changes_to_add as $file_path => $new_contents) { + // Kick out anything we read from the former version + // of this file + $code_base->flushDependenciesForFile($file_path); + + // If the file is gone, no need to continue + $real = \realpath($file_path); + if ($real === false || !\file_exists($real)) { + Daemon::debugf("file $file_path no longer exists on disk, but we tried to replace it?"); + continue; + } + Daemon::debugf("Parsing temporary file instead of %s", $file_path); + try { + // Parse the file + Analysis::parseFile($code_base, $file_path, false, $new_contents); + } catch (\Throwable $throwable) { + \error_log(\sprintf("Analysis::parseFile threw %s for %s: %s\n%s", get_class($throwable), $file_path, $throwable->getMessage(), $throwable->getTraceAsString())); + } + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Daemon/Transport/CapturerResponder.php b/bundled-libs/phan/phan/src/Phan/Daemon/Transport/CapturerResponder.php new file mode 100644 index 000000000..ce3d28f25 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Daemon/Transport/CapturerResponder.php @@ -0,0 +1,54 @@ + the data for getRequestData() */ + private $request_data; + + /** @var ?array the data sent via sendAndClose */ + private $response_data; + + /** @param array $data the data for getRequestData() */ + public function __construct(array $data) + { + $this->request_data = $data; + } + + /** + * @return array the request data + */ + public function getRequestData(): array + { + return $this->request_data; + } + + /** + * @param array $data + * @throws \RuntimeException if called twice + */ + public function sendResponseAndClose(array $data): void + { + if (\is_array($this->response_data)) { + throw new \RuntimeException("Called sendResponseAndClose twice: data = " . StringUtil::jsonEncode($data)); + } + $this->response_data = $data; + } + + /** + * @return ?array the raw response data that the analysis would have sent back serialized if this was actually a fork. + */ + public function getResponseData(): ?array + { + return $this->response_data; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Daemon/Transport/Responder.php b/bundled-libs/phan/phan/src/Phan/Daemon/Transport/Responder.php new file mode 100644 index 000000000..54faa5fec --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Daemon/Transport/Responder.php @@ -0,0 +1,26 @@ + the request data(E.g. returns null if JSON is malformed) + */ + public function getRequestData(): ?array; + + /** + * This must be called exactly once + * @param array $data the response fields + */ + public function sendResponseAndClose(array $data): void; +} diff --git a/bundled-libs/phan/phan/src/Phan/Daemon/Transport/StreamResponder.php b/bundled-libs/phan/phan/src/Phan/Daemon/Transport/StreamResponder.php new file mode 100644 index 000000000..d05879da8 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Daemon/Transport/StreamResponder.php @@ -0,0 +1,87 @@ + the request data */ + private $request_data; + + /** @var bool did this process already finish reading the data of the request? */ + private $did_read_request_data = false; + + /** @param resource $connection a stream */ + public function __construct($connection, bool $expect_request) + { + if (!\is_resource($connection)) { + throw new TypeError("Expected connection to be resource, saw " . \gettype($connection)); + } + $this->connection = $connection; + if (!$expect_request) { + $this->did_read_request_data = true; + $this->request_data = []; + } + } + + /** + * @return ?array the request data(E.g. returns null if JSON is malformed) + */ + public function getRequestData(): ?array + { + if (!$this->did_read_request_data) { + $response_connection = $this->connection; + if (!$response_connection) { + Daemon::debugf("Should not happen, missing a response connection"); // debugging code + return null; + } + Daemon::debugf("Got a connection"); // debugging code + $request_bytes = ''; + while (!\feof($response_connection)) { + $request_bytes .= \fgets($response_connection); + } + $request = \json_decode($request_bytes, true); + if (!\is_array($request)) { + Daemon::debugf("Received invalid request, expected JSON: %s", StringUtil::jsonEncode($request_bytes)); + $request = null; + } + $this->did_read_request_data = true; + $this->request_data = $request; + } + return $this->request_data; + } + + /** + * @param array $data the response fields + * @throws \RuntimeException if called twice + */ + public function sendResponseAndClose(array $data): void + { + $connection = $this->connection; + if (!$this->did_read_request_data) { + throw new \RuntimeException("Called sendAndClose before calling getRequestData"); + } + if ($connection === null) { + throw new \RuntimeException("Called sendAndClose twice: data = " . StringUtil::jsonEncode($data)); + } + \fwrite($connection, StringUtil::jsonEncode($data) . "\n"); + // disable further receptions and transmissions + // Note: This is likely a giant hack, + // and pcntl and sockets may break in the future if used together. (multiple processes owning a single resource). + // Not sure how to do that safely. + \stream_socket_shutdown($connection, \STREAM_SHUT_RDWR); + \fclose($connection); + $this->connection = null; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Debug.php b/bundled-libs/phan/phan/src/Phan/Debug.php new file mode 100644 index 000000000..6461c0243 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Debug.php @@ -0,0 +1,474 @@ +kind; + if (\is_string($kind)) { + // For placeholders created by tolerant-php-parser-to-php-ast + return "string ($kind)"; + } + try { + return Parser::getKindName($kind); + } catch (LogicException $_) { + return "UNKNOWN_KIND($kind)"; + } + } + + /** + * Convert an AST node to a compact string representation of that node. + * + * @param string|int|float|Node|null $node + * An AST node + * + * @param int|float|string|null $name + * The name of the node (if this node has a parent) + * + * @param int $indent + * The indentation level for the string + * + * @return string + * A string representation of an AST node + * + * @phan-side-effect-free + */ + public static function nodeToString( + $node, + $name = null, + int $indent = 0 + ): string { + $string = \str_repeat("\t", $indent); + + if ($name !== null) { + $string .= "$name => "; + } + + if (\is_string($node)) { + return $string . $node . "\n"; + } + + if ($node === null) { + return $string . 'null' . "\n"; + } + + if (!\is_object($node)) { + return $string . (\is_array($node) ? \json_encode($node) : $node) . "\n"; + } + $kind = $node->kind; + + $string .= self::nodeName($node); + + $string .= ' [' + . (\is_int($kind) ? self::astFlagDescription($node->flags ?? 0, $kind) : 'unknown') + . ']'; + + if (isset($node->lineno)) { + $string .= ' #' . $node->lineno; + } + + $end_lineno = $node->endLineno ?? null; + if (!\is_null($end_lineno)) { + $string .= ':' . $end_lineno; + } + + $string .= "\n"; + + foreach ($node->children as $name => $child_node) { + if (\is_string($name) && \strncmp($name, 'phan', 4) === 0) { + // Dynamic property added by Phan + continue; + } + $string .= self::nodeToString( + $child_node, + $name, + $indent + 1 + ); + } + + return $string; + } + + /** + * Computes a string representation of AST node flags such as + * 'ASSIGN_DIV|TYPE_ARRAY' + * @see self::formatFlags() for a similar function also printing the integer flag value. + */ + public static function astFlagDescription(int $flags, int $kind): string + { + [$exclusive, $combinable] = self::getFlagInfo(); + $flag_names = []; + if (isset($exclusive[$kind])) { + $flag_info = $exclusive[$kind]; + if (isset($flag_info[$flags])) { + $flag_names[] = $flag_info[$flags]; + } + } elseif (isset($combinable[$kind])) { + $flag_info = $combinable[$kind]; + foreach ($flag_info as $flag => $name) { + if ($flags & $flag) { + $flag_names[] = $name; + } + } + } + + return \implode('|', $flag_names); + } + + /** + * @return string + * Get a string representation of AST node flags such as + * 'ASSIGN_DIV (26)' + * Source: https://github.com/nikic/php-ast/blob/master/util.php + */ + public static function formatFlags(int $kind, int $flags): string + { + [$exclusive, $combinable] = self::getFlagInfo(); + if (isset($exclusive[$kind])) { + $flag_info = $exclusive[$kind]; + if (isset($flag_info[$flags])) { + return "{$flag_info[$flags]} ($flags)"; + } + } elseif (isset($combinable[$kind])) { + $flag_info = $combinable[$kind]; + $names = []; + foreach ($flag_info as $flag => $name) { + if ($flags & $flag) { + $names[] = $name; + } + } + if (\count($names) > 0) { + return \implode(" | ", $names) . " ($flags)"; + } + } + return (string) $flags; + } + + + /** + * @return void + * Pretty-printer for debug_backtrace + * + * @suppress PhanUnreferencedPublicMethod + */ + public static function backtrace(int $levels = 0): void + { + $bt = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, $levels + 1); + foreach ($bt as $level => $context) { + if (!$level) { + continue; + } + $file = $context['file'] ?? 'unknown'; + $line = $context['line'] ?? 1; + $class = $context['class'] ?? 'global'; + $function = $context['function'] ?? ''; + + echo "#" . ($level - 1) . " $file:$line $class "; + if (isset($context['type'])) { + echo $context['class'] . $context['type']; + } + echo $function; + echo "\n"; + } + } + + /** + * Dumps abstract syntax tree + * Source: https://github.com/nikic/php-ast/blob/master/util.php + * @param Node|string|int|float|null $ast + * @param int $options (self::AST_DUMP_*) + */ + public static function astDump($ast, int $options = 0): string + { + if ($ast instanceof Node) { + $result = Parser::getKindName($ast->kind); + + if ($options & self::AST_DUMP_LINENOS) { + $result .= " @ $ast->lineno"; + $end_lineno = $ast->endLineno ?? null; + if (!\is_null($end_lineno)) { + $result .= "-$end_lineno"; + } + } + + if (ast\kind_uses_flags($ast->kind)) { + $flags_without_phan_additions = $ast->flags & ~BlockExitStatusChecker::STATUS_BITMASK; + if ($flags_without_phan_additions !== 0) { + $result .= "\n flags: " . self::formatFlags($ast->kind, $flags_without_phan_additions); + } + } + foreach ($ast->children as $i => $child) { + $result .= "\n $i: " . \str_replace("\n", "\n ", self::astDump($child, $options)); + } + return $result; + } elseif ($ast === null) { + return 'null'; + } elseif (\is_string($ast)) { + return "\"$ast\""; + } else { + return (string) $ast; + } + } + + /** + * Source: https://github.com/nikic/php-ast/blob/master/util.php + * + * Returns the information necessary to map the node id to the flag id to the name. + * + * @return array{0:associative-array>,1:associative-array>} + * Returns [string[][] $exclusive, string[][] $combinable]. + */ + private static function getFlagInfo(): array + { + // TODO: Use AST's built in flag info if available. + static $exclusive, $combinable; + // Write this in a way that lets Phan infer the value of $combinable at the end. + if ($exclusive === null) { + $function_modifiers = [ + flags\MODIFIER_PUBLIC => 'MODIFIER_PUBLIC', + flags\MODIFIER_PROTECTED => 'MODIFIER_PROTECTED', + flags\MODIFIER_PRIVATE => 'MODIFIER_PRIVATE', + flags\MODIFIER_STATIC => 'MODIFIER_STATIC', + flags\MODIFIER_ABSTRACT => 'MODIFIER_ABSTRACT', + flags\MODIFIER_FINAL => 'MODIFIER_FINAL', + flags\FUNC_RETURNS_REF => 'FUNC_RETURNS_REF', + flags\FUNC_GENERATOR => 'FUNC_GENERATOR', + ]; + $property_modifiers = [ + flags\MODIFIER_PUBLIC => 'MODIFIER_PUBLIC', + flags\MODIFIER_PROTECTED => 'MODIFIER_PROTECTED', + flags\MODIFIER_PRIVATE => 'MODIFIER_PRIVATE', + flags\MODIFIER_STATIC => 'MODIFIER_STATIC', + flags\MODIFIER_ABSTRACT => 'MODIFIER_ABSTRACT', + flags\MODIFIER_FINAL => 'MODIFIER_FINAL', + ]; + $types = [ + flags\TYPE_NULL => 'TYPE_NULL', + flags\TYPE_FALSE => 'TYPE_FALSE', + flags\TYPE_BOOL => 'TYPE_BOOL', + flags\TYPE_LONG => 'TYPE_LONG', + flags\TYPE_DOUBLE => 'TYPE_DOUBLE', + flags\TYPE_STRING => 'TYPE_STRING', + flags\TYPE_ARRAY => 'TYPE_ARRAY', + flags\TYPE_OBJECT => 'TYPE_OBJECT', + flags\TYPE_CALLABLE => 'TYPE_CALLABLE', + flags\TYPE_VOID => 'TYPE_VOID', + flags\TYPE_ITERABLE => 'TYPE_ITERABLE', + flags\TYPE_STATIC => 'TYPE_STATIC', + ]; + $use_types = [ + flags\USE_NORMAL => 'USE_NORMAL', + flags\USE_FUNCTION => 'USE_FUNCTION', + flags\USE_CONST => 'USE_CONST', + ]; + $shared_binary_ops = [ + flags\BINARY_BITWISE_OR => 'BINARY_BITWISE_OR', + flags\BINARY_BITWISE_AND => 'BINARY_BITWISE_AND', + flags\BINARY_BITWISE_XOR => 'BINARY_BITWISE_XOR', + flags\BINARY_CONCAT => 'BINARY_CONCAT', + flags\BINARY_ADD => 'BINARY_ADD', + flags\BINARY_SUB => 'BINARY_SUB', + flags\BINARY_MUL => 'BINARY_MUL', + flags\BINARY_DIV => 'BINARY_DIV', + flags\BINARY_MOD => 'BINARY_MOD', + flags\BINARY_POW => 'BINARY_POW', + flags\BINARY_SHIFT_LEFT => 'BINARY_SHIFT_LEFT', + flags\BINARY_SHIFT_RIGHT => 'BINARY_SHIFT_RIGHT', + flags\BINARY_COALESCE => 'BINARY_COALESCE', + ]; + + $exclusive = [ + ast\AST_NAME => [ + flags\NAME_FQ => 'NAME_FQ', + flags\NAME_NOT_FQ => 'NAME_NOT_FQ', + flags\NAME_RELATIVE => 'NAME_RELATIVE', + ], + ast\AST_CLASS => [ + flags\CLASS_ABSTRACT => 'CLASS_ABSTRACT', + flags\CLASS_FINAL => 'CLASS_FINAL', + flags\CLASS_TRAIT => 'CLASS_TRAIT', + flags\CLASS_INTERFACE => 'CLASS_INTERFACE', + flags\CLASS_ANONYMOUS => 'CLASS_ANONYMOUS', + ], + ast\AST_TYPE => $types, + ast\AST_CAST => $types, + ast\AST_UNARY_OP => [ + flags\UNARY_BOOL_NOT => 'UNARY_BOOL_NOT', + flags\UNARY_BITWISE_NOT => 'UNARY_BITWISE_NOT', + flags\UNARY_MINUS => 'UNARY_MINUS', + flags\UNARY_PLUS => 'UNARY_PLUS', + flags\UNARY_SILENCE => 'UNARY_SILENCE', + ], + ast\AST_BINARY_OP => $shared_binary_ops + [ + flags\BINARY_BOOL_AND => 'BINARY_BOOL_AND', + flags\BINARY_BOOL_OR => 'BINARY_BOOL_OR', + flags\BINARY_BOOL_XOR => 'BINARY_BOOL_XOR', + flags\BINARY_IS_IDENTICAL => 'BINARY_IS_IDENTICAL', + flags\BINARY_IS_NOT_IDENTICAL => 'BINARY_IS_NOT_IDENTICAL', + flags\BINARY_IS_EQUAL => 'BINARY_IS_EQUAL', + flags\BINARY_IS_NOT_EQUAL => 'BINARY_IS_NOT_EQUAL', + flags\BINARY_IS_SMALLER => 'BINARY_IS_SMALLER', + flags\BINARY_IS_SMALLER_OR_EQUAL => 'BINARY_IS_SMALLER_OR_EQUAL', + flags\BINARY_IS_GREATER => 'BINARY_IS_GREATER', + flags\BINARY_IS_GREATER_OR_EQUAL => 'BINARY_IS_GREATER_OR_EQUAL', + flags\BINARY_SPACESHIP => 'BINARY_SPACESHIP', + ], + ast\AST_ASSIGN_OP => $shared_binary_ops, + ast\AST_MAGIC_CONST => [ + flags\MAGIC_LINE => 'MAGIC_LINE', + flags\MAGIC_FILE => 'MAGIC_FILE', + flags\MAGIC_DIR => 'MAGIC_DIR', + flags\MAGIC_NAMESPACE => 'MAGIC_NAMESPACE', + flags\MAGIC_FUNCTION => 'MAGIC_FUNCTION', + flags\MAGIC_METHOD => 'MAGIC_METHOD', + flags\MAGIC_CLASS => 'MAGIC_CLASS', + flags\MAGIC_TRAIT => 'MAGIC_TRAIT', + ], + ast\AST_USE => $use_types, + ast\AST_GROUP_USE => $use_types, + ast\AST_USE_ELEM => $use_types, + ast\AST_INCLUDE_OR_EVAL => [ + flags\EXEC_EVAL => 'EXEC_EVAL', + flags\EXEC_INCLUDE => 'EXEC_INCLUDE', + flags\EXEC_INCLUDE_ONCE => 'EXEC_INCLUDE_ONCE', + flags\EXEC_REQUIRE => 'EXEC_REQUIRE', + flags\EXEC_REQUIRE_ONCE => 'EXEC_REQUIRE_ONCE', + ], + ast\AST_ARRAY => [ + flags\ARRAY_SYNTAX_LIST => 'ARRAY_SYNTAX_LIST', + flags\ARRAY_SYNTAX_LONG => 'ARRAY_SYNTAX_LONG', + flags\ARRAY_SYNTAX_SHORT => 'ARRAY_SYNTAX_SHORT', + ], + ast\AST_ARRAY_ELEM => [ + flags\ARRAY_ELEM_REF => 'ARRAY_ELEM_REF', + ], + ast\AST_CLOSURE_VAR => [ + flags\CLOSURE_USE_REF => 'CLOSURE_USE_REF', + ], + ]; + + $combinable = [ + ast\AST_METHOD => $function_modifiers, + ast\AST_FUNC_DECL => $function_modifiers, + ast\AST_CLOSURE => $function_modifiers, + ast\AST_ARROW_FUNC => $function_modifiers, + ast\AST_CLASS_CONST_DECL => [ + flags\MODIFIER_PUBLIC => 'MODIFIER_PUBLIC', + flags\MODIFIER_PROTECTED => 'MODIFIER_PROTECTED', + flags\MODIFIER_PRIVATE => 'MODIFIER_PRIVATE', + ], + ast\AST_PROP_GROUP => $property_modifiers, + ast\AST_TRAIT_ALIAS => $property_modifiers, + ast\AST_DIM => [ + flags\DIM_ALTERNATIVE_SYNTAX => 'DIM_ALTERNATIVE_SYNTAX', + ], + ast\AST_CONDITIONAL => [ + flags\PARENTHESIZED_CONDITIONAL => 'PARENTHESIZED_CONDITIONAL', + ], + ast\AST_PARAM => [ + flags\PARAM_REF => 'PARAM_REF', + flags\PARAM_VARIADIC => 'PARAM_VARIADIC', + ], + ]; + } + + return [$exclusive, $combinable]; + } + + /** + * Print a message with the file and line. + * @suppress PhanUnreferencedPublicMethod added for debugging + */ + public static function debugLog(string $message): void + { + $frame = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS)[0]; + \fprintf(\STDERR, "%s:%d %s\n", $frame['file'] ?? 'unknown', $frame['line'] ?? 0, $message); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Debug/Breakpoint.php b/bundled-libs/phan/phan/src/Phan/Debug/Breakpoint.php new file mode 100644 index 000000000..4b866ecd6 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Debug/Breakpoint.php @@ -0,0 +1,59 @@ + + */ + +\readline_completion_function(static function (string $input): array { + $matches = []; + foreach (\get_declared_classes() as $class_name) { + if (\strpos($class_name, $input) === 0) { + $matches[] = $class_name; + } + } + return $matches; +}); + +print "\n"; +do { + /** @var string|null */ + $input = \readline("breakpoint> "); + + if (\is_string($input)) { + \readline_add_history($input); + } + + if (\in_array($input, [ + 'quit', + 'exit', + 'continue', + 'run', + 'c' + ], true)) { + break; + } + try { + // @phan-suppress-next-line PhanPluginUnsafeEval this is only used ever manually for debugging + eval($input . ';'); + } catch (\ParseError $exception) { + print "Parse error in `$input`\n"; + } catch (\CompileError $exception) { + print "Compile error in `$input`\n"; + } catch (\Throwable $exception) { + print $exception->getMessage() . "\n"; + print $exception->getTraceAsString() . "\n"; + } + print "\n"; +} while (true); diff --git a/bundled-libs/phan/phan/src/Phan/Debug/DebugUnionType.php b/bundled-libs/phan/phan/src/Phan/Debug/DebugUnionType.php new file mode 100644 index 000000000..5e8f96cf3 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Debug/DebugUnionType.php @@ -0,0 +1,43 @@ +get()) . ')'; + } + if ($value instanceof None) { + return 'None'; + } + + if ($value instanceof Method) { + return get_class($value) . '(' . $value->getRepresentationForIssue() . ')'; + } + if ($value instanceof AddressableElement + || $value instanceof UnaddressableTypedElement + || $value instanceof UnionType + || $value instanceof Context + || $value instanceof Type) { + return get_class($value) . '(' . $value . ')'; + } + return get_class($value) . '(' . StringUtil::jsonEncode($value) . ')'; + } + if (!is_array($value)) { + if (is_resource($value)) { + \ob_start(); + // @phan-suppress-next-line PhanPluginRemoveDebugCall + \var_dump($value); + return \trim(\ob_get_clean() ?: 'resource'); + } + return StringUtil::jsonEncode($value); + } + if ($max_depth <= 0) { + return count($value) > 0 ? '[...]' : '[]'; + } + $is_consecutive = true; + $i = 0; + foreach ($value as $key => $_) { + if ($key !== $i) { + $is_consecutive = false; + break; + } + $i++; + } + + if ($is_consecutive) { + $result = []; + foreach ($value as $i => $inner_value) { + if ($i >= 10) { + $result[] = '... ' . (count($value) - 10) . ' more element(s)'; + break; + } + $result[] = self::encodeValue($inner_value); + } + return '[' . \implode(', ', $result) . ']'; + } + $result = []; + $i = 0; + foreach ($value as $key => $inner_value) { + $i++; + if ($i > 10) { + $result[] = '... ' . (count($value) - 10) . ' more field(s)'; + break; + } + $result[] = StringUtil::jsonEncode($key) . ':' . self::encodeValue($inner_value); + } + return '{' . \implode(', ', $result) . '}'; + } + + /** + * Utility to show more information about an unexpected error + * @param array $frame the frame from debug_backtrace() + */ + public static function frameToString(array $frame): string + { + return \with_disabled_phan_error_handler(static function () use ($frame): string { + $invocation = $frame['function'] ?? '(unknown)'; + if (isset($frame['class'])) { + $invocation = $frame['class'] . ($frame['type'] ?? '::') . $invocation; + } + $result = $invocation . '()'; + $args = $frame['args'] ?? null; + if (isset($frame['file'])) { + $result .= ' called at [' . $frame['file'] . ':' . ($frame['line'] ?? 0) . ']'; + } + if ($args) { + $result .= ' Args: ' . self::encodeValue($args); + } + return $result; + }); + } + + /** + * Returns details about a call to asExpandedTypes that hit a RecursionDepthException + */ + public static function getExpandedTypesDetails(): string + { + $result = []; + foreach (\debug_backtrace() as $frame) { + if (($frame['function'] ?? null) === 'asExpandedTypes' && isset($frame['object'])) { + $object = $frame['object']; + if ($object instanceof Type) { + $result[] = 'when expanding type (' . (string)$object . ')'; + } elseif ($object instanceof UnionType) { + $result[] = 'when expanding union type (' . (string)$object . ')'; + } + } + } + return \implode("\n", $result); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Debug/SignalHandler.php b/bundled-libs/phan/phan/src/Phan/Debug/SignalHandler.php new file mode 100644 index 000000000..0eff55d2b --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Debug/SignalHandler.php @@ -0,0 +1,53 @@ +missing_fqsen = $missing_fqsen; + } + + /** + * @return bool + * True if we have an FQSEN defined + * + * @suppress PhanUnreferencedPublicMethod + */ + public function hasFQSEN(): bool + { + return !\is_null($this->missing_fqsen); + } + + /** + * @return FQSEN + * The missing FQSEN + */ + public function getFQSEN(): FQSEN + { + $fqsen = $this->missing_fqsen; + if (!$fqsen) { + throw new AssertionError('Should check CodeBaseException->hasFQSEN()'); + } + return $fqsen; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Exception/EmptyFQSENException.php b/bundled-libs/phan/phan/src/Phan/Exception/EmptyFQSENException.php new file mode 100644 index 000000000..64299cac8 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Exception/EmptyFQSENException.php @@ -0,0 +1,12 @@ +fqsen = $fqsen; + } + + /** + * @return string the empty, unparseable FQSEN input that caused this exception + */ + public function getFQSEN(): string + { + return $this->fqsen; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Exception/InvalidFQSENException.php b/bundled-libs/phan/phan/src/Phan/Exception/InvalidFQSENException.php new file mode 100644 index 000000000..cd4458a22 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Exception/InvalidFQSENException.php @@ -0,0 +1,12 @@ +getFile(), + * $node->getLine() ?? 0 + * ) + * ); + * ``` + */ +class IssueException extends Exception +{ + + /** + * @var IssueInstance + * An instance of an issue that was found but can't be + * reported on immediately. + */ + private $issue_instance; + + /** + * @param IssueInstance $issue_instance + * An instance of an issue that was found but can't be + * reported on immediately. + */ + public function __construct( + IssueInstance $issue_instance + ) { + parent::__construct(); + $this->issue_instance = $issue_instance; + } + + /** + * @return IssueInstance + * The issue that was found + */ + public function getIssueInstance(): IssueInstance + { + return $this->issue_instance; + } + + /** + * @override + */ + public function __toString(): string + { + return \sprintf( + "IssueException at %s:%d: %s\n%s", + $this->getFile(), + $this->getLine(), + (string)$this->issue_instance, + $this->getTraceAsString() + ); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Exception/NodeException.php b/bundled-libs/phan/phan/src/Phan/Exception/NodeException.php new file mode 100644 index 000000000..1812d794b --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Exception/NodeException.php @@ -0,0 +1,51 @@ +node = $node; + } + + /** + * @return Node + * The node for which we have an exception + * + * @suppress PhanUnreferencedPublicMethod + */ + public function getNode(): Node + { + return $this->node; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Exception/RecursionDepthException.php b/bundled-libs/phan/phan/src/Phan/Exception/RecursionDepthException.php new file mode 100644 index 000000000..da39d58a7 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Exception/RecursionDepthException.php @@ -0,0 +1,14 @@ +class = $class; + $this->prop_name = $prop_name; + } + + /** + * Returns the class which the magic property belongs to + */ + public function getClass(): Clazz + { + return $this->class; + } + + /** + * Returns the property name of the magic property + * @suppress PhanUnreferencedPublicMethod added for API completeness + */ + public function getPropName(): string + { + return $this->prop_name; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Exception/UsageException.php b/bundled-libs/phan/phan/src/Phan/Exception/UsageException.php new file mode 100644 index 000000000..a3820fcac --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Exception/UsageException.php @@ -0,0 +1,46 @@ +print_type = $print_type ?? self::PRINT_INVALID_ARGS; + $this->forbid_color = $forbid_color; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/ForkPool.php b/bundled-libs/phan/phan/src/Phan/ForkPool.php new file mode 100644 index 000000000..45158d384 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/ForkPool.php @@ -0,0 +1,373 @@ + a list of process ids that have been forked*/ + private $child_pid_list = []; + + /** @var list a list of read strings for $this->child_pid_list */ + private $read_streams = []; + + /** @var list a list of Readers for $this->child_pid_list */ + private $readers = []; + + /** @var list a map from workers to their progress */ + private $progress = []; + + /** @var float the maximum memory usage at any time during the run. Excludes finished workers. */ + private $max_total_mem = 0; + + /** @var list the combination of issues emitted by all workers */ + private $issues = []; + + /** @var bool did any of the child processes fail (e.g. crash or send data that couldn't be unserialized) */ + private $did_have_error = false; + + private function updateProgress(int $i, Progress $progress): void + { + // fwrite(STDERR, "Received progress from $i " . json_encode($progress) . "\n"); + $this->progress[$i] = $progress; + + static $previous_update_time = 0.0; + $time = \microtime(true); + + // If not enough time has elapsed, then don't update the progress bar. + // Making the update frequency based on time (instead of the number of files) + // prevents the terminal from rapidly flickering while processing small files. + $interval = Config::getValue('progress_bar_sample_interval'); + if ($time - $previous_update_time < $interval) { + // Make sure to output 100%, to avoid confusion. + // https://github.com/phan/phan/issues/2694 + if ($progress->progress < 1.0) { + return; + } + } + if ($previous_update_time) { + $previous_update_time += $interval; + } else { + $previous_update_time = $time; + } + $this->renderAggregateProgress(); + } + + private function renderAggregateProgress(): void + { + $total_progress = 0.0; + $total_cur_mem = 0.0; + $total_max_mem = 0.0; + $file_count = 0; + $analyzed_files = 0; + foreach ($this->progress as $progress) { + $total_progress += $progress->progress; + $total_cur_mem += $progress->cur_mem / 1024 / 1024; + if ($progress->cur_mem) { + $total_max_mem += $progress->max_mem / 1024 / 1024; + } + $file_count += $progress->file_count; + $analyzed_files += $progress->analyzed_files; + } + $this->max_total_mem = \max($this->max_total_mem, $total_max_mem); + CLI::outputProgressLine('analyze', $total_progress / count($this->progress), $total_cur_mem, $this->max_total_mem, $file_count, $analyzed_files); + } + + /** + * @param array $process_task_data_iterator + * An array of task data items to be divided up among the + * workers. The size of this is the number of forked processes. + * + * @param Closure():void $startup_closure + * A closure to execute upon starting a child + * + * @param Closure(int,mixed,int) $task_closure + * A method to execute on each task data. + * This closure must return an array (to be gathered). + * + * @param Closure():array $shutdown_closure + * A closure to execute upon shutting down a child + * @throws InvalidArgumentException if count($process_task_data_iterator) < 2 + * @throws AssertionError if pcntl is disabled before using this + * @suppress PhanAccessMethodInternal + */ + public function __construct( + array $process_task_data_iterator, + Closure $startup_closure, + Closure $task_closure, + Closure $shutdown_closure + ) { + $process_task_data_iterator = \array_values($process_task_data_iterator); + + $pool_size = count($process_task_data_iterator); + + if ($pool_size < 2) { + throw new InvalidArgumentException('The pool size must be >= 2 to use the fork pool.'); + } + + if (!\extension_loaded('pcntl')) { + throw new AssertionError('The pcntl extension must be loaded in order for Phan to be able to fork.'); + } + + // We'll keep track of if this is the parent process + // so that we can tell who will be doing the waiting + $is_parent = false; + + // Fork as many times as requested to get the given + // pool size + for ($proc_id = 0; $proc_id < $pool_size; $proc_id++) { + // Create an IPC socket pair. + $sockets = \stream_socket_pair(\STREAM_PF_UNIX, \STREAM_SOCK_STREAM, \STREAM_IPPROTO_IP); + if (!$sockets) { + \error_log("unable to create stream socket pair"); + exit(EXIT_FAILURE); + } + + // Fork + $pid = \pcntl_fork(); + if ($pid < 0) { + \error_log(\posix_strerror(\posix_get_last_error())); + exit(EXIT_FAILURE); + } + + // Parent + if ($pid > 0) { + $is_parent = true; + $this->child_pid_list[] = $pid; + $read_stream = self::streamForParent($sockets); + $i = \count($this->progress); + $task_data_iterator = $process_task_data_iterator[$proc_id]; + $this->progress[] = new Progress(0.0, 0, count($task_data_iterator)); + $this->read_streams[] = $read_stream; + $this->readers[intval($read_stream)] = new Reader($read_stream, function (string $notification_type, string $payload) use ($i): void { + switch ($notification_type) { + case Writer::TYPE_PROGRESS: + $progress = unserialize($payload); + $this->updateProgress($i, $progress); + break; + case Writer::TYPE_ISSUE_LIST: + // This worker has stopped, and should no longer be using memory shortly + // @phan-suppress-next-line PhanAccessReadOnlyProperty + $this->progress[$i]->cur_mem = 0; + + $issues = unserialize($payload); + if ($issues) { + \array_push($this->issues, ...$issues); + } + break; + } + }); + continue; + } + + // Child + if ($pid === 0) { + $is_parent = false; + break; + } + } + + // If we're the parent, return + if ($is_parent) { + return; + } + + // Get the write stream for the child. + if (!isset($sockets)) { + throw new AssertionError('$sockets must be set if this is the child process'); + } + $write_stream = self::streamForChild($sockets); + Writer::initialize($write_stream); + + // Execute anything the children wanted to execute upon + // starting up + $startup_closure(); + + // Get the work for this process + $task_data_iterator = $process_task_data_iterator[$proc_id]; + $task_count = \count($task_data_iterator); + foreach ($task_data_iterator as $i => $task_data) { + $task_closure($i, $task_data, $task_count); + } + + // Execute each child's shutdown closure before + // exiting the process + $results = $shutdown_closure(); + + // Serialize this child's produced results and send them to the parent. + Writer::emitIssues($results ?: []); + + \fclose($write_stream); + + // Children exit after completing their work + exit(EXIT_SUCCESS); + } + + /** + * Prepare the socket pair to be used in a parent process and + * return the stream the parent will use to read results. + * + * @param array{0:resource, 1:resource} $sockets the socket pair for IPC + * @return resource + */ + private static function streamForParent(array $sockets) + { + [$for_read, $for_write] = $sockets; + + // The parent will not use the write channel, so it + // must be closed to prevent deadlock. + \fclose($for_write); + + // stream_select will be used to read multiple streams, so these + // must be set to non-blocking mode. + if (!\stream_set_blocking($for_read, false)) { + \error_log('unable to set read stream to non-blocking'); + exit(EXIT_FAILURE); + } + + return $for_read; + } + + /** + * Prepare the socket pair to be used in a child process and return + * the stream the child will use to write results. + * + * @param array{0:resource, 1:resource} $sockets the socket pair for IPC + * @return resource + */ + private static function streamForChild(array $sockets) + { + [$for_read, $for_write] = $sockets; + + // The while will not use the read channel, so it must + // be closed to prevent deadlock. + \fclose($for_read); + return $for_write; + } + + /** + * Read the results that each child process has serialized on their write streams. + * The results are returned in an array, one for each worker. The order of the results + * is not maintained. + * + * @return list + * @suppress PhanAccessMethodInternal + */ + private function readResultsFromChildren(): array + { + // Create an array of all active streams, indexed by + // resource id. + $streams = []; + foreach ($this->read_streams as $stream) { + $streams[intval($stream)] = $stream; + } + + // Read the data off of all the stream. + while (count($streams) > 0) { + $needs_read = \array_values($streams); + $needs_write = null; + $needs_except = null; + + // Wait for data on at least one stream. + $num = \stream_select($needs_read, $needs_write, $needs_except, null /* no timeout */); + if ($num === false) { + \error_log("unable to select on read stream"); + exit(EXIT_FAILURE); + } + + // For each stream that was ready, read the content. + foreach ($this->readers as $reader) { + $reader->readMessages(); + } + foreach ($needs_read as $file) { + if (\feof($file)) { + \fclose($file); + $idx = intval($file); + unset($streams[$idx]); + } + } + } + $this->assertAnalysisWorkersExitedNormally(); + + return $this->issues; + } + + /** + * Exit with a non-zero exit code if any of the workers exited without sending a valid response. + * @suppress PhanAccessMethodInternal + */ + private function assertAnalysisWorkersExitedNormally(): void + { + // Verify that the readers worked. + $saw_errors = false; + foreach ($this->readers as $reader) { + $errors = $reader->computeErrorsAfterRead(); + if (StringUtil::isNonZeroLengthString($errors)) { + // @phan-suppress-next-line PhanPluginRemoveDebugCall + \fwrite(\STDERR, "Saw errors for an analysis worker:\n" . $errors); + $saw_errors = true; + } + } + if ($saw_errors) { + exit(EXIT_FAILURE); + } + } + + /** + * Wait for all child processes to complete + * @return list + */ + public function wait(): array + { + // Read all the streams from child processes into an array. + $content = $this->readResultsFromChildren(); + + // Wait for all children to return + foreach ($this->child_pid_list as $child_pid) { + if (\pcntl_waitpid($child_pid, $status) < 0) { + \error_log(\posix_strerror(\posix_get_last_error())); + } + + // Check to see if the child died a graceful death + if (\pcntl_wifsignaled($status)) { + $return_code = \pcntl_wexitstatus($status); + $term_sig = \pcntl_wtermsig($status); + $this->did_have_error = true; + \error_log("Child terminated with return code $return_code and signal $term_sig"); + } + } + + return $content; + } + + /** + * Returns true if this had an error, e.g. due to memory limits or due to a child process crashing. + */ + public function didHaveError(): bool + { + return $this->did_have_error; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/ForkPool/Progress.php b/bundled-libs/phan/phan/src/Phan/ForkPool/Progress.php new file mode 100644 index 000000000..670a0db2f --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/ForkPool/Progress.php @@ -0,0 +1,35 @@ +progress = $progress; + $this->cur_mem = \memory_get_usage(); + $this->max_mem = \memory_get_peak_usage(); + $this->analyzed_files = $analyzed_files; + $this->file_count = $file_count; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/ForkPool/Reader.php b/bundled-libs/phan/phan/src/Phan/ForkPool/Reader.php new file mode 100644 index 000000000..964633087 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/ForkPool/Reader.php @@ -0,0 +1,128 @@ + the JSON-RPC headers. Currently just Content-Length. */ + private $headers; + + /** @var Closure(string,string):void $notification_handler */ + private $notification_handler; + + /** @var array $read_messages the count of read messages of each type */ + private $read_messages = []; + + /** @var bool was the end of the input stream reached */ + private $eof = false; + + /** + * @param resource $input + * @param Closure(string,string):void $notification_handler + */ + public function __construct($input, Closure $notification_handler) + { + if (!\is_resource($input)) { + throw new TypeError('Expected resource for $input, got ' . \gettype($input)); + } + $this->input = $input; + $this->notification_handler = $notification_handler; + } + + /** + * Read serialized messages from the analysis workers + */ + public function readMessages(): void + { + if ($this->eof) { + return; + } + while (($c = \fgetc($this->input)) !== false && $c !== '') { + $this->buffer .= $c; + switch ($this->parsing_mode) { + case self::PARSE_HEADERS: + if ($this->buffer === "\r\n") { + $this->parsing_mode = self::PARSE_BODY; + $this->content_length = (int)$this->headers['Content-Length']; + $this->notification_type = $this->headers['Notification-Type'] ?? 'unknown'; + $this->buffer = ''; + } elseif (\substr($this->buffer, -2) === "\r\n") { + $parts = \explode(':', $this->buffer); + $this->headers[$parts[0]] = \trim($parts[1]); + $this->buffer = ''; + } + break; + case self::PARSE_BODY: + if (\strlen($this->buffer) < $this->content_length) { + // We know the number of remaining bytes to read - try to read them all at once. + $buf = \fread($this->input, $this->content_length - \strlen($this->buffer)); + if (\is_string($buf) && \strlen($buf) > 0) { + $this->buffer .= $buf; + } + } + if (\strlen($this->buffer) === $this->content_length) { + $this->read_messages[$this->notification_type] = ($this->read_messages[$this->notification_type] ?? 0) + 1; + ($this->notification_handler)($this->notification_type, $this->buffer); + $this->parsing_mode = self::PARSE_HEADERS; + $this->headers = []; + $this->buffer = ''; + } + break; + } + } + $this->eof = \feof($this->input); + } + + /** + * Returns an error message for errors caused by an analysis worker exiting abnormally or sending invalid data. + * During normal operation, should return null. + */ + public function computeErrorsAfterRead(): ?string + { + $error = ""; + if (StringUtil::isNonZeroLengthString($this->buffer)) { + $error .= \sprintf("Saw non-empty buffer of length %d\n", \strlen($this->buffer)); + } + if ($this->parsing_mode !== self::PARSE_HEADERS) { + $error .= "Expected to be finished parsing the last message body\n"; + } + if (!$this->eof) { + $error .= "Expected to reach eof\n"; + } + if (!isset($this->read_messages[Writer::TYPE_ISSUE_LIST])) { + $error .= "Expected to have received a list of 0 or more issues (as the last notification)\n"; + } + return $error !== '' ? $error : null; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/ForkPool/Writer.php b/bundled-libs/phan/phan/src/Phan/ForkPool/Writer.php new file mode 100644 index 000000000..cb9f982dd --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/ForkPool/Writer.php @@ -0,0 +1,74 @@ + $issues + */ + public static function emitIssues(array $issues): void + { + self::writeNotification(self::TYPE_ISSUE_LIST, \serialize($issues)); + } + + /** + * Report the analysis progress + */ + public static function recordProgress(float $percent, int $files_analyzed, int $total_files): void + { + self::writeNotification(self::TYPE_PROGRESS, \serialize(new Progress($percent, $files_analyzed, $total_files))); + } + + /** + * @suppress PhanThrowTypeAbsent + */ + private static function writeNotification(string $type, string $payload): void + { + if (!\is_resource(self::$output)) { + throw new Error('Attempted to writeNotification before calling Writer::initialize'); + } + + \fwrite(self::$output, \sprintf("Content-Length: %d\r\nNotification-Type: %s\r\n\r\n%s", \strlen($payload), $type, $payload)); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Issue.php b/bundled-libs/phan/phan/src/Phan/Issue.php new file mode 100644 index 000000000..ef0661c7a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Issue.php @@ -0,0 +1,5694 @@ + 'AccessError', + self::CATEGORY_ANALYSIS => 'Analysis', + self::CATEGORY_COMMENT => 'CommentError', + self::CATEGORY_COMPATIBLE => 'CompatError', + self::CATEGORY_CONTEXT => 'Context', + self::CATEGORY_DEPRECATED => 'DeprecatedError', + self::CATEGORY_GENERIC => 'Generic', + self::CATEGORY_INTERNAL => 'Internal', + self::CATEGORY_NOOP => 'NOOPError', + self::CATEGORY_PARAMETER => 'ParamError', + self::CATEGORY_PLUGIN => 'Plugin', + self::CATEGORY_REDEFINE => 'RedefineError', + self::CATEGORY_STATIC => 'StaticCallError', + self::CATEGORY_SYNTAX => 'Syntax', + self::CATEGORY_TYPE => 'TypeError', + self::CATEGORY_UNDEFINED => 'UndefError', + self::CATEGORY_VARIABLE => 'VarError', + ]; + + /** Low severity. E.g. documentation errors or code that would cause a (typically harmless) PHP notice. */ + public const SEVERITY_LOW = 0; + /** Normal severity. E.g. something that may cause a minor bug. */ + public const SEVERITY_NORMAL = 5; + /** Highest severity. Likely to cause an uncaught Error, Exception, or fatal error at runtime. */ + public const SEVERITY_CRITICAL = 10; + + // See https://docs.codeclimate.com/v1.0/docs/remediation + // TODO: Decide on a way to estimate these and bring these up to date once codeclimate updates phan. + // Right now, almost everything is REMEDIATION_B. + public const REMEDIATION_A = 1000000; + public const REMEDIATION_B = 3000000; + /** @suppress PhanUnreferencedPublicClassConstant */ + public const REMEDIATION_C = 6000000; + /** @suppress PhanUnreferencedPublicClassConstant */ + public const REMEDIATION_D = 12000000; + /** @suppress PhanUnreferencedPublicClassConstant */ + public const REMEDIATION_E = 16000000; + /** @suppress PhanUnreferencedPublicClassConstant */ + public const REMEDIATION_F = 18000000; + + // type id constants. + public const TYPE_ID_UNKNOWN = 999; + + // Keep sorted and in sync with Colorizing::default_color_for_template + public const UNCOLORED_FORMAT_STRING_FOR_TEMPLATE = [ + 'CLASS' => '%s', + 'CLASSLIKE' => '%s', + 'CODE' => '%s', // A snippet from the code + 'COMMENT' => '%s', // contents of a phpdoc comment + 'CONST' => '%s', + 'COUNT' => '%d', + 'DETAILS' => '%s', // additional details about an error + 'FILE' => '%s', + 'FUNCTIONLIKE' => '%s', + 'FUNCTION' => '%s', + 'INDEX' => '%d', + 'INTERFACE' => '%s', + 'ISSUETYPE' => '%s', // used by Phan\Output\Printer, for minor issues. + 'ISSUETYPE_CRITICAL' => '%s', // for critical issues + 'ISSUETYPE_NORMAL' => '%s', // for normal issues + 'LINE' => '%d', + 'METHOD' => '%s', + 'NAMESPACE' => '%s', + 'OPERATOR' => '%s', + 'PARAMETER' => '%s', + 'PROPERTY' => '%s', + 'SCALAR' => '%s', // A scalar from the code + 'STRING_LITERAL' => '%s', // A string literal from the code + 'SUGGESTION' => '%s', + 'TYPE' => '%s', + 'TRAIT' => '%s', + 'VARIABLE' => '%s', + ]; + + /** @var string the type of this issue */ + private $type; + + /** + * @var int (a preferably unique integer for $type, for the pylint output formatter) + * Built in issue types must have a unique type id. + */ + private $type_id; + + /** @var int the category of this issue (self::CATEGORY_*) */ + private $category; + + /** @var int the severity of this issue (self::SEVERITY_*) */ + private $severity; + + /** @var string The format string for this issue type. Contains a mix of {CLASS} and %s/%d annotations. Used for colorizing option. */ + private $template_raw; + + /** @var string The printf format string for this issue type. If --color is enabled, this will have unix color codes. */ + private $template; + + /** @var int the expected number of arguments to the format string $this->template */ + private $argument_count; + + /** @var int self::REMEDIATION_* */ + private $remediation_difficulty; + + /** + * @param string $type the type of this issue + * @param int $category the category of this issue (self::CATEGORY_*) + * @param int $severity the severity of this issue (self::SEVERITY_*) + * @param string $template_raw the template string for issue messages. Contains a mix of {CLASS} and %s/%d annotations. + * @param int $remediation_difficulty self::REMEDIATION_* + * @param int $type_id (unique integer id for $type) + */ + public function __construct( + string $type, + int $category, + int $severity, + string $template_raw, + int $remediation_difficulty, + int $type_id + ) { + $this->type = $type; + $this->category = $category; + $this->severity = $severity; + $this->template_raw = $template_raw; + $this->template = self::templateToFormatString($template_raw); + $this->remediation_difficulty = $remediation_difficulty; + $this->type_id = $type_id; + } + + /** + * Converts the Phan template string to a regular format string. + */ + public static function templateToFormatString( + string $template + ): string { + /** @param list $matches */ + return \preg_replace_callback('/{([A-Z_]+)}/', static function (array $matches) use ($template): string { + $key = $matches[1]; + $replacement_exists = \array_key_exists($key, self::UNCOLORED_FORMAT_STRING_FOR_TEMPLATE); + if (!$replacement_exists) { + \error_log(\sprintf( + "No coloring info for issue message (%s), key {%s}. Valid template types: %s", + $template, + $key, + \implode(', ', \array_keys(self::UNCOLORED_FORMAT_STRING_FOR_TEMPLATE)) + )); + return '%s'; + } + return self::UNCOLORED_FORMAT_STRING_FOR_TEMPLATE[$key]; + }, $template); + } + + /** + * @return array + */ + public static function issueMap(): array + { + static $error_map; + return $error_map ?? ($error_map = self::generateIssueMap()); + } + + /** + * @return array + */ + private static function generateIssueMap(): array + { + // phpcs:disable Generic.Files.LineLength + /** + * @var list + * Note: All type ids should be unique, and be grouped by the category. + * (E.g. If the category is (1 << x), then the type_id should be x*1000 + y + * If new type ids are added, existing ones should not be changed. + */ + $error_list = [ + // Issue::CATEGORY_SYNTAX + new Issue( + self::SyntaxError, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + "%s", + self::REMEDIATION_A, + 17000 + ), + new Issue( + self::InvalidConstantExpression, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + "Constant expression contains invalid operations", + self::REMEDIATION_A, + 17001 + ), + new Issue( + self::InvalidNode, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + "%s", + self::REMEDIATION_A, + 17002 + ), + new Issue( + self::InvalidWriteToTemporaryExpression, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + "Cannot use temporary expression ({CODE} of type {TYPE}) in write context", + self::REMEDIATION_A, + 17003 + ), + new Issue( + self::InvalidTraitUse, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + 'Invalid trait use: {DETAILS}', + self::REMEDIATION_A, + 17004 + ), + // Could try to make a better suggestion, optionally + new Issue( + self::ContinueTargetingSwitch, + self::CATEGORY_SYNTAX, + self::SEVERITY_NORMAL, + '"continue" targeting switch is equivalent to "break". Did you mean to use "continue 2"?', + self::REMEDIATION_A, + 17005 + ), + new Issue( + self::ContinueOrBreakNotInLoop, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + '\'{OPERATOR}\' not in the \'loop\' or \'switch\' context.', + self::REMEDIATION_A, + 17006 + ), + new Issue( + self::ContinueOrBreakTooManyLevels, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + 'Cannot \'{OPERATOR}\' {INDEX} levels.', + self::REMEDIATION_A, + 17007 + ), + new Issue( + self::DuplicateUseNormal, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + "Cannot use {CLASSLIKE} as {CLASSLIKE} because the name is already in use", + self::REMEDIATION_B, + 17008 + ), + new Issue( + self::DuplicateUseFunction, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + "Cannot use function {FUNCTION} as {FUNCTION} because the name is already in use", + self::REMEDIATION_B, + 17009 + ), + new Issue( + self::DuplicateUseConstant, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + "Cannot use constant {CONST} as {CONST} because the name is already in use", + self::REMEDIATION_B, + 17010 + ), + new Issue( + self::SyntaxCompileWarning, + self::CATEGORY_SYNTAX, + self::SEVERITY_NORMAL, + 'Saw a warning while parsing: {DETAILS}', + self::REMEDIATION_A, + 17011 + ), + new Issue( + self::SyntaxEmptyListArrayDestructuring, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + 'Cannot use an empty list in the left hand side of an array destructuring operation', + self::REMEDIATION_A, + 17012 + ), + new Issue( + self::SyntaxMixedKeyNoKeyArrayDestructuring, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + 'Cannot mix keyed and unkeyed array entries in array destructuring assignments ({CODE})', + self::REMEDIATION_A, + 17013 + ), + new Issue( + self::SyntaxReturnValueInVoid, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + 'Syntax error: {TYPE} function {FUNCTIONLIKE} must not return a value (did you mean "{CODE}" instead of "{CODE}"?)', + self::REMEDIATION_A, + 17014 + ), + new Issue( + self::SyntaxReturnExpectedValue, + self::CATEGORY_SYNTAX, + self::SEVERITY_CRITICAL, + 'Syntax error: Function {FUNCTIONLIKE} with return type {TYPE} must return a value (did you mean "{CODE}" instead of "{CODE}"?)', + self::REMEDIATION_A, + 17015 + ), + + // Issue::CATEGORY_UNDEFINED + new Issue( + self::EmptyFile, + self::CATEGORY_UNDEFINED, + self::SEVERITY_LOW, + "Empty file {FILE}", + self::REMEDIATION_B, + 11000 + ), + new Issue( + self::MissingRequireFile, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Missing required file {FILE}", + self::REMEDIATION_B, + 11040 + ), + new Issue( + self::InvalidRequireFile, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Required file {FILE} is not a file", + self::REMEDIATION_B, + 11041 + ), + new Issue( + self::ParentlessClass, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Reference to parent of class {CLASS} that does not extend anything", + self::REMEDIATION_B, + 11001 + ), + new Issue( + self::UndeclaredClass, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Reference to undeclared class {CLASS}", + self::REMEDIATION_B, + 11002 + ), + new Issue( + self::UndeclaredExtendedClass, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Class extends undeclared class {CLASS}", + self::REMEDIATION_B, + 11003 + ), + new Issue( + self::UndeclaredInterface, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Class implements undeclared interface {INTERFACE}", + self::REMEDIATION_B, + 11004 + ), + new Issue( + self::UndeclaredTrait, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Class uses undeclared trait {TRAIT}", + self::REMEDIATION_B, + 11005 + ), + new Issue( + self::UndeclaredClassCatch, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Catching undeclared class {CLASS}", + self::REMEDIATION_B, + 11006 + ), + new Issue( + self::UndeclaredClassConstant, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Reference to constant {CONST} from undeclared class {CLASS}", + self::REMEDIATION_B, + 11007 + ), + new Issue( + self::UndeclaredClassInstanceof, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Checking instanceof against undeclared class {CLASS}", + self::REMEDIATION_B, + 11008 + ), + new Issue( + self::UndeclaredClassMethod, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Call to method {METHOD} from undeclared class {CLASS}", + self::REMEDIATION_B, + 11009 + ), + new Issue( + self::UndeclaredClassProperty, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Reference to instance property {PROPERTY} from undeclared class {CLASS}", + self::REMEDIATION_B, + 11038 + ), + new Issue( + self::UndeclaredClassStaticProperty, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Reference to static property {PROPERTY} from undeclared class {CLASS}", + self::REMEDIATION_B, + 11039 + ), + new Issue( + self::UndeclaredClassReference, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Reference to undeclared class {CLASS}", + self::REMEDIATION_B, + 11010 + ), + new Issue( + self::UndeclaredConstant, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Reference to undeclared constant {CONST}. This will cause a thrown Error in php 8.0+.", + self::REMEDIATION_B, + 11011 + ), + new Issue( + self::UndeclaredConstantOfClass, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Reference to undeclared class constant {CONST}", + self::REMEDIATION_B, + 11053 + ), + new Issue( + self::UndeclaredFunction, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Call to undeclared function {FUNCTION}", + self::REMEDIATION_B, + 11012 + ), + new Issue( + self::UndeclaredMethod, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Call to undeclared method {METHOD}", + self::REMEDIATION_B, + 11013 + ), + new Issue( + self::PossiblyUndeclaredMethod, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Call to possibly undeclared method {METHOD} on type {TYPE} ({TYPE} does not declare the method)", + self::REMEDIATION_B, + 11049 + ), + new Issue( + self::UndeclaredStaticMethod, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Static call to undeclared method {METHOD}", + self::REMEDIATION_B, + 11014 + ), + new Issue( + self::UndeclaredProperty, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Reference to undeclared property {PROPERTY}", + self::REMEDIATION_B, + 11015 + ), + new Issue( + self::PossiblyUndeclaredProperty, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Reference to possibly undeclared property {PROPERTY} of expression of type {TYPE} ({TYPE} does not declare that property)", + self::REMEDIATION_B, + 11050 + ), + new Issue( + self::UndeclaredStaticProperty, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Static property '{PROPERTY}' on {CLASS} is undeclared", + self::REMEDIATION_B, + 11016 + ), + new Issue( + self::TraitParentReference, + self::CATEGORY_UNDEFINED, + self::SEVERITY_LOW, + "Reference to parent from trait {TRAIT}", + self::REMEDIATION_B, + 11017 + ), + new Issue( + self::UndeclaredVariable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Variable \${VARIABLE} is undeclared", + self::REMEDIATION_B, + 11018 + ), + new Issue( + self::PossiblyUndeclaredVariable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Variable \${VARIABLE} is possibly undeclared", + self::REMEDIATION_B, + 11051 + ), + new Issue( + self::UndeclaredThis, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Variable \${VARIABLE} is undeclared", + self::REMEDIATION_B, + 11046 + ), + new Issue( + self::UndeclaredGlobalVariable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Global variable \${VARIABLE} is undeclared", + self::REMEDIATION_B, + 11047 + ), + new Issue( + self::PossiblyUndeclaredGlobalVariable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Global variable \${VARIABLE} is possibly undeclared", + self::REMEDIATION_B, + 11052 + ), + new Issue( + self::UndeclaredTypeParameter, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Parameter \${PARAMETER} has undeclared type {TYPE}", + self::REMEDIATION_B, + 11019 + ), + new Issue( + self::UndeclaredTypeProperty, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Property {PROPERTY} has undeclared type {TYPE}", + self::REMEDIATION_B, + 11020 + ), + new Issue( + self::UndeclaredTypeClassConstant, + self::CATEGORY_UNDEFINED, + self::SEVERITY_LOW, + "Class constant {CONST} has undeclared class type {TYPE}", + self::REMEDIATION_B, + 11054 + ), + new Issue( + self::UndeclaredClosureScope, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Reference to undeclared class {CLASS} in @phan-closure-scope", + self::REMEDIATION_B, + 11021 + ), + new Issue( + self::ClassContainsAbstractMethod, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "non-abstract class {CLASS} contains abstract method {METHOD} declared at {FILE}:{LINE}", + self::REMEDIATION_B, + 11022 + ), + new Issue( + self::ClassContainsAbstractMethodInternal, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "non-abstract class {CLASS} contains abstract internal method {METHOD}", + self::REMEDIATION_B, + 11023 + ), + new Issue( + self::UndeclaredAliasedMethodOfTrait, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Alias {METHOD} was defined for a method {METHOD} which does not exist in trait {TRAIT}", + self::REMEDIATION_B, + 11024 + ), + new Issue( + self::RequiredTraitNotAdded, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Required trait {TRAIT} for trait adaptation was not added to class", + self::REMEDIATION_B, + 11025 + ), + new Issue( + self::AmbiguousTraitAliasSource, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Trait alias {METHOD} has an ambiguous source method {METHOD} with more than one possible source trait. Possibilities: {TRAIT}", + self::REMEDIATION_B, + 11026 + ), + new Issue( + self::UndeclaredVariableDim, + self::CATEGORY_UNDEFINED, + self::SEVERITY_LOW, + "Variable \${VARIABLE} was undeclared, but array fields are being added to it.", + self::REMEDIATION_B, + 11027 + ), + new Issue( + self::UndeclaredVariableAssignOp, + self::CATEGORY_UNDEFINED, + self::SEVERITY_LOW, + "Variable \${VARIABLE} was undeclared, but it is being used as the left-hand side of an assignment operation", + self::REMEDIATION_B, + 11037 + ), + new Issue( + self::UndeclaredTypeReturnType, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Return type of {METHOD} is undeclared type {TYPE}", + self::REMEDIATION_B, + 11028 + ), + new Issue( + self::UndeclaredTypeThrowsType, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "@throws type of {METHOD} has undeclared type {TYPE}", + self::REMEDIATION_B, + 11034 + ), + new Issue( + self::UndeclaredClassAliasOriginal, + self::CATEGORY_UNDEFINED, + self::SEVERITY_CRITICAL, + "Reference to undeclared class {CLASS} for the original class of a class_alias for {CLASS}", + self::REMEDIATION_B, + 11029 + ), + new Issue( + self::UndeclaredClassInCallable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Reference to undeclared class {CLASS} in callable {METHOD}", + self::REMEDIATION_B, + 11030 + ), + new Issue( + self::UndeclaredStaticMethodInCallable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Reference to undeclared static method {METHOD} in callable", + self::REMEDIATION_B, + 11031 + ), + new Issue( + self::UndeclaredFunctionInCallable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Call to undeclared function {FUNCTION} in callable", + self::REMEDIATION_B, + 11032 + ), + new Issue( + self::UndeclaredMethodInCallable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Call to undeclared method {METHOD} in callable. Possible object type(s) for that method are {TYPE}", + self::REMEDIATION_B, + 11033 + ), + new Issue( + self::EmptyFQSENInCallable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Possible call to a function '{FUNCTIONLIKE}' with an empty FQSEN.", + self::REMEDIATION_B, + 11035 + ), + new Issue( + self::EmptyFQSENInClasslike, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Possible use of a classlike '{CLASSLIKE}' with an empty FQSEN.", + self::REMEDIATION_B, + 11036 + ), + new Issue( + self::InvalidFQSENInCallable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Possible call to a function '{FUNCTIONLIKE}' with an invalid FQSEN.", + self::REMEDIATION_B, + 11042 + ), + new Issue( + self::InvalidFQSENInClasslike, + self::CATEGORY_UNDEFINED, + self::SEVERITY_NORMAL, + "Possible use of a classlike '{CLASSLIKE}' with an invalid FQSEN.", + self::REMEDIATION_B, + 11043 + ), + new Issue( + self::UndeclaredMagicConstant, + self::CATEGORY_UNDEFINED, + self::SEVERITY_LOW, + "Reference to magic constant {CONST} that is undeclared in the current scope: {DETAILS}", + self::REMEDIATION_B, + 11044 + ), + new Issue( + self::UndeclaredInvokeInCallable, + self::CATEGORY_UNDEFINED, + self::SEVERITY_LOW, + "Possible attempt to access missing magic method {FUNCTIONLIKE} of '{CLASS}'", + self::REMEDIATION_B, + 11045 + ), + new Issue( + self::PossiblyUnsetPropertyOfThis, + self::CATEGORY_UNDEFINED, + self::SEVERITY_LOW, + 'Attempting to read property {PROPERTY} which was unset in the current scope', + self::REMEDIATION_B, + 11048 + ), + + + // Issue::CATEGORY_ANALYSIS + new Issue( + self::Unanalyzable, + self::CATEGORY_ANALYSIS, + self::SEVERITY_LOW, + "Expression is unanalyzable or feature is unimplemented. Please create an issue at https://github.com/phan/phan/issues/new.", + self::REMEDIATION_B, + 2000 + ), + new Issue( + self::UnanalyzableInheritance, + self::CATEGORY_ANALYSIS, + self::SEVERITY_LOW, + "Unable to determine the method(s) which {METHOD} overrides, but Phan inferred that it did override something earlier. Please create an issue at https://github.com/phan/phan/issues/new with a test case.", + self::REMEDIATION_B, + 2001 + ), + new Issue( + self::InvalidConstantFQSEN, + self::CATEGORY_ANALYSIS, + self::SEVERITY_NORMAL, + "'{CONST}' is an invalid FQSEN for a constant", + self::REMEDIATION_B, + 2002 + ), + new Issue( + self::ReservedConstantName, + self::CATEGORY_ANALYSIS, + self::SEVERITY_NORMAL, + "'{CONST}' has a reserved keyword in the constant name", + self::REMEDIATION_B, + 2003 + ), + + // Issue::CATEGORY_TYPE + new Issue( + self::TypeMismatchProperty, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Assigning {CODE} of type {TYPE} to property but {PROPERTY} is {TYPE}", + self::REMEDIATION_B, + 10001 + ), + new Issue( + self::PartialTypeMismatchProperty, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Assigning {CODE} of type {TYPE} to property but {PROPERTY} is {TYPE} ({TYPE} is incompatible)", + self::REMEDIATION_B, + 10063 + ), + new Issue( + self::PossiblyNullTypeMismatchProperty, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Assigning {CODE} of type {TYPE} to property but {PROPERTY} is {TYPE} ({TYPE} is incompatible)", + self::REMEDIATION_B, + 10064 + ), + new Issue( + self::PossiblyFalseTypeMismatchProperty, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Assigning {CODE} of type {TYPE} to property but {PROPERTY} is {TYPE} ({TYPE} is incompatible)", + self::REMEDIATION_B, + 10065 + ), + new Issue( + self::TypeMismatchDefault, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Default value for {TYPE} \${PARAMETER} can't be {TYPE}", + self::REMEDIATION_B, + 10002 + ), + new Issue( + self::TypeMismatchVariadicComment, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "{PARAMETER} is variadic in comment, but not variadic in param ({PARAMETER})", + self::REMEDIATION_B, + 10021 + ), + new Issue( + self::TypeMismatchVariadicParam, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + '{PARAMETER} is not variadic in comment, but variadic in param ({PARAMETER})', + self::REMEDIATION_B, + 10023 + ), + new Issue( + self::TypeMismatchArgument, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE} defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 10003 + ), + new Issue( + self::TypeMismatchArgumentProbablyReal, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE}{DETAILS} but {FUNCTIONLIKE} takes {TYPE}{DETAILS} defined at {FILE}:{LINE} (the inferred real argument type has nothing in common with the parameter\'s phpdoc type)', + self::REMEDIATION_B, + 10166 + ), + new Issue( + self::TypeMismatchArgumentReal, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE}{DETAILS} but {FUNCTIONLIKE} takes {TYPE}{DETAILS} defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 10140 + ), + new Issue( + self::TypeMismatchArgumentNullable, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE} defined at {FILE}:{LINE} (expected type to be non-nullable)', + self::REMEDIATION_B, + 10105 + ), + new Issue( + self::TypeMismatchArgumentInternal, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE}', + self::REMEDIATION_B, + 10004 + ), + new Issue( + self::TypeMismatchArgumentInternalReal, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE}{DETAILS} but {FUNCTIONLIKE} takes {TYPE}{DETAILS}', + self::REMEDIATION_B, + 10139 + ), + new Issue( + self::TypeMismatchArgumentInternalProbablyReal, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE}{DETAILS} but {FUNCTIONLIKE} takes {TYPE}{DETAILS}', + self::REMEDIATION_B, + 10148 + ), + new Issue( + self::TypeMismatchArgumentNullableInternal, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE} (expected type to be non-nullable)', + self::REMEDIATION_B, + 10106 + ), + new Issue( + self::TypeMismatchArgumentPropertyReference, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} is property {PROPERTY} with type {TYPE} but {FUNCTIONLIKE} takes a reference of type {TYPE}', + self::REMEDIATION_B, + 10141 + ), + new Issue( + self::TypeMismatchArgumentPropertyReferenceReal, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} is property {PROPERTY} with type {TYPE}{DETAILS} but {FUNCTIONLIKE} takes a reference of type {TYPE}{DETAILS}', + self::REMEDIATION_B, + 10142 + ), + new Issue( + self::TypeMismatchGeneratorYieldValue, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Yield statement has a value {CODE} with type {TYPE} but {FUNCTIONLIKE} is declared to yield values of type {TYPE} in {TYPE}", + self::REMEDIATION_B, + 10067 + ), + new Issue( + self::TypeMismatchGeneratorYieldKey, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Yield statement has a key {CODE} with type {TYPE} but {FUNCTIONLIKE} is declared to yield keys of type {TYPE} in {TYPE}", + self::REMEDIATION_B, + 10068 + ), + new Issue( + self::TypeInvalidYieldFrom, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Yield from statement was passed an invalid expression {CODE} of type {TYPE} (expected Traversable/array)", + self::REMEDIATION_B, + 10069 + ), + new Issue( + self::PartialTypeMismatchArgument, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE} ({TYPE} is incompatible) defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 10054 + ), + new Issue( + self::PartialTypeMismatchArgumentInternal, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE} ({TYPE} is incompatible)', + self::REMEDIATION_B, + 10055 + ), + new Issue( + self::PossiblyNullTypeArgument, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE} ({TYPE} is incompatible) defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 10056 + ), + new Issue( + self::PossiblyNullTypeArgumentInternal, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE} ({TYPE} is incompatible)', + self::REMEDIATION_B, + 10057 + ), + new Issue( + self::PossiblyFalseTypeArgument, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE} ({TYPE} is incompatible) defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 10058 + ), + new Issue( + self::PossiblyFalseTypeArgumentInternal, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {CODE} of type {TYPE} but {FUNCTIONLIKE} takes {TYPE} ({TYPE} is incompatible)', + self::REMEDIATION_B, + 10059 + ), + new Issue( + self::TypeMismatchReturn, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Returning {CODE} of type {TYPE} but {FUNCTIONLIKE} is declared to return {TYPE}", + self::REMEDIATION_B, + 10005 + ), + new Issue( + self::TypeMismatchReturnNullable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Returning {CODE} of type {TYPE} but {FUNCTIONLIKE} is declared to return {TYPE} (expected returned value to be non-nullable)", + self::REMEDIATION_B, + 10107 + ), + new Issue( + self::TypeMismatchReturnReal, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Returning {CODE} of type {TYPE}{DETAILS} but {FUNCTIONLIKE} is declared to return {TYPE}{DETAILS}", + self::REMEDIATION_B, + 10138 + ), + new Issue( + self::TypeMismatchReturnProbablyReal, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Returning {CODE} of type {TYPE}{DETAILS} but {FUNCTIONLIKE} is declared to return {TYPE}{DETAILS} (the inferred real return type has nothing in common with the declared phpdoc return type)", + self::REMEDIATION_B, + 10167 + ), + new Issue( + self::PartialTypeMismatchReturn, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Returning {CODE} of type {TYPE} but {FUNCTIONLIKE} is declared to return {TYPE} ({TYPE} is incompatible)", + self::REMEDIATION_B, + 10060 + ), + new Issue( + self::PossiblyNullTypeReturn, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Returning {CODE} of type {TYPE} but {FUNCTIONLIKE} is declared to return {TYPE} ({TYPE} is incompatible)", + self::REMEDIATION_B, + 10061 + ), + new Issue( + self::PossiblyFalseTypeReturn, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Returning {CODE} of type {TYPE} but {FUNCTIONLIKE} is declared to return {TYPE} ({TYPE} is incompatible)", + self::REMEDIATION_B, + 10062 + ), + new Issue( + self::TypeMismatchDeclaredReturn, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Doc-block of {METHOD} contains declared return type {TYPE} which is incompatible with the return type {TYPE} declared in the signature", + self::REMEDIATION_B, + 10020 + ), + new Issue( + self::TypeMismatchDeclaredReturnNullable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Doc-block of {METHOD} has declared return type {TYPE} which is not a permitted replacement of the nullable return type {TYPE} declared in the signature ('?T' should be documented as 'T|null' or '?T')", + self::REMEDIATION_B, + 10028 + ), + new Issue( + self::TypeMismatchDeclaredParam, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Doc-block of \${PARAMETER} in {METHOD} contains phpdoc param type {TYPE} which is incompatible with the param type {TYPE} declared in the signature", + self::REMEDIATION_B, + 10022 + ), + new Issue( + self::TypeMismatchDeclaredParamNullable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Doc-block of \${PARAMETER} in {METHOD} is phpdoc param type {TYPE} which is not a permitted replacement of the nullable param type {TYPE} declared in the signature ('?T' should be documented as 'T|null' or '?T')", + self::REMEDIATION_B, + 10027 + ), + new Issue( + self::TypeMissingReturn, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Method {METHOD} is declared to return {TYPE} in phpdoc but has no return value", + self::REMEDIATION_B, + 10006 + ), + new Issue( + self::TypeMissingReturnReal, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Method {METHOD} is declared to return {TYPE} in its real type signature but has no return value", + self::REMEDIATION_B, + 10157 + ), + new Issue( + self::TypeMismatchForeach, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "{TYPE} passed to foreach instead of array", + self::REMEDIATION_B, + 10007 + ), + new Issue( + self::TypeArrayOperator, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid array operand provided to operator '{OPERATOR}' between types {TYPE} and {TYPE}", + self::REMEDIATION_B, + 10008 + ), + new Issue( + self::TypeArraySuspicious, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Suspicious array access to {CODE} of type {TYPE}", + self::REMEDIATION_B, + 10009 + ), + new Issue( + self::TypeArrayUnsetSuspicious, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Suspicious attempt to unset an offset of a value {CODE} of type {TYPE}", + self::REMEDIATION_B, + 10048 + ), + new Issue( + self::TypeComparisonToArray, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "{TYPE} to array comparison", + self::REMEDIATION_B, + 10010 + ), + new Issue( + self::TypeComparisonFromArray, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "array to {TYPE} comparison", + self::REMEDIATION_B, + 10011 + ), + new Issue( + self::TypeConversionFromArray, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "array to {TYPE} conversion", + self::REMEDIATION_B, + 10012 + ), + new Issue( + self::TypeInstantiateAbstract, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Instantiation of abstract class {CLASS}", + self::REMEDIATION_B, + 10013 + ), + new Issue( + self::TypeInstantiateAbstractStatic, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Potential instantiation of abstract class {CLASS} (not an issue if this method is only called from a non-abstract subclass)", + self::REMEDIATION_B, + 10111 + ), + new Issue( + self::TypeInstantiateInterface, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Instantiation of interface {INTERFACE}", + self::REMEDIATION_B, + 10014 + ), + new Issue( + self::TypeInstantiateTrait, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Instantiation of trait {TRAIT}", + self::REMEDIATION_B, + 10074 + ), + new Issue( + self::TypeInstantiateTraitStaticOrSelf, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Potential instantiation of trait {TRAIT} (not an issue if this method is only called from a non-abstract class using the trait)", + self::REMEDIATION_B, + 10112 + ), + new Issue( + self::TypeInvalidClosureScope, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid @phan-closure-scope: expected a class name, got {TYPE}", + self::REMEDIATION_B, + 10024 + ), + new Issue( + self::TypeInvalidRightOperand, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: left operand is array and right is not", + self::REMEDIATION_B, + 10015 + ), + new Issue( + self::TypeInvalidLeftOperand, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: right operand is array and left is not", + self::REMEDIATION_B, + 10016 + ), + new Issue( + self::TypeInvalidRightOperandOfAdd, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: right operand of {OPERATOR} is {TYPE} (expected array or number)", + self::REMEDIATION_B, + 10070 + ), + new Issue( + self::TypeInvalidLeftOperandOfAdd, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: left operand of {OPERATOR} is {TYPE} (expected array or number)", + self::REMEDIATION_B, + 10071 + ), + new Issue( + self::TypeInvalidRightOperandOfNumericOp, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: right operand of {OPERATOR} is {TYPE} (expected number)", + self::REMEDIATION_B, + 10072 + ), + new Issue( + self::TypeInvalidLeftOperandOfNumericOp, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: left operand of {OPERATOR} is {TYPE} (expected number)", + self::REMEDIATION_B, + 10073 + ), + new Issue( + self::TypeInvalidRightOperandOfIntegerOp, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: right operand of {OPERATOR} is {TYPE} (expected int)", + self::REMEDIATION_B, + 10100 + ), + new Issue( + self::TypeInvalidLeftOperandOfIntegerOp, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: left operand of {OPERATOR} is {TYPE} (expected int)", + self::REMEDIATION_B, + 10101 + ), + new Issue( + self::TypeInvalidRightOperandOfBitwiseOp, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: right operand of {OPERATOR} is {TYPE} (expected int|string)", + self::REMEDIATION_B, + 10163 + ), + new Issue( + self::TypeInvalidLeftOperandOfBitwiseOp, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: left operand of {OPERATOR} is {TYPE} (expected int|string)", + self::REMEDIATION_B, + 10164 + ), + new Issue( + self::TypeInvalidUnaryOperandNumeric, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: unary operand of {STRING_LITERAL} is {TYPE} (expected number)", + self::REMEDIATION_B, + 10075 + ), + new Issue( + self::TypeInvalidUnaryOperandBitwiseNot, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: unary operand of {STRING_LITERAL} is {TYPE} (expected number that can fit in an int, or string)", + self::REMEDIATION_B, + 10076 + ), + new Issue( + self::TypeParentConstructorCalled, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Must call parent::__construct() from {CLASS} which extends {CLASS}", + self::REMEDIATION_B, + 10017 + ), + new Issue( + self::TypeNonVarPassByRef, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Only variables can be passed by reference at argument {INDEX} of {FUNCTIONLIKE}", + self::REMEDIATION_B, + 10018 + ), + new Issue( + self::TypeNonVarReturnByRef, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Only variables can be returned by reference in {FUNCTIONLIKE}", + self::REMEDIATION_B, + 10144 + ), + new Issue( + self::NonClassMethodCall, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Call to method {METHOD} on non-class type {TYPE}", + self::REMEDIATION_B, + 10019 + ), + new Issue( + self::TypeVoidAssignment, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Cannot assign void return value", + self::REMEDIATION_B, + 10000 + ), + new Issue( + self::TypeVoidArgument, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Cannot use void return value {CODE} as a function argument", + self::REMEDIATION_B, + 10161 + ), + new Issue( + self::TypeVoidExpression, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious use of void return value {CODE} where a value is expected", + self::REMEDIATION_B, + 10162 + ), + new Issue( + self::TypeSuspiciousIndirectVariable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Indirect variable ${(expr)} has invalid inner expression type {TYPE}, expected string/integer', + self::REMEDIATION_B, + 10025 + ), + new Issue( + self::TypeMagicVoidWithReturn, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + 'Found a return statement with a value in the implementation of the magic method {METHOD}, expected void return type', + self::REMEDIATION_B, + 10026 + ), + new Issue( + self::TypeInvalidInstanceof, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Found an instanceof class name {CODE} of type {TYPE}, but class name must be a valid object or a string', + self::REMEDIATION_B, + 10029 + ), + new Issue( + self::TypeMismatchDimAssignment, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'When appending to a value of type {TYPE}, found an array access index of type {TYPE}, but expected the index to be of type {TYPE}', + self::REMEDIATION_B, + 10030 + ), + new Issue( + self::TypeMismatchDimEmpty, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + 'Assigning to an empty array index of a value of type {TYPE}, but expected the index to exist and be of type {TYPE}', + self::REMEDIATION_B, + 10031 + ), + new Issue( + self::TypeMismatchDimFetch, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'When fetching an array index from a value of type {TYPE}, found an array index of type {TYPE}, but expected the index to be of type {TYPE}', + self::REMEDIATION_B, + 10032 + ), + new Issue( + self::TypeMismatchDimFetchNullable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'When fetching an array index from a value of type {TYPE}, found an array index of type {TYPE}, but expected the index to be of the non-nullable type {TYPE}', + self::REMEDIATION_B, + 10044 + ), + new Issue( + self::TypeInvalidCallableArraySize, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'In a place where phan was expecting a callable, saw an array of size {COUNT}, but callable arrays must be of size 2', + self::REMEDIATION_B, + 10033 + ), + new Issue( + self::TypeInvalidCallableArrayKey, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'In a place where phan was expecting a callable, saw an array with an unexpected key for element #{INDEX} (expected [$class_or_expr, $method_name])', + self::REMEDIATION_B, + 10034 + ), + new Issue( + self::TypeInvalidCallableObjectOfMethod, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'In a place where phan was expecting a callable, saw a two-element array with a class or expression with an unexpected type {TYPE} (expected a class type or string). Method name was {METHOD}', + self::REMEDIATION_B, + 10035 + ), + new Issue( + self::TypeExpectedObject, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Expected an object instance but saw expression {CODE} with type {TYPE}', + self::REMEDIATION_B, + 10036 + ), + new Issue( + self::TypeExpectedObjectOrClassName, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Expected an object instance or the name of a class but saw expression {CODE} with type {TYPE}', + self::REMEDIATION_B, + 10037 + ), + new Issue( + self::TypeExpectedObjectPropAccess, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + 'Expected an object instance when accessing an instance property, but saw an expression {CODE} with type {TYPE}', + self::REMEDIATION_B, + 10038 + ), + new Issue( + self::TypeExpectedObjectStaticPropAccess, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Expected an object instance or a class name when accessing a static property, but saw an expression {CODE} with type {TYPE}', + self::REMEDIATION_B, + 10039 + ), + new Issue( + self::TypeExpectedObjectPropAccessButGotNull, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Expected an object instance when accessing an instance property, but saw an expression {CODE} with type {TYPE}', + self::REMEDIATION_B, + 10040 + ), + new Issue( + self::TypeMismatchUnpackKey, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'When unpacking a value of type {TYPE}, the value\'s keys were of type {TYPE}, but the keys should be consecutive integers starting from 0', + self::REMEDIATION_B, + 10041 + ), + new Issue( + self::TypeMismatchUnpackKeyArraySpread, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'When unpacking a value of type {TYPE}, the value\'s keys were of type {TYPE}, but the keys should be integers', + self::REMEDIATION_B, + 10109 + ), + new Issue( + self::TypeMismatchUnpackValue, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Attempting to unpack a value of type {TYPE} which does not contain any subtypes of iterable (such as array or Traversable)', + self::REMEDIATION_B, + 10042 + ), + new Issue( + self::TypeMismatchArrayDestructuringKey, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Attempting an array destructing assignment with a key of type {TYPE} but the only key types of the right-hand side are of type {TYPE}', + self::REMEDIATION_B, + 10043 + ), + new Issue( + self::TypeArraySuspiciousNullable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Suspicious array access to {CODE} of nullable type {TYPE}", + self::REMEDIATION_B, + 10045 + ), + new Issue( + self::TypeArraySuspiciousNull, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Suspicious array access to {CODE} of type null", + self::REMEDIATION_B, + 10136 + ), + new Issue( + self::TypeInvalidDimOffset, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid offset {SCALAR} of {CODE} of array type {TYPE}", + self::REMEDIATION_B, + 10046 + ), + new Issue( + self::TypePossiblyInvalidDimOffset, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Possibly invalid offset {SCALAR} of {CODE} of array type {TYPE}", + self::REMEDIATION_B, + 10154 + ), + new Issue( + self::TypeInvalidDimOffsetArrayDestructuring, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid offset {SCALAR} of {CODE} of array type {TYPE} in an array destructuring assignment", + self::REMEDIATION_B, + 10047 + ), + new Issue( + self::TypeInvalidCallExpressionAssignment, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Probably unused assignment to function result {CODE} for function returning {TYPE}", + self::REMEDIATION_B, + 10153 + ), + new Issue( + self::TypeInvalidExpressionArrayDestructuring, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid value {CODE} of type {TYPE} in an array destructuring assignment, expected {TYPE}", + self::REMEDIATION_B, + 10077 + ), + new Issue( + self::TypeSuspiciousEcho, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Suspicious argument {CODE} of type {TYPE} for an echo/print statement", + self::REMEDIATION_B, + 10049 + ), + + new Issue( + self::TypeInvalidThrowsNonObject, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "@throws annotation of {FUNCTIONLIKE} has invalid non-object type {TYPE}, expected a class", + self::REMEDIATION_B, + 10050 + ), + new Issue( + self::TypeInvalidThrowsIsTrait, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "@throws annotation of {FUNCTIONLIKE} has invalid trait type {TYPE}, expected a class", + self::REMEDIATION_B, + 10051 + ), + new Issue( + self::TypeInvalidThrowsIsInterface, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "@throws annotation of {FUNCTIONLIKE} has suspicious interface type {TYPE} for an @throws annotation, expected class (PHP allows interfaces to be caught, so this might be intentional)", + self::REMEDIATION_B, + 10052 + ), + new Issue( + self::TypeInvalidThrowsNonThrowable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "@throws annotation of {FUNCTIONLIKE} has suspicious class type {TYPE}, which does not extend Error/Exception", + self::REMEDIATION_B, + 10053 + ), + new Issue( + self::TypeInvalidThrowStatementNonThrowable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "{FUNCTIONLIKE} can throw {CODE} of type {TYPE} here which can't cast to {TYPE}", + self::REMEDIATION_B, + 10158 + ), + new Issue( + self::TypeSuspiciousStringExpression, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Suspicious type {TYPE} of a variable or expression {CODE} used to build a string. (Expected type to be able to cast to a string)", + self::REMEDIATION_B, + 10066 + ), + new Issue( + self::TypeInvalidMethodName, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Instance method name must be a string, got {TYPE}", + self::REMEDIATION_B, + 10078 + ), + new Issue( + self::TypeInvalidStaticMethodName, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Static method name must be a string, got {TYPE}", + self::REMEDIATION_B, + 10079 + ), + new Issue( + self::TypeInvalidCallableMethodName, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Method name of callable must be a string, got {TYPE}", + self::REMEDIATION_B, + 10080 + ), + new Issue( + self::TypeObjectUnsetDeclaredProperty, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, // There are valid reasons to do this, e.g. for the typed properties V2 RFC or to change serialization + "Suspicious attempt to unset class {TYPE}'s property {PROPERTY} declared at {FILE}:{LINE} (This can be done, but is more commonly done for dynamic properties and Phan does not expect this)", + self::REMEDIATION_B, + 10081 + ), + new Issue( + self::TypeNoAccessiblePropertiesForeach, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Class {TYPE} was passed to foreach, but it does not extend Traversable and none of its declared properties are accessible from this context. (This check excludes dynamic properties)", + self::REMEDIATION_B, + 10082 + ), + new Issue( + self::TypeNoPropertiesForeach, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Class {TYPE} was passed to foreach, but it does not extend Traversable and doesn't have any declared properties. (This check excludes dynamic properties)", + self::REMEDIATION_B, + 10083 + ), + new Issue( + self::TypeSuspiciousNonTraversableForeach, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Class {TYPE} was passed to foreach, but it does not extend Traversable. This may be intentional, because some of that class's declared properties are accessible from this context. (This check excludes dynamic properties)", + self::REMEDIATION_B, + 10084 + ), + new Issue( + self::TypeInvalidRequire, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Require statement was passed an invalid expression of type {TYPE} (expected a string)", + self::REMEDIATION_B, + 10085 + ), + new Issue( + self::TypeInvalidEval, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Eval statement was passed an invalid expression of type {TYPE} (expected a string)", + self::REMEDIATION_B, + 10086 + ), + new Issue( + self::RelativePathUsed, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "{FUNCTION}() statement was passed a relative path {STRING_LITERAL} instead of an absolute path", + self::REMEDIATION_B, + 10087 + ), + new Issue( + self::TypeInvalidCloneNotObject, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Expected an object to be passed to clone() but got {TYPE}", + self::REMEDIATION_B, + 10088 + ), + new Issue( + self::TypePossiblyInvalidCloneNotObject, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Expected an object to be passed to clone() but got possible non-object {TYPE}", + self::REMEDIATION_B, + 10143 + ), + new Issue( + self::TypeInvalidTraitReturn, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Expected a class or interface (or built-in type) to be the real return type of {FUNCTIONLIKE} but got trait {TRAIT}", + self::REMEDIATION_B, + 10089 + ), + new Issue( + self::TypeInvalidTraitParam, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "{FUNCTIONLIKE} is declared to have a parameter \${PARAMETER} with a real type of trait {TYPE} (expected a class or interface or built-in type)", + self::REMEDIATION_B, + 10090 + ), + // TODO: Deprecate and remove this issue? + new Issue( + self::TypeInvalidBitwiseBinaryOperator, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid non-int/non-string operand provided to operator '{OPERATOR}' between types {TYPE} and {TYPE}", + self::REMEDIATION_B, + 10091 + ), + new Issue( + self::TypeMismatchBitwiseBinaryOperands, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Unexpected mix of int and string operands provided to operator '{OPERATOR}' between types {TYPE} and {TYPE} (expected one type but not both)", + self::REMEDIATION_B, + 10092 + ), + new Issue( + self::InfiniteRecursion, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "{FUNCTIONLIKE} is calling itself in a way that may cause infinite recursion.", + self::REMEDIATION_B, + 10093 + ), + new Issue( + self::PossibleInfiniteRecursionSameParams, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "{FUNCTIONLIKE} is calling itself with the same parameters it was called with. This may cause infinite recursion (Phan does not check for changes to global or shared state).", + self::REMEDIATION_B, + 10149 + ), + new Issue( + self::PossiblyNonClassMethodCall, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Call to method {METHOD} on type {TYPE} that could be a non-object", + self::REMEDIATION_B, + 10094 + ), + new Issue( + self::TypeInvalidCallable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Saw type {TYPE} which cannot be a callable', + self::REMEDIATION_B, + 10095 + ), + new Issue( + self::TypePossiblyInvalidCallable, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Saw type {TYPE} which is possibly not a callable', + self::REMEDIATION_B, + 10096 + ), + new Issue( + self::TypeComparisonToInvalidClass, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Saw code asserting that an expression has a class, but that class is an invalid/impossible FQSEN {STRING_LITERAL}', + self::REMEDIATION_B, + 10097 + ), + new Issue( + self::TypeComparisonToInvalidClassType, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Saw code asserting that an expression has a class, but saw an invalid/impossible union type {TYPE} (expected {TYPE})', + self::REMEDIATION_B, + 10098 + ), + new Issue( + self::TypeInvalidUnaryOperandIncOrDec, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Invalid operator: unary operand of {STRING_LITERAL} is {TYPE} (expected int or string or float)", + self::REMEDIATION_B, + 10099 + ), + new Issue( + self::TypeInvalidPropertyName, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, // Not a runtime Error for an instance property + "Saw a dynamic usage of an instance property with a name of type {TYPE} but expected the name to be a string", + self::REMEDIATION_B, + 10102 + ), + new Issue( + self::TypeInvalidStaticPropertyName, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, // Likely to be an Error for a static property + "Saw a dynamic usage of a static property with a name of type {TYPE} but expected the name to be a string", + self::REMEDIATION_B, + 10103 + ), + new Issue( + self::TypeErrorInInternalCall, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Saw a call to an internal function {FUNCTION}() with what would be invalid arguments in strict mode, when trying to infer the return value literal type: {DETAILS}", + self::REMEDIATION_B, + 10104 + ), + new Issue( + self::TypeErrorInOperation, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Saw an error when attempting to infer the type of expression {CODE}: {DETAILS}", + self::REMEDIATION_B, + 10110 + ), + new Issue( + self::TypeMismatchPropertyDefaultReal, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Default value for {TYPE} \${PROPERTY} can't be {CODE} of type {TYPE}", + self::REMEDIATION_B, + 10108 + ), + new Issue( + self::TypeMismatchPropertyDefault, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Default value for {TYPE} \${PROPERTY} can't be {CODE} of type {TYPE} based on phpdoc types", + self::REMEDIATION_B, + 10159 + ), + new Issue( + self::TypeMismatchPropertyReal, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "Assigning {CODE} of type {TYPE}{DETAILS} to property but {PROPERTY} is {TYPE}{DETAILS}", + self::REMEDIATION_B, + 10137 + ), + new Issue( + self::TypeMismatchPropertyProbablyReal, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "Assigning {CODE} of type {TYPE}{DETAILS} to property but {PROPERTY} is {TYPE}{DETAILS} (the inferred real assigned type has nothing in common with the declared phpdoc property type)", + self::REMEDIATION_B, + 10168 + ), + new Issue( + self::TypeMismatchPropertyRealByRef, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + "{CODE} of type {TYPE} may end up assigned to property {PROPERTY} of type {TYPE} by reference at {FILE}:{LINE}", + self::REMEDIATION_B, + 10150 + ), + new Issue( + self::TypeMismatchPropertyByRef, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "{CODE} of type {TYPE} may end up assigned to property {PROPERTY} of type {TYPE} by reference at {FILE}:{LINE}", + self::REMEDIATION_B, + 10151 + ), + new Issue( + self::ImpossibleCondition, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Impossible attempt to cast {CODE} of type {TYPE} to {TYPE}", + self::REMEDIATION_B, + 10113 + ), + new Issue( + self::ImpossibleConditionInLoop, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Impossible attempt to cast {CODE} of type {TYPE} to {TYPE} in a loop body (may be a false positive)", + self::REMEDIATION_B, + 10118 + ), + new Issue( + self::ImpossibleConditionInGlobalScope, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Impossible attempt to cast {CODE} of type {TYPE} to {TYPE} in the global scope (may be a false positive)", + self::REMEDIATION_B, + 10123 + ), + new Issue( + self::RedundantCondition, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Redundant attempt to cast {CODE} of type {TYPE} to {TYPE}", + self::REMEDIATION_B, + 10114 + ), + new Issue( + self::RedundantConditionInLoop, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Redundant attempt to cast {CODE} of type {TYPE} to {TYPE} in a loop body (likely a false positive)", + self::REMEDIATION_B, + 10119 + ), + new Issue( + self::RedundantConditionInGlobalScope, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Redundant attempt to cast {CODE} of type {TYPE} to {TYPE} in the global scope (likely a false positive)", + self::REMEDIATION_B, + 10124 + ), + new Issue( + self::InfiniteLoop, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "The loop condition {CODE} of type {TYPE} is always {TYPE} and nothing seems to exit the loop", + self::REMEDIATION_B, + 10135 + ), + new Issue( + self::PossiblyInfiniteLoop, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + "The loop condition {CODE} does not seem to change within the loop and nothing seems to exit the loop", + self::REMEDIATION_B, + 10169 + ), + new Issue( + self::ImpossibleTypeComparison, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Impossible attempt to check if {CODE} of type {TYPE} is identical to {CODE} of type {TYPE}", + self::REMEDIATION_B, + 10115 + ), + new Issue( + self::ImpossibleTypeComparisonInLoop, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Impossible attempt to check if {CODE} of type {TYPE} is identical to {CODE} of type {TYPE} in a loop body (likely a false positive)", + self::REMEDIATION_B, + 10120 + ), + new Issue( + self::ImpossibleTypeComparisonInGlobalScope, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Impossible attempt to check if {CODE} of type {TYPE} is identical to {CODE} of type {TYPE} in the global scope (likely a false positive)", + self::REMEDIATION_B, + 10125 + ), + new Issue( + self::SuspiciousValueComparison, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious attempt to compare {CODE} of type {TYPE} to {CODE} of type {TYPE} with operator '{OPERATOR}'", + self::REMEDIATION_B, + 10131 + ), + new Issue( + self::SuspiciousValueComparisonInLoop, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious attempt to compare {CODE} of type {TYPE} to {CODE} of type {TYPE} with operator '{OPERATOR}' in a loop (likely a false positive)", + self::REMEDIATION_B, + 10132 + ), + new Issue( + self::SuspiciousValueComparisonInGlobalScope, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious attempt to compare {CODE} of type {TYPE} to {CODE} of type {TYPE} with operator '{OPERATOR}' in the global scope (likely a false positive)", + self::REMEDIATION_B, + 10133 + ), + new Issue( + self::SuspiciousLoopDirection, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious loop appears to {DETAILS} after each iteration in {CODE}, but the loop condition is {CODE}", + self::REMEDIATION_B, + 10134 + ), + new Issue( + self::SuspiciousWeakTypeComparison, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious attempt to compare {CODE} of type {TYPE} to {CODE} of type {TYPE}", + self::REMEDIATION_B, + 10128 + ), + new Issue( + self::SuspiciousWeakTypeComparisonInLoop, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious attempt to compare {CODE} of type {TYPE} to {CODE} of type {TYPE} in a loop body (likely a false positive)", + self::REMEDIATION_B, + 10129 + ), + new Issue( + self::SuspiciousWeakTypeComparisonInGlobalScope, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious attempt to compare {CODE} of type {TYPE} to {CODE} of type {TYPE} in the global scope (likely a false positive)", + self::REMEDIATION_B, + 10130 + ), + new Issue( + self::SuspiciousTruthyCondition, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious attempt to check if {CODE} of type {TYPE} is truthy/falsey. This contains both objects/arrays and scalars", + self::REMEDIATION_B, + 10155 + ), + new Issue( + self::SuspiciousTruthyString, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Suspicious attempt to check if {CODE} of type {TYPE} is truthy/falsey. This is false both for '' and '0'", + self::REMEDIATION_B, + 10156 + ), + new Issue( + self::CoalescingNeverNull, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Using non-null {CODE} of type {TYPE} as the left hand side of a null coalescing (??) operation. The right hand side may be unnecessary.", + self::REMEDIATION_B, + 10116 + ), + new Issue( + self::CoalescingNeverNullInLoop, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Using non-null {CODE} of type {TYPE} as the left hand side of a null coalescing (??) operation. The right hand side may be unnecessary. (in a loop body - this is likely a false positive)", + self::REMEDIATION_B, + 10121 + ), + new Issue( + self::CoalescingNeverNullInGlobalScope, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Using non-null {CODE} of type {TYPE} as the left hand side of a null coalescing (??) operation. The right hand side may be unnecessary. (in the global scope - this is likely a false positive)", + self::REMEDIATION_B, + 10126 + ), + new Issue( + self::CoalescingAlwaysNull, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Using {CODE} of type {TYPE} as the left hand side of a null coalescing (??) operation. The left hand side may be unnecessary.", + self::REMEDIATION_B, + 10117 + ), + new Issue( + self::CoalescingAlwaysNullInLoop, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Using {CODE} of type {TYPE} as the left hand side of a null coalescing (??) operation. The left hand side may be unnecessary. (in a loop body - this is likely a false positive)", + self::REMEDIATION_B, + 10122 + ), + new Issue( + self::CoalescingAlwaysNullInGlobalScope, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + "Using {CODE} of type {TYPE} as the left hand side of a null coalescing (??) operation. The left hand side may be unnecessary. (in the global scope - this is likely a false positive)", + self::REMEDIATION_B, + 10127 + ), + new Issue( + self::CoalescingNeverUndefined, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + 'Using {CODE} ?? null seems unnecessary - the expression appears to always be defined', + self::REMEDIATION_B, + 10160 + ), + new Issue( + self::DivisionByZero, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Saw {CODE} with a divisor of type {TYPE}', + self::REMEDIATION_B, + 10145 + ), + new Issue( + self::ModuloByZero, + self::CATEGORY_TYPE, + self::SEVERITY_NORMAL, + 'Saw {CODE} with modulus of type {TYPE}', + self::REMEDIATION_B, + 10146 + ), + new Issue( + self::PowerOfZero, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + 'Saw {CODE} exponentiating to a power of type {TYPE} (the result will always be 1)', + self::REMEDIATION_B, + 10147 + ), + new Issue( + self::InvalidMixin, + self::CATEGORY_TYPE, + self::SEVERITY_LOW, + 'Attempting to use a mixin of invalid or missing type {TYPE}', + self::REMEDIATION_B, + 10152 + ), + new Issue( + self::IncompatibleRealPropertyType, + self::CATEGORY_TYPE, + self::SEVERITY_CRITICAL, + 'Declaration of {PROPERTY} of real type {TYPE} is incompatible with inherited property {PROPERTY} of real type {TYPE} defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 10165 + ), + // Issue::CATEGORY_VARIABLE + new Issue( + self::VariableUseClause, + self::CATEGORY_VARIABLE, + self::SEVERITY_NORMAL, + "Non-variables ({CODE}) not allowed within use clause", + self::REMEDIATION_B, + 12000 + ), + + // Issue::CATEGORY_STATIC + new Issue( + self::StaticCallToNonStatic, + self::CATEGORY_STATIC, + self::SEVERITY_CRITICAL, + "Static call to non-static method {METHOD} defined at {FILE}:{LINE}. This is an Error in PHP 8.0+.", + self::REMEDIATION_B, + 9000 + ), + new Issue( + self::StaticPropIsStaticType, + self::CATEGORY_STATIC, + self::SEVERITY_LOW, + "Static property {PROPERTY} is declared to have type {TYPE}, but the only instance is shared among all subclasses (Did you mean {TYPE})", + self::REMEDIATION_A, + 9001 + ), + new Issue( + self::AbstractStaticMethodCall, + self::CATEGORY_STATIC, + self::SEVERITY_CRITICAL, + "Potentially calling an abstract static method {METHOD} in {CODE}", + self::REMEDIATION_B, + 9002 + ), + new Issue( + self::AbstractStaticMethodCallInStatic, + self::CATEGORY_STATIC, + self::SEVERITY_CRITICAL, + "Potentially calling an abstract static method {METHOD} with static:: in {CODE} (the calling static method's class scope may be an abstract class)", + self::REMEDIATION_B, + 9003 + ), + new Issue( + self::AbstractStaticMethodCallInTrait, + self::CATEGORY_STATIC, + self::SEVERITY_CRITICAL, + "Potentially calling an abstract static method {METHOD} on a trait in {CODE}, if the caller's method is called on the trait instead of a concrete class using the trait", + self::REMEDIATION_B, + 9004 + ), + + // Issue::CATEGORY_CONTEXT + new Issue( + self::ContextNotObject, + self::CATEGORY_CONTEXT, + self::SEVERITY_CRITICAL, + "Cannot access {CLASS} when not in object context", + self::REMEDIATION_B, + 4000 + ), + new Issue( + self::ContextNotObjectInCallable, + self::CATEGORY_CONTEXT, + self::SEVERITY_NORMAL, + "Cannot access {CLASS} when not in object context, but code is using callable {METHOD}", + self::REMEDIATION_B, + 4001 + ), + new Issue( + self::ContextNotObjectUsingSelf, + self::CATEGORY_CONTEXT, + self::SEVERITY_NORMAL, + 'Cannot use {CLASS} as type when not in object context in {FUNCTION}', + self::REMEDIATION_B, + 4002 + ), + new Issue( + self::SuspiciousMagicConstant, + self::CATEGORY_CONTEXT, + self::SEVERITY_NORMAL, + 'Suspicious reference to magic constant {CODE}: {DETAILS}', + self::REMEDIATION_B, + 4003 + ), + + // Issue::CATEGORY_DEPRECATED + new Issue( + self::DeprecatedFunction, + self::CATEGORY_DEPRECATED, + self::SEVERITY_NORMAL, + "Call to deprecated function {FUNCTIONLIKE} defined at {FILE}:{LINE}{DETAILS}", + self::REMEDIATION_B, + 5000 + ), + new Issue( + self::DeprecatedFunctionInternal, + self::CATEGORY_DEPRECATED, + self::SEVERITY_NORMAL, + "Call to deprecated function {FUNCTIONLIKE}{DETAILS}", + self::REMEDIATION_B, + 5005 + ), + new Issue( + self::DeprecatedClass, + self::CATEGORY_DEPRECATED, + self::SEVERITY_NORMAL, + "Using a deprecated class {CLASS} defined at {FILE}:{LINE}{DETAILS}", + self::REMEDIATION_B, + 5001 + ), + new Issue( + self::DeprecatedProperty, + self::CATEGORY_DEPRECATED, + self::SEVERITY_NORMAL, + "Reference to deprecated property {PROPERTY} defined at {FILE}:{LINE}{DETAILS}", + self::REMEDIATION_B, + 5002 + ), + new Issue( + self::DeprecatedClassConstant, + self::CATEGORY_DEPRECATED, + self::SEVERITY_NORMAL, + "Reference to deprecated class constant {CONST} defined at {FILE}:{LINE}{DETAILS}", + self::REMEDIATION_B, + 5007 + ), + new Issue( + self::DeprecatedInterface, + self::CATEGORY_DEPRECATED, + self::SEVERITY_NORMAL, + "Using a deprecated interface {INTERFACE} defined at {FILE}:{LINE}{DETAILS}", + self::REMEDIATION_B, + 5003 + ), + new Issue( + self::DeprecatedTrait, + self::CATEGORY_DEPRECATED, + self::SEVERITY_NORMAL, + "Using a deprecated trait {TRAIT} defined at {FILE}:{LINE}{DETAILS}", + self::REMEDIATION_B, + 5004 + ), + new Issue( + self::DeprecatedCaseInsensitiveDefine, + self::CATEGORY_DEPRECATED, + self::SEVERITY_NORMAL, + "Creating case-insensitive constants with define() has been deprecated in PHP 7.3", + self::REMEDIATION_B, + 5006 + ), + + // Issue::CATEGORY_PARAMETER + new Issue( + self::ParamReqAfterOpt, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + 'Required parameter {PARAMETER} follows optional {PARAMETER}', + self::REMEDIATION_B, + 7000 + ), + new Issue( + self::ParamTooMany, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Call with {COUNT} arg(s) to {FUNCTIONLIKE} which only takes {COUNT} arg(s) defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 7001 + ), + new Issue( + self::ParamTooManyInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + "Call with {COUNT} arg(s) to {FUNCTIONLIKE} which only takes {COUNT} arg(s). This is an ArgumentCountError for internal functions in PHP 8.0+.", + self::REMEDIATION_B, + 7002 + ), + new Issue( + self::ParamTooManyCallable, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Call with {COUNT} arg(s) to {FUNCTIONLIKE} (As a provided callable) which only takes {COUNT} arg(s) defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 7043 + ), + new Issue( + self::ParamTooFew, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + "Call with {COUNT} arg(s) to {FUNCTIONLIKE} which requires {COUNT} arg(s) defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 7003 + ), + new Issue( + self::ParamTooFewInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + "Call with {COUNT} arg(s) to {FUNCTIONLIKE} which requires {COUNT} arg(s)", + self::REMEDIATION_B, + 7004 + ), + new Issue( + self::ParamTooFewCallable, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + "Call with {COUNT} arg(s) to {FUNCTIONLIKE} (as a provided callable) which requires {COUNT} arg(s) defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 7044 + ), + new Issue( + self::ParamTooFewInPHPDoc, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + 'Call with {COUNT} arg(s) to {FUNCTIONLIKE} which has phpdoc indicating it requires {COUNT} arg(s) (${PARAMETER} is mandatory) defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 7049 + ), + new Issue( + self::ParamSpecial1, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {TYPE} but {FUNCTIONLIKE} takes {TYPE} when argument {INDEX} is {TYPE}', + self::REMEDIATION_B, + 7005 + ), + new Issue( + self::ParamSpecial2, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + 'Argument {INDEX} (${PARAMETER}) is {TYPE} but {FUNCTIONLIKE} takes {TYPE} when passed only one argument', + self::REMEDIATION_B, + 7006 + ), + new Issue( + self::ParamSpecial3, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "The last argument to {FUNCTIONLIKE} must be of type {TYPE}", + self::REMEDIATION_B, + 7007 + ), + new Issue( + self::ParamSpecial4, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "The second to last argument to {FUNCTIONLIKE} must be of type {TYPE}", + self::REMEDIATION_B, + 7008 + ), + new Issue( + self::ParamTypeMismatch, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Argument {INDEX} is {TYPE} but {FUNCTIONLIKE} takes {TYPE}", + self::REMEDIATION_B, + 7009 + ), + new Issue( + self::ParamSignatureMismatch, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with {METHOD} defined in {FILE}:{LINE}{DETAILS}", + self::REMEDIATION_B, + 7010 + ), + new Issue( + self::ParamSignatureMismatchInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD}{DETAILS}", + self::REMEDIATION_B, + 7011 + ), + new Issue( + self::ParamRedefined, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + "Redefinition of parameter {PARAMETER}", + self::REMEDIATION_B, + 7012 + ), + // TODO: Optionally, change the other message to say that it's based off of phpdoc and LSP in a future PR. + // NOTE: Incompatibilities in the param list are SEVERITY_NORMAL, because the php interpreter emits a notice. + // Incompatibilities in the return types are SEVERITY_CRITICAL, because the php interpreter will throw an Error. + new Issue( + self::ParamSignatureRealMismatchReturnType, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + "Declaration of {METHOD} should be compatible with {METHOD} (method returning '{TYPE}' cannot override method returning '{TYPE}') defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7013 + ), + new Issue( + self::ParamSignatureRealMismatchReturnTypeInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (method returning '{TYPE}' cannot override method returning '{TYPE}')", + self::REMEDIATION_B, + 7014 + ), + // NOTE: Incompatibilities in param types does not cause the php interpreter to throw an error. + // It emits a warning instead, so these are SEVERITY_NORMAL. + new Issue( + self::ParamSignaturePHPDocMismatchReturnType, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (method returning '{TYPE}' cannot override method returning '{TYPE}') defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7033 + ), + new Issue( + self::ParamSignatureRealMismatchParamType, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with {METHOD} (parameter #{INDEX} of type '{TYPE}' cannot replace original parameter of type '{TYPE}') defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7015 + ), + new Issue( + self::ParamSignatureRealMismatchParamTypeInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (parameter #{INDEX} of type '{TYPE}' cannot replace original parameter of type '{TYPE}')", + self::REMEDIATION_B, + 7016 + ), + new Issue( + self::ParamSignaturePHPDocMismatchParamType, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (parameter #{INDEX} of type '{TYPE}' cannot replace original parameter of type '{TYPE}') defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7034 + ), + new Issue( + self::ParamSignatureRealMismatchHasParamType, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + "Declaration of {METHOD} should be compatible with {METHOD} (parameter #{INDEX} has type '{TYPE}' which cannot replace original parameter with no type) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7017 + ), + new Issue( + self::ParamSignatureRealMismatchHasParamTypeInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (parameter #{INDEX} has type '{TYPE}' which cannot replace original parameter with no type)", + self::REMEDIATION_B, + 7018 + ), + new Issue( + self::ParamSignaturePHPDocMismatchHasParamType, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (parameter #{INDEX} has type '{TYPE}' which cannot replace original parameter with no type) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7035 + ), + new Issue( + self::ParamSignatureRealMismatchHasNoParamType, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, // NOTE: See allow_method_param_type_widening + "Declaration of {METHOD} should be compatible with {METHOD} (parameter #{INDEX} with no type cannot replace original parameter with type '{TYPE}') defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7019 + ), + new Issue( + self::ParamSignatureRealMismatchHasNoParamTypeInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (parameter #{INDEX} with no type cannot replace original parameter with type '{TYPE}')", + self::REMEDIATION_B, + 7020 + ), + new Issue( + self::ParamSignaturePHPDocMismatchHasNoParamType, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (parameter #{INDEX} with no type cannot replace original parameter with type '{TYPE}') defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7036 + ), + new Issue( + self::ParamSignatureRealMismatchParamVariadic, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with {METHOD} (parameter #{INDEX} is a variadic parameter replacing a non-variadic parameter) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7021 + ), + new Issue( + self::ParamSignatureRealMismatchParamVariadicInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (parameter #{INDEX} is a variadic parameter replacing a non-variadic parameter)", + self::REMEDIATION_B, + 7022 + ), + new Issue( + self::ParamSignaturePHPDocMismatchParamVariadic, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (parameter #{INDEX} is a variadic parameter replacing a non-variadic parameter) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7037 + ), + new Issue( + self::ParamSignatureRealMismatchParamNotVariadic, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with {METHOD} (parameter #{INDEX} is a non-variadic parameter replacing a variadic parameter) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7023 + ), + new Issue( + self::ParamSignatureRealMismatchParamNotVariadicInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (parameter #{INDEX} is a non-variadic parameter replacing a variadic parameter)", + self::REMEDIATION_B, + 7024 + ), + new Issue( + self::ParamSignaturePHPDocMismatchParamNotVariadic, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (parameter #{INDEX} is a non-variadic parameter replacing a variadic parameter) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7038 + ), + new Issue( + self::ParamSignatureRealMismatchParamIsReference, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with {METHOD} (parameter #{INDEX} is a reference parameter overriding a non-reference parameter) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7025 + ), + new Issue( + self::ParamSignatureRealMismatchParamIsReferenceInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (parameter #{INDEX} is a reference parameter overriding a non-reference parameter)", + self::REMEDIATION_B, + 7026 + ), + new Issue( + self::ParamSignaturePHPDocMismatchParamIsReference, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (parameter #{INDEX} is a reference parameter overriding a non-reference parameter) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7039 + ), + new Issue( + self::ParamSignatureRealMismatchParamIsNotReference, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with {METHOD} (parameter #{INDEX} is a non-reference parameter overriding a reference parameter) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7027 + ), + new Issue( + self::ParamSignatureRealMismatchParamIsNotReferenceInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (parameter #{INDEX} is a non-reference parameter overriding a reference parameter)", + self::REMEDIATION_B, + 7028 + ), + new Issue( + self::ParamSignaturePHPDocMismatchParamIsNotReference, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (parameter #{INDEX} is a non-reference parameter overriding a reference parameter) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7040 + ), + new Issue( + self::ParamSignatureRealMismatchTooFewParameters, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with {METHOD} (the method override accepts {COUNT} parameter(s), but the overridden method can accept {COUNT}) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7029 + ), + new Issue( + self::ParamSignatureRealMismatchTooFewParametersInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (the method override accepts {COUNT} parameter(s), but the overridden method can accept {COUNT})", + self::REMEDIATION_B, + 7030 + ), + new Issue( + self::ParamSignaturePHPDocMismatchTooFewParameters, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (the method override accepts {COUNT} parameter(s), but the overridden method can accept {COUNT}) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7041 + ), + new Issue( + self::ParamSignatureRealMismatchTooManyRequiredParameters, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with {METHOD} (the method override requires {COUNT} parameter(s), but the overridden method requires only {COUNT}) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7031 + ), + new Issue( + self::ParamSignatureRealMismatchTooManyRequiredParametersInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} should be compatible with internal {METHOD} (the method override requires {COUNT} parameter(s), but the overridden method requires only {COUNT})", + self::REMEDIATION_B, + 7032 + ), + new Issue( + self::ParamSignaturePHPDocMismatchTooManyRequiredParameters, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Declaration of real/@method {METHOD} should be compatible with real/@method {METHOD} (the method override requires {COUNT} parameter(s), but the overridden method requires only {COUNT}) defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 7042 + ), + new Issue( + self::ParamSuspiciousOrder, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + "Argument #{INDEX} of this call to {FUNCTIONLIKE} is typically a literal or constant but isn't, but argument #{INDEX} (which is typically a variable) is a literal or constant. The arguments may be in the wrong order.", + self::REMEDIATION_B, + 7045 + ), + new Issue( + self::ParamTooManyUnpack, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Call with {COUNT} or more args to {FUNCTIONLIKE} which only takes {COUNT} arg(s) defined at {FILE}:{LINE} (argument unpacking was used)", + self::REMEDIATION_B, + 7046 + ), + new Issue( + self::ParamTooManyUnpackInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + "Call with {COUNT} or more args to {FUNCTIONLIKE} which only takes {COUNT} arg(s) (argument unpacking was used)", + self::REMEDIATION_B, + 7047 + ), + new Issue( + self::ParamMustBeUserDefinedClassname, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + "First argument of class_alias() must be a name of user defined class ('{CLASS}' attempted)", + self::REMEDIATION_B, + 7048 + ), + new Issue( + self::ParamNameIndicatingUnused, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + 'Saw a parameter named ${PARAMETER}. If this was used to indicate that a parameter is unused to Phan, consider using @unused-param after a param comment or suppressing unused parameter warnings instead. PHP 8.0 introduces support for named parameters, so changing names to suppress unused parameter warnings is no longer recommended.', + self::REMEDIATION_B, + 7050 + ), + new Issue( + self::ParamNameIndicatingUnusedInClosure, + self::CATEGORY_PARAMETER, + self::SEVERITY_LOW, + 'Saw a parameter named ${PARAMETER}. If this was used to indicate that a parameter is unused to Phan, consider using @unused-param after a param comment or suppressing unused parameter warnings instead. PHP 8.0 introduces support for named parameters, so changing names to suppress unused parameter warnings is no longer recommended.', + self::REMEDIATION_B, + 7051 + ), + new Issue( + self::UndeclaredNamedArgument, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + 'Saw a call with undeclared named argument ({CODE}) to {FUNCTIONLIKE} defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 7052 + ), + new Issue( + self::UndeclaredNamedArgumentInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + 'Saw a call with undeclared named argument ({CODE}) to {FUNCTIONLIKE}', + self::REMEDIATION_B, + 7053 + ), + new Issue( + self::DuplicateNamedArgument, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + 'Saw a call with arguments ({CODE}) and ({CODE}) passed to the same parameter of {FUNCTIONLIKE} defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 7054 + ), + new Issue( + self::DuplicateNamedArgumentInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + 'Saw a call with arguments ({CODE}) and ({CODE}) passed to the same parameter of {FUNCTIONLIKE}', + self::REMEDIATION_B, + 7055 + ), + new Issue( + self::DefinitelyDuplicateNamedArgument, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + 'Cannot repeat the same name for named arguments ({CODE}) and ({CODE})', + self::REMEDIATION_B, + 7056 + ), + new Issue( + self::PositionalArgumentAfterNamedArgument, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + 'Saw positional argument ({CODE}) after a named argument {CODE}', + self::REMEDIATION_B, + 7057 + ), + new Issue( + self::ArgumentUnpackingUsedWithNamedArgument, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + 'Cannot mix named arguments and argument unpacking in {CODE}', + self::REMEDIATION_B, + 7058 + ), + new Issue( + self::MissingNamedArgument, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + 'Missing named argument for {PARAMETER} in call to {METHOD} defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 7059 + ), + new Issue( + self::MissingNamedArgumentInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_CRITICAL, + 'Missing named argument for {PARAMETER} in call to {METHOD}', + self::REMEDIATION_B, + 7060 + ), + new Issue( + self::SuspiciousNamedArgumentForVariadic, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + 'Passing named argument to a variadic parameter ${PARAMETER} of the same name in a call to {METHOD}. This will set the array offset "{PARAMETER}" of the resulting variadic parameter, not the parameter itself (suppress this if this is deliberate).', + self::REMEDIATION_B, + 7061 + ), + new Issue( + self::SuspiciousNamedArgumentVariadicInternal, + self::CATEGORY_PARAMETER, + self::SEVERITY_NORMAL, + 'Passing named argument {CODE} to the variadic parameter of the internal function {METHOD}. Except for a few internal methods that call methods/constructors dynamically, this is usually not supported by internal functions.', + self::REMEDIATION_B, + 7062 + ), + + // Issue::CATEGORY_NOOP + new Issue( + self::NoopProperty, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused property", + self::REMEDIATION_B, + 6000 + ), + new Issue( + self::NoopArray, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused array", + self::REMEDIATION_B, + 6001 + ), + new Issue( + self::NoopConstant, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused constant", + self::REMEDIATION_B, + 6002 + ), + new Issue( + self::NoopClosure, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused closure", + self::REMEDIATION_B, + 6003 + ), + new Issue( + self::NoopVariable, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused variable", + self::REMEDIATION_B, + 6004 + ), + new Issue( + self::NoopUnaryOperator, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of a unary '{OPERATOR}' operator", + self::REMEDIATION_B, + 6020 + ), + new Issue( + self::NoopBinaryOperator, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of a binary '{OPERATOR}' operator", + self::REMEDIATION_B, + 6021 + ), + new Issue( + self::NoopStringLiteral, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of a string literal {STRING_LITERAL} near this line", + self::REMEDIATION_B, + 6029 + ), + new Issue( + self::NoopEmpty, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of an empty({CODE}) check", + self::REMEDIATION_B, + 6051 + ), + new Issue( + self::NoopIsset, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of an isset({CODE}) check", + self::REMEDIATION_B, + 6052 + ), + new Issue( + self::NoopCast, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of a ({TYPE})({CODE}) cast", + self::REMEDIATION_B, + 6053 + ), + new Issue( + self::NoopEncapsulatedStringLiteral, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of an encapsulated string literal", + self::REMEDIATION_B, + 6030 + ), + new Issue( + self::NoopNumericLiteral, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of a numeric literal {SCALAR} near this line", + self::REMEDIATION_B, + 6031 + ), + new Issue( + self::UnreferencedClass, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to class {CLASS}", + self::REMEDIATION_B, + 6005 + ), + new Issue( + self::UnreferencedConstant, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to global constant {CONST}", + self::REMEDIATION_B, + 6008 + ), + new Issue( + self::UnreferencedFunction, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to function {FUNCTION}", + self::REMEDIATION_B, + 6009 + ), + new Issue( + self::UnreferencedClosure, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to {FUNCTION}", + self::REMEDIATION_B, + 6010 + ), + new Issue( + self::UnreferencedPublicMethod, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to public method {METHOD}", + self::REMEDIATION_B, + 6011 + ), + new Issue( + self::UnreferencedProtectedMethod, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to protected method {METHOD}", + self::REMEDIATION_B, + 6012 + ), + new Issue( + self::UnreferencedPrivateMethod, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to private method {METHOD}", + self::REMEDIATION_B, + 6013 + ), + new Issue( + self::UnreferencedPublicProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to public property {PROPERTY}", + self::REMEDIATION_B, + 6014 + ), + new Issue( + self::UnreferencedPHPDocProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to PHPDoc @property {PROPERTY}", + self::REMEDIATION_B, + 6056 + ), + new Issue( + self::UnreferencedProtectedProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to protected property {PROPERTY}", + self::REMEDIATION_B, + 6015 + ), + new Issue( + self::UnreferencedPrivateProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to private property {PROPERTY}", + self::REMEDIATION_B, + 6016 + ), + new Issue( + self::ReadOnlyPublicProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero write references to public property {PROPERTY}", + self::REMEDIATION_B, + 6032 + ), + new Issue( + self::ReadOnlyProtectedProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero write references to protected property {PROPERTY}", + self::REMEDIATION_B, + 6033 + ), + new Issue( + self::ReadOnlyPrivateProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero write references to private property {PROPERTY}", + self::REMEDIATION_B, + 6034 + ), + new Issue( + self::ReadOnlyPHPDocProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero write references to PHPDoc @property {PROPERTY}", + self::REMEDIATION_B, + 6058 + ), + new Issue( + self::WriteOnlyPublicProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero read references to public property {PROPERTY}", + self::REMEDIATION_B, + 6025 + ), + new Issue( + self::WriteOnlyProtectedProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero read references to protected property {PROPERTY}", + self::REMEDIATION_B, + 6026 + ), + new Issue( + self::WriteOnlyPrivateProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero read references to private property {PROPERTY}", + self::REMEDIATION_B, + 6027 + ), + new Issue( + self::WriteOnlyPHPDocProperty, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero read references to PHPDoc @property {PROPERTY}", + self::REMEDIATION_B, + 6057 + ), + new Issue( + self::UnreferencedPublicClassConstant, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to public class constant {CONST}", + self::REMEDIATION_B, + 6017 + ), + new Issue( + self::UnreferencedProtectedClassConstant, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to protected class constant {CONST}", + self::REMEDIATION_B, + 6018 + ), + new Issue( + self::UnreferencedPrivateClassConstant, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to private class constant {CONST}", + self::REMEDIATION_B, + 6019 + ), + new Issue( + self::UnreferencedUseNormal, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to use statement for classlike/namespace {CLASSLIKE} ({CLASSLIKE})", + self::REMEDIATION_B, + 6022 + ), + new Issue( + self::UnreferencedUseFunction, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to use statement for function {FUNCTION} ({FUNCTION})", + self::REMEDIATION_B, + 6023 + ), + new Issue( + self::UnreferencedUseConstant, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Possibly zero references to use statement for constant {CONST} ({CONST})", + self::REMEDIATION_B, + 6024 + ), + new Issue( + self::UnreachableCatch, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Catch statement for {CLASSLIKE} is unreachable. An earlier catch statement at line {LINE} caught the ancestor class/interface {CLASSLIKE}", + self::REMEDIATION_B, + 6028 + ), + new Issue( + self::UnusedVariable, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Unused definition of variable ${VARIABLE}', + self::REMEDIATION_B, + 6035 + ), + new Issue( + self::UnusedPublicMethodParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6036 + ), + new Issue( + self::UnusedPublicFinalMethodParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6037 + ), + new Issue( + self::UnusedPublicNoOverrideMethodParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6060 + ), + new Issue( + self::UnusedProtectedMethodParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6038 + ), + new Issue( + self::UnusedProtectedNoOverrideMethodParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6059 + ), + new Issue( + self::UnusedProtectedFinalMethodParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6039 + ), + new Issue( + self::UnusedPrivateMethodParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6040 + ), + new Issue( + self::UnusedPrivateFinalMethodParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6041 + ), + new Issue( + self::UnusedClosureUseVariable, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Closure use variable ${VARIABLE} is never used', + self::REMEDIATION_B, + 6042 + ), + new Issue( + self::ShadowedVariableInArrowFunc, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Short arrow function shadows variable ${VARIABLE} from the outer scope', + self::REMEDIATION_B, + 6072 + ), + new Issue( + self::UnusedClosureParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6043 + ), + new Issue( + self::UnusedGlobalFunctionParameter, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Parameter ${PARAMETER} is never used', + self::REMEDIATION_B, + 6044 + ), + new Issue( + self::UnusedVariableValueOfForeachWithKey, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Unused definition of variable ${VARIABLE} as the value of a foreach loop that included keys', + self::REMEDIATION_B, + 6045 + ), + new Issue( + self::EmptyForeach, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Saw a foreach statement with empty iterable type {TYPE}', + self::REMEDIATION_B, + 6079 + ), + new Issue( + self::EmptyForeachBody, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Saw a foreach statement with empty body over array of type {TYPE} (iterating has no side effects)', + self::REMEDIATION_B, + 6086 + ), + new Issue( + self::SideEffectFreeForeachBody, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Saw a foreach loop which probably has no side effects', + self::REMEDIATION_B, + 6089 + ), + new Issue( + self::SideEffectFreeForBody, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Saw a for loop which probably has no side effects', + self::REMEDIATION_B, + 6090 + ), + new Issue( + self::SideEffectFreeWhileBody, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Saw a while loop which probably has no side effects', + self::REMEDIATION_B, + 6091 + ), + new Issue( + self::SideEffectFreeDoWhileBody, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Saw a do-while loop which probably has no side effects', + self::REMEDIATION_B, + 6092 + ), + new Issue( + self::EmptyYieldFrom, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Saw a yield from statement with empty iterable type {TYPE}', + self::REMEDIATION_B, + 6080 + ), + new Issue( + self::UselessBinaryAddRight, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Addition of {TYPE} + {TYPE} {CODE} is probably unnecessary. Array fields from the left hand side will be used instead of each of the fields from the right hand side", + self::REMEDIATION_B, + 6081 + ), + new Issue( + self::SuspiciousBinaryAddLists, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Addition of {TYPE} + {TYPE} {CODE} is a suspicious way to add two lists. Some of the array fields from the left hand side will be part of the result, replacing the fields with the same key from the right hand side (this operator does not concatenate the lists)", + self::REMEDIATION_B, + 6082 + ), + new Issue( + self::UnusedVariableCaughtException, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Unused definition of variable ${VARIABLE} as a caught exception', + self::REMEDIATION_B, + 6046 + ), + new Issue( + self::UnusedVariableReference, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Unused definition of variable ${VARIABLE} as a reference', + self::REMEDIATION_B, + 6069 + ), + new Issue( + self::UnusedVariableStatic, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Unreferenced definition of variable ${VARIABLE} as a static variable', + self::REMEDIATION_B, + 6070 + ), + new Issue( + self::UnusedVariableGlobal, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Unreferenced definition of variable ${VARIABLE} as a global variable', + self::REMEDIATION_B, + 6071 + ), + new Issue( + self::UnusedReturnBranchWithoutSideEffects, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Possibly useless branch in a function where the return value must be used - all branches return values equivalent to {CODE} (previous return is at line {LINE})', + self::REMEDIATION_B, + 6083 + ), + new Issue( + self::RedundantArrayValuesCall, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'Attempting to convert {TYPE} to a list using {FUNCTION} (it is already a list)', + self::REMEDIATION_B, + 6087 + ), + new Issue( + self::UseNormalNamespacedNoEffect, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'The use statement for class/namespace {CLASS} in a namespace has no effect', + self::REMEDIATION_A, + 6047 + ), + new Issue( + self::UseNormalNoEffect, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'The use statement for class/namespace {CLASS} in the global namespace has no effect', + self::REMEDIATION_A, + 6048 + ), + new Issue( + self::UseFunctionNoEffect, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'The use statement for function {FUNCTION} has no effect', + self::REMEDIATION_A, + 6049 + ), + new Issue( + self::UseConstantNoEffect, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + 'The use statement for constant {CONST} has no effect', + self::REMEDIATION_A, + 6050 + ), + new Issue( + self::NoopArrayAccess, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused array offset fetch", + self::REMEDIATION_B, + 6054 + ), + new Issue( + self::UnusedGotoLabel, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused goto label {CODE}", + self::REMEDIATION_B, + 6055 + ), + new Issue( + self::VariableDefinitionCouldBeConstant, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Uses of ${VARIABLE} could probably be replaced with a literal or constant', + self::REMEDIATION_B, + 6061 + ), + new Issue( + self::VariableDefinitionCouldBeConstantEmptyArray, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Uses of ${VARIABLE} could probably be replaced with an empty array', + self::REMEDIATION_B, + 6062 + ), + new Issue( + self::VariableDefinitionCouldBeConstantString, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Uses of ${VARIABLE} could probably be replaced with a literal or constant string', + self::REMEDIATION_B, + 6063 + ), + new Issue( + self::VariableDefinitionCouldBeConstantFloat, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Uses of ${VARIABLE} could probably be replaced with a literal or constant float', + self::REMEDIATION_B, + 6064 + ), + new Issue( + self::VariableDefinitionCouldBeConstantInt, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Uses of ${VARIABLE} could probably be replaced with literal integer or a named constant', + self::REMEDIATION_B, + 6065 + ), + new Issue( + self::VariableDefinitionCouldBeConstantTrue, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Uses of ${VARIABLE} could probably be replaced with true or a named constant', + self::REMEDIATION_B, + 6066 + ), + new Issue( + self::VariableDefinitionCouldBeConstantFalse, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Uses of ${VARIABLE} could probably be replaced with false or a named constant', + self::REMEDIATION_B, + 6067 + ), + new Issue( + self::VariableDefinitionCouldBeConstantNull, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Uses of ${VARIABLE} could probably be replaced with null or a named constant', + self::REMEDIATION_B, + 6068 + ), + new Issue( + self::NoopTernary, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of a ternary expression where the true/false results don't seem to have side effects", + self::REMEDIATION_B, + 6073 + ), + new Issue( + self::NoopNew, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + "Unused result of new object creation expression in {CODE} (this may be called for the side effects of the non-empty constructor or destructor)", + self::REMEDIATION_B, + 6084 + ), + new Issue( + self::NoopNewNoSideEffects, + self::CATEGORY_NOOP, + self::SEVERITY_NORMAL, + "Unused result of new object creation expression in {CODE} (this is likely free of side effects - there is no known non-empty constructor or destructor)", + self::REMEDIATION_B, + 6085 + ), + new Issue( + self::EmptyPublicMethod, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Empty public method {METHOD}', + self::REMEDIATION_B, + 6074 + ), + new Issue( + self::EmptyProtectedMethod, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Empty protected method {METHOD}', + self::REMEDIATION_B, + 6075 + ), + new Issue( + self::EmptyPrivateMethod, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Empty private method {METHOD}', + self::REMEDIATION_B, + 6076 + ), + new Issue( + self::EmptyFunction, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Empty function {FUNCTION}', + self::REMEDIATION_B, + 6077 + ), + new Issue( + self::EmptyClosure, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Empty closure {FUNCTION}', + self::REMEDIATION_B, + 6078 + ), + new Issue( + self::NoopSwitchCases, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'This switch statement only has the default case', + self::REMEDIATION_B, + 6088 + ), + new Issue( + self::ProvidingUnusedParameter, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Providing an unused optional parameter ${PARAMETER} to {FUNCTIONLIKE} defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 6093 + ), + new Issue( + self::ProvidingUnusedParameterOfClosure, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Providing an unused optional parameter ${PARAMETER} to {FUNCTIONLIKE} defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 6094 + ), + new Issue( + self::NoopMatchArms, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'This match expression only has the default arm in {CODE}', + self::REMEDIATION_B, + 6095 + ), + new Issue( + self::NoopMatchExpression, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'The result of this match expression is not used and the arms have no side effects (except for possibly throwing UnhandledMatchError) in {CODE}', + self::REMEDIATION_B, + 6096 + ), + // TODO: If this is the attributes syntax in php 8.0 stable then this should be become critical. + new Issue( + self::NoopRepeatedSilenceOperator, + self::CATEGORY_NOOP, + self::SEVERITY_LOW, + 'Saw a repeated silence operator in {CODE}', + self::REMEDIATION_B, + 6097 + ), + + // Issue::CATEGORY_REDEFINE + new Issue( + self::RedefineClass, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "{CLASS} defined at {FILE}:{LINE} was previously defined as {CLASS} at {FILE}:{LINE}", + self::REMEDIATION_B, + 8000 + ), + new Issue( + self::RedefineClassInternal, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "{CLASS} defined at {FILE}:{LINE} was previously defined as {CLASS} internally", + self::REMEDIATION_B, + 8001 + ), + // TODO: Split into RedefineMethod, which would be fatal + new Issue( + self::RedefineFunction, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "Function {FUNCTION} defined at {FILE}:{LINE} was previously defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 8002 + ), + new Issue( + self::RedefineFunctionInternal, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "Function {FUNCTION} defined at {FILE}:{LINE} was previously defined internally", + self::REMEDIATION_B, + 8003 + ), + new Issue( + self::IncompatibleCompositionProp, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "{TRAIT} and {TRAIT} define the same property ({PROPERTY}) in the composition of {CLASS}, as the types {TYPE} and {TYPE} respectively. However, the definition differs and is considered incompatible. Class was composed in {FILE} on line {LINE}", + self::REMEDIATION_B, + 8004 + ), + new Issue( + self::IncompatibleCompositionMethod, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "Declaration of {METHOD} must be compatible with {METHOD} in {FILE} on line {LINE}", + self::REMEDIATION_B, + 8005 + ), + // FIXME: It's redundant to include the first FILE:LINE of the declaration in the full issue message + new Issue( + self::RedefineClassAlias, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "{CLASS} aliased at {FILE}:{LINE} was previously defined as {CLASS} at {FILE}:{LINE}", + self::REMEDIATION_B, + 8006 + ), + new Issue( + self::RedefinedUsedTrait, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "{CLASS} uses {TRAIT} declared at {FILE}:{LINE} which is also declared at {FILE}:{LINE}. This may lead to confusing errors.", + self::REMEDIATION_B, + 8007 + ), + new Issue( + self::RedefinedInheritedInterface, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "{CLASS} inherits {INTERFACE} declared at {FILE}:{LINE} which is also declared at {FILE}:{LINE}. This may lead to confusing errors.", + self::REMEDIATION_B, + 8008 + ), + new Issue( + self::RedefinedExtendedClass, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "{CLASS} extends {CLASS} declared at {FILE}:{LINE} which is also declared at {FILE}:{LINE}. This may lead to confusing errors. It may be possible to exclude the class that isn't used with exclude_file_list.", + self::REMEDIATION_B, + 8009 + ), + new Issue( + self::RedefinedClassReference, + self::CATEGORY_REDEFINE, + self::SEVERITY_NORMAL, + "Saw reference to {CLASS} declared at {FILE}:{LINE} which is also declared at {FILE}:{LINE}. This may lead to confusing errors. It may be possible to exclude the class that isn't used with exclude_file_list. In addition to normal ways to suppress issues, this issue type can be suppressed on either of the class definitions if it is impractical to exclude one file.", + self::REMEDIATION_B, + 8012 + ), + new Issue( + self::RedefineClassConstant, + self::CATEGORY_REDEFINE, + self::SEVERITY_CRITICAL, + "Class constant {CONST} defined at {FILE}:{LINE} was previously defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 8010 + ), + new Issue( + self::RedefineProperty, + self::CATEGORY_REDEFINE, + self::SEVERITY_CRITICAL, + 'Property ${PROPERTY} defined at {FILE}:{LINE} was previously defined at {FILE}:{LINE}', + self::REMEDIATION_B, + 8011 + ), + + // Issue::CATEGORY_ACCESS + new Issue( + self::AccessPropertyProtected, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot access protected property {PROPERTY} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1000 + ), + new Issue( + self::AccessPropertyPrivate, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot access private property {PROPERTY} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1001 + ), + new Issue( + self::AccessReadOnlyProperty, + self::CATEGORY_ACCESS, + self::SEVERITY_LOW, + "Cannot modify read-only property {PROPERTY} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1028 + ), + new Issue( + self::AccessWriteOnlyProperty, + self::CATEGORY_ACCESS, + self::SEVERITY_LOW, + "Cannot read write-only property {PROPERTY} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1029 + ), + new Issue( + self::AccessReadOnlyMagicProperty, + self::CATEGORY_ACCESS, + self::SEVERITY_NORMAL, + "Cannot modify read-only magic property {PROPERTY} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1030 + ), + new Issue( + self::AccessWriteOnlyMagicProperty, + self::CATEGORY_ACCESS, + self::SEVERITY_NORMAL, + "Cannot read write-only magic property {PROPERTY} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1031 + ), + new Issue( + self::AccessMethodProtected, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot access protected method {METHOD} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1002 + ), + new Issue( + self::AccessMethodPrivate, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot access private method {METHOD} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1003 + ), + new Issue( + self::AccessSignatureMismatch, + self::CATEGORY_ACCESS, + self::SEVERITY_NORMAL, + "Access level to {METHOD} must be compatible with {METHOD} defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 1004 + ), + new Issue( + self::AccessSignatureMismatchInternal, + self::CATEGORY_ACCESS, + self::SEVERITY_NORMAL, + "Access level to {METHOD} must be compatible with internal {METHOD}", + self::REMEDIATION_B, + 1005 + ), + new Issue( + self::ConstructAccessSignatureMismatch, + self::CATEGORY_ACCESS, + self::SEVERITY_NORMAL, + "Access level to {METHOD} must be compatible with {METHOD} defined in {FILE}:{LINE} in PHP versions 7.1 and below", + self::REMEDIATION_B, + 1032 + ), + new Issue( + self::PropertyAccessSignatureMismatch, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Access level to {PROPERTY} must be compatible with {PROPERTY} defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 1022 + ), + new Issue( + self::PropertyAccessSignatureMismatchInternal, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Access level to {PROPERTY} must be compatible with internal {PROPERTY}", + self::REMEDIATION_B, + 1023 + ), + new Issue( + self::ConstantAccessSignatureMismatch, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Access level to {CONST} must be compatible with {CONST} defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 1024 + ), + new Issue( + self::ConstantAccessSignatureMismatchInternal, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Access level to {CONST} must be compatible with internal {CONST}", + self::REMEDIATION_B, + 1025 + ), + new Issue( + self::AccessStaticToNonStaticProperty, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot make static property {PROPERTY} into the non static property {PROPERTY}", + self::REMEDIATION_B, + 1026 + ), + new Issue( + self::AccessNonStaticToStaticProperty, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot make non static property {PROPERTY} into the static property {PROPERTY}", + self::REMEDIATION_B, + 1027 + ), + new Issue( + self::AccessStaticToNonStatic, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot make static method {METHOD}() non static", + self::REMEDIATION_B, + 1006 + ), + new Issue( + self::AccessNonStaticToStatic, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot make non static method {METHOD}() static", + self::REMEDIATION_B, + 1007 + ), + new Issue( + self::AccessClassConstantPrivate, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot access private class constant {CONST} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1008 + ), + new Issue( + self::AccessClassConstantProtected, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot access protected class constant {CONST} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1009 + ), + new Issue( + self::AccessPropertyStaticAsNonStatic, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Accessing static property {PROPERTY} as non static", + self::REMEDIATION_B, + 1010 + ), + new Issue( + self::AccessOwnConstructor, + self::CATEGORY_ACCESS, + self::SEVERITY_NORMAL, + "Accessing own constructor directly via {CLASS}::__construct", + self::REMEDIATION_B, + 1020 + ), + new Issue( + self::AccessMethodProtectedWithCallMagicMethod, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot access protected method {METHOD} defined at {FILE}:{LINE} (if this call should be handled by __call, consider adding a @method tag to the class)", + self::REMEDIATION_B, + 1011 + ), + new Issue( + self::AccessMethodPrivateWithCallMagicMethod, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Cannot access private method {METHOD} defined at {FILE}:{LINE} (if this call should be handled by __call, consider adding a @method tag to the class)", + self::REMEDIATION_B, + 1012 + ), + new Issue( + self::AccessWrongInheritanceCategory, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Attempting to inherit {CLASSLIKE} defined at {FILE}:{LINE} as if it were a {CLASSLIKE}", + self::REMEDIATION_B, + 1013 + ), + new Issue( + self::AccessWrongInheritanceCategoryInternal, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Attempting to inherit internal {CLASSLIKE} as if it were a {CLASSLIKE}", + self::REMEDIATION_B, + 1014 + ), + new Issue( + self::AccessExtendsFinalClass, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Attempting to extend from final class {CLASS} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 1015 + ), + new Issue( + self::AccessExtendsFinalClassInternal, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Attempting to extend from final internal class {CLASS}", + self::REMEDIATION_B, + 1016 + ), + new Issue( + self::AccessOverridesFinalMethod, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Declaration of method {METHOD} overrides final method {METHOD} defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 1017 + ), + new Issue( + self::AccessOverridesFinalMethodInternal, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Declaration of method {METHOD} overrides final internal method {METHOD}", + self::REMEDIATION_B, + 1018 + ), + new Issue( + self::AccessOverridesFinalMethodPHPDoc, + self::CATEGORY_ACCESS, + self::SEVERITY_LOW, + "Declaration of phpdoc method {METHOD} is an unnecessary override of final method {METHOD} defined in {FILE}:{LINE}", + self::REMEDIATION_B, + 1019 + ), + new Issue( + self::AccessPropertyNonStaticAsStatic, + self::CATEGORY_ACCESS, + self::SEVERITY_CRITICAL, + "Accessing non static property {PROPERTY} as static", + self::REMEDIATION_B, + 1021 + ), + new Issue( + self::AccessOverridesFinalMethodInTrait, + self::CATEGORY_ACCESS, + self::SEVERITY_NORMAL, + "Declaration of method {METHOD} overrides final method {METHOD} defined in trait in {FILE}:{LINE}. This is actually allowed in case of traits, even for final methods, but may lead to unexpected behavior", + self::REMEDIATION_B, + 1033 + ), + + // Issue::CATEGORY_COMPATIBLE + new Issue( + self::CompatiblePHP7, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Expression may not be PHP 7 compatible", + self::REMEDIATION_B, + 3000 + ), + new Issue( + self::CompatibleExpressionPHP7, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "{CLASS} expression may not be PHP 7 compatible", + self::REMEDIATION_B, + 3001 + ), + new Issue( + self::CompatibleNullableTypePHP70, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Nullable type '{TYPE}' is not compatible with PHP 7.0", + self::REMEDIATION_B, + 3002 + ), + new Issue( + self::CompatibleShortArrayAssignPHP70, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Square bracket syntax for an array destructuring assignment is not compatible with PHP 7.0", + self::REMEDIATION_A, + 3003 + ), + new Issue( + self::CompatibleKeyedArrayAssignPHP70, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Using array keys in an array destructuring assignment is not compatible with PHP 7.0", + self::REMEDIATION_B, + 3004 + ), + new Issue( + self::CompatibleVoidTypePHP70, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Return type '{TYPE}' means the absence of a return value starting in PHP 7.1. In PHP 7.0, void refers to a class/interface with the name 'void'", + self::REMEDIATION_B, + 3005 + ), + new Issue( + self::CompatibleIterableTypePHP70, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Return type '{TYPE}' means a Traversable/array value starting in PHP 7.1. In PHP 7.0, iterable refers to a class/interface with the name 'iterable'", + self::REMEDIATION_B, + 3006 + ), + new Issue( + self::CompatibleObjectTypePHP71, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Type '{TYPE}' refers to any object starting in PHP 7.2. In PHP 7.1 and earlier, it refers to a class/interface with the name 'object'", + self::REMEDIATION_B, + 3007 + ), + new Issue( + self::CompatibleMixedType, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Type '{TYPE}' refers to any value starting in PHP 8.0. In PHP 7.4 and earlier, it refers to a class/interface with the name 'mixed'", + self::REMEDIATION_B, + 3029 + ), + new Issue( + self::CompatibleUseVoidPHP70, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Using '{TYPE}' as void will be a syntax error in PHP 7.1 (void becomes the absence of a return type).", + self::REMEDIATION_B, + 3008 + ), + new Issue( + self::CompatibleUseIterablePHP71, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Using '{TYPE}' as iterable will be a syntax error in PHP 7.2 (iterable becomes a native type with subtypes Array and Iterator).", + self::REMEDIATION_B, + 3009 + ), + new Issue( + self::CompatibleUseObjectPHP71, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Using '{TYPE}' as object will be a syntax error in PHP 7.2 (object becomes a native type that accepts any class instance).", + self::REMEDIATION_B, + 3010 + ), + new Issue( + self::CompatibleUseMixed, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Using '{TYPE}' as mixed will be a syntax error in PHP 8.0 (mixed becomes a native type that accepts any value).", + self::REMEDIATION_B, + 3030 + ), + new Issue( + self::CompatibleMultiExceptionCatchPHP70, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Catching multiple exceptions is not supported before PHP 7.1", + self::REMEDIATION_B, + 3011 + ), + new Issue( + self::CompatibleNonCapturingCatch, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Catching exceptions without a variable is not supported before PHP 8.0 in catch ({CLASS})", + self::REMEDIATION_B, + 3031 + ), + new Issue( + self::CompatibleNegativeStringOffset, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Using negative string offsets is not supported before PHP 7.1 (emits an 'Uninitialized string offset' notice)", + self::REMEDIATION_B, + 3012 + ), + new Issue( + self::CompatibleAutoload, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Declaring an autoloader with function __autoload() was deprecated in PHP 7.2 and is a fatal error in PHP 8.0+. Use spl_autoload_register() instead (supported since PHP 5.1).", + self::REMEDIATION_B, + 3013 + ), + new Issue( + self::CompatibleUnsetCast, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "The unset cast (in {CODE}) was deprecated in PHP 7.2 and is a fatal error in PHP 8.0+.", + self::REMEDIATION_B, + 3014 + ), + new Issue( + self::ThrowStatementInToString, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "{FUNCTIONLIKE} throws {TYPE} here, but throwing in __toString() is a fatal error prior to PHP 7.4", + self::REMEDIATION_A, + 3015 + ), + new Issue( + self::ThrowCommentInToString, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "{FUNCTIONLIKE} documents that it throws {TYPE}, but throwing in __toString() is a fatal error prior to PHP 7.4", + self::REMEDIATION_A, + 3016 + ), + new Issue( + self::CompatibleSyntaxNotice, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Saw a parse notice: {DETAILS}", + self::REMEDIATION_B, + 3017 + ), + // TODO: Update messages to reflect that these were removed in php 8.0 + new Issue( + self::CompatibleDimAlternativeSyntax, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Array and string offset access syntax with curly braces is deprecated in PHP 7.4. Use square brackets instead. Seen for {CODE}", + self::REMEDIATION_B, + 3018 + ), + new Issue( + self::CompatibleImplodeOrder, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "In php 7.4, passing glue string after the array is deprecated for {FUNCTION}. Should this swap the parameters of type {TYPE} and {TYPE}?", + self::REMEDIATION_B, + 3019 + ), + new Issue( + self::CompatibleUnparenthesizedTernary, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "Unparenthesized '{CODE}' is deprecated. Use either '{CODE}' or '{CODE}'", + self::REMEDIATION_B, + 3020 + ), + new Issue( + self::CompatibleTypedProperty, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use typed properties before php 7.4. This property group has type {TYPE}", + self::REMEDIATION_B, + 3021 + ), + // TODO mention that they will be treated like regular methods. + new Issue( + self::CompatiblePHP8PHP4Constructor, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "PHP4 constructors will be removed in php 8, and should not be used. __construct() should be added/used instead to avoid accidentally calling {METHOD}", + self::REMEDIATION_B, + 3022 + ), + new Issue( + self::CompatibleDefaultEqualsNull, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "In PHP 8.0, using a default ({CODE}) that resolves to null will no longer cause the parameter ({PARAMETER}) to be nullable", + self::REMEDIATION_B, + 3023 + ), + new Issue( + self::CompatibleScalarTypePHP56, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "In PHP 5.6, scalar types such as {TYPE} in type signatures are treated like class names", + self::REMEDIATION_B, + 3024 + ), + new Issue( + self::CompatibleAnyReturnTypePHP56, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_CRITICAL, + "In PHP 5.6, return types ({TYPE}) are not supported", + self::REMEDIATION_B, + 3025 + ), + new Issue( + self::CompatibleUnionType, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use union types ({TYPE}) before php 8.0", + self::REMEDIATION_B, + 3026 + ), + new Issue( + self::CompatibleStaticType, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use static return types before php 8.0", + self::REMEDIATION_B, + 3027 + ), + new Issue( + self::CompatibleThrowExpression, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use throw as an expression before php 8.0 in {CODE}", + self::REMEDIATION_B, + 3028 + ), + new Issue( + self::CompatibleMatchExpression, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use match expressions before php 8.0 in {CODE}", + self::REMEDIATION_B, + 3032 + ), + new Issue( + self::CompatibleArrowFunction, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use arrow functions before php 7.4 in {CODE}", + self::REMEDIATION_B, + 3033 + ), + new Issue( + self::CompatibleNullsafeOperator, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use nullsafe operator before php 8.0 in {CODE}", + self::REMEDIATION_B, + 3034 + ), + new Issue( + self::CompatibleNamedArgument, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use named arguments before php 8.0 in argument ({CODE})", + self::REMEDIATION_B, + 3035 + ), + // NOTE: The fact that the native php-ast does not track trailing commas is by design. + // It exposes the information that php's implementation stores internally, + // and that information is not available because php itself does not need it. + new Issue( + self::CompatibleTrailingCommaParameterList, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use trailing commas in parameter or closure use lists before php 8.0 in declaration of {FUNCTIONLIKE}. NOTE: THIS ISSUE CAN ONLY DETECTED BY THE POLYFILL.", + self::REMEDIATION_B, + 3036 + ), + new Issue( + self::CompatibleTrailingCommaArgumentList, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use trailing commas in argument lists before php 7.3 in {CODE}. NOTE: THIS ISSUE CAN ONLY DETECTED BY THE POLYFILL.", + self::REMEDIATION_B, + 3037 + ), + new Issue( + self::CompatibleConstructorPropertyPromotion, + self::CATEGORY_COMPATIBLE, + self::SEVERITY_NORMAL, + "Cannot use constructor property promotion before php 8.0 for {PARAMETER} of {METHOD}", + self::REMEDIATION_B, + 3038 + ), + + // Issue::CATEGORY_GENERIC + new Issue( + self::TemplateTypeConstant, + self::CATEGORY_GENERIC, + self::SEVERITY_NORMAL, + "constant {CONST} may not have a template type", + self::REMEDIATION_B, + 14000 + ), + new Issue( + self::TemplateTypeStaticMethod, + self::CATEGORY_GENERIC, + self::SEVERITY_NORMAL, + "static method {METHOD} does not declare template type in its own comment and may not use the template type of class instances", + self::REMEDIATION_B, + 14001 + ), + new Issue( + self::TemplateTypeStaticProperty, + self::CATEGORY_GENERIC, + self::SEVERITY_NORMAL, + "static property {PROPERTY} may not have a template type", + self::REMEDIATION_B, + 14002 + ), + new Issue( + self::GenericGlobalVariable, + self::CATEGORY_GENERIC, + self::SEVERITY_NORMAL, + "Global variable {VARIABLE} may not be assigned an instance of a generic class", + self::REMEDIATION_B, + 14003 + ), + new Issue( + self::GenericConstructorTypes, + self::CATEGORY_GENERIC, + self::SEVERITY_NORMAL, + "Missing template parameter for type {TYPE} on constructor for generic class {CLASS}", + self::REMEDIATION_B, + 14004 + ), + // TODO: Reword this if template types can be used for phan-assert or for compatibility with other arguments + new Issue( + self::TemplateTypeNotUsedInFunctionReturn, + self::CATEGORY_GENERIC, + self::SEVERITY_NORMAL, + "Template type {TYPE} not used in return value of function/method {FUNCTIONLIKE}", + self::REMEDIATION_B, + 14005 + ), + new Issue( + self::TemplateTypeNotDeclaredInFunctionParams, + self::CATEGORY_GENERIC, + self::SEVERITY_NORMAL, + "Template type {TYPE} not declared in parameters of function/method {FUNCTIONLIKE} (or Phan can't extract template types for this use case)", + self::REMEDIATION_B, + 14006 + ), + + // Issue::CATEGORY_INTERNAL + new Issue( + self::AccessConstantInternal, + self::CATEGORY_INTERNAL, + self::SEVERITY_NORMAL, + "Cannot access internal constant {CONST} of namespace {NAMESPACE} defined at {FILE}:{LINE} from namespace {NAMESPACE}", + self::REMEDIATION_B, + 15000 + ), + new Issue( + self::AccessClassInternal, + self::CATEGORY_INTERNAL, + self::SEVERITY_NORMAL, + "Cannot access internal {CLASS} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 15001 + ), + new Issue( + self::AccessClassConstantInternal, + self::CATEGORY_INTERNAL, + self::SEVERITY_NORMAL, + "Cannot access internal class constant {CONST} defined at {FILE}:{LINE}", + self::REMEDIATION_B, + 15002 + ), + new Issue( + self::AccessPropertyInternal, + self::CATEGORY_INTERNAL, + self::SEVERITY_NORMAL, + "Cannot access internal property {PROPERTY} of namespace {NAMESPACE} defined at {FILE}:{LINE} from namespace {NAMESPACE}", + self::REMEDIATION_B, + 15003 + ), + new Issue( + self::AccessMethodInternal, + self::CATEGORY_INTERNAL, + self::SEVERITY_NORMAL, + "Cannot access internal method {METHOD} of namespace {NAMESPACE} defined at {FILE}:{LINE} from namespace {NAMESPACE}", + self::REMEDIATION_B, + 15004 + ), + + // Issue::CATEGORY_COMMENT + new Issue( + self::InvalidCommentForDeclarationType, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "The phpdoc comment for {COMMENT} cannot occur on a {TYPE}", + self::REMEDIATION_B, + 16000 + ), + new Issue( + self::MisspelledAnnotation, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Saw misspelled annotation {COMMENT}. {SUGGESTION}", + self::REMEDIATION_B, + 16001 + ), + new Issue( + self::UnextractableAnnotation, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Saw unextractable annotation for comment '{COMMENT}'", + self::REMEDIATION_B, + 16002 + ), + new Issue( + self::UnextractableAnnotationPart, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Saw unextractable annotation for a fragment of comment '{COMMENT}': '{COMMENT}'", + self::REMEDIATION_B, + 16003 + ), + new Issue( + self::UnextractableAnnotationSuffix, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Saw a token Phan may have failed to parse after '{COMMENT}': after {TYPE}, saw '{COMMENT}'", + self::REMEDIATION_B, + 16009 + ), + new Issue( + self::UnextractableAnnotationElementName, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Saw possibly unextractable annotation for a fragment of comment '{COMMENT}': after {TYPE}, did not see an element name (will guess based on comment order)", + self::REMEDIATION_B, + 16010 + ), + new Issue( + self::CommentParamWithoutRealParam, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + 'Saw an @param annotation for ${PARAMETER}, but it was not found in the param list of {FUNCTIONLIKE}', + self::REMEDIATION_B, + 16004 + ), + new Issue( + self::CommentParamAssertionWithoutRealParam, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + 'Saw an @phan-assert annotation for ${PARAMETER}, but it was not found in the param list of {FUNCTIONLIKE}', + self::REMEDIATION_B, + 16019 + ), + new Issue( + self::CommentParamOnEmptyParamList, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + 'Saw an @param annotation for ${PARAMETER}, but the param list of {FUNCTIONLIKE} is empty', + self::REMEDIATION_B, + 16005 + ), + new Issue( + self::CommentOverrideOnNonOverrideMethod, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Saw an @override annotation for method {METHOD}, but could not find an overridden method and it is not a magic method", + self::REMEDIATION_B, + 16006 + ), + new Issue( + self::CommentOverrideOnNonOverrideConstant, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Saw an @override annotation for class constant {CONST}, but could not find an overridden constant", + self::REMEDIATION_B, + 16007 + ), + new Issue( + self::CommentOverrideOnNonOverrideProperty, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Saw an @override annotation for property {PROPERTY}, but could not find an overridden property", + self::REMEDIATION_B, + 16026 + ), + new Issue( + self::CommentAbstractOnInheritedConstant, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Class {CLASS} inherits a class constant {CONST} declared at {FILE}:{LINE} marked as {COMMENT} in phpdoc but does not override it", + self::REMEDIATION_B, + 16023 + ), + new Issue( + self::CommentAbstractOnInheritedProperty, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Class {CLASS} inherits a property {PROPERTY} declared at {FILE}:{LINE} marked as {COMMENT} in phpdoc but does not override it", + self::REMEDIATION_B, + 16024 + ), + new Issue( + self::CommentAbstractOnInheritedMethod, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Class {CLASS} inherits a method {METHOD} declared at {FILE}:{LINE} marked as {COMMENT} in phpdoc but does not override it", + self::REMEDIATION_B, + 16025 + ), + new Issue( + self::CommentParamOutOfOrder, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + 'Expected @param annotation for ${PARAMETER} to be before the @param annotation for ${PARAMETER}', + self::REMEDIATION_A, + 16008 + ), + new Issue( + self::CommentVarInsteadOfParam, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + 'Saw @var annotation for ${VARIABLE} but Phan expects the @param annotation to document the parameter with that name for {FUNCTION}', + self::REMEDIATION_A, + 16022 + ), + new Issue( + self::ThrowTypeAbsent, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "{FUNCTIONLIKE} can throw {CODE} of type {TYPE} here, but has no '@throws' declarations for that class", + self::REMEDIATION_A, + 16011 + ), + new Issue( + self::ThrowTypeAbsentForCall, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "{FUNCTIONLIKE} can throw {TYPE} because it calls {FUNCTIONLIKE}, but has no '@throws' declarations for that class", + self::REMEDIATION_A, + 16012 + ), + new Issue( + self::ThrowTypeMismatch, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "{FUNCTIONLIKE} throws {CODE} of type {TYPE} here, but it only has declarations of '@throws {TYPE}'", + self::REMEDIATION_A, + 16013 + ), + new Issue( + self::ThrowTypeMismatchForCall, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "{FUNCTIONLIKE} throws {TYPE} because it calls {FUNCTIONLIKE}, but it only has declarations of '@throws {TYPE}'", + self::REMEDIATION_A, + 16014 + ), + new Issue( + self::CommentAmbiguousClosure, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Comment {STRING_LITERAL} refers to {TYPE} instead of \\Closure - Assuming \\Closure", + self::REMEDIATION_A, + 16015 + ), + new Issue( + self::CommentDuplicateParam, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + 'Comment declares @param ${PARAMETER} multiple times', + self::REMEDIATION_A, + 16016 + ), + new Issue( + self::CommentDuplicateMagicProperty, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + 'Comment declares @property* ${PROPERTY} multiple times', + self::REMEDIATION_A, + 16017 + ), + // TODO: Support declaring both instance and static methods of the same name + new Issue( + self::CommentDuplicateMagicMethod, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + 'Comment declares @method {METHOD} multiple times', + self::REMEDIATION_A, + 16018 + ), + new Issue( + self::DebugAnnotation, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + '@phan-debug-var requested for variable ${VARIABLE} - it has union type {TYPE}', + self::REMEDIATION_A, + 16020 + ), + new Issue( + self::CommentObjectInClassConstantType, + self::CATEGORY_COMMENT, + self::SEVERITY_LOW, + "Impossible phpdoc declaration that a class constant {CONST} has a type {TYPE} containing objects. This type is ignored during analysis.", + self::REMEDIATION_B, + 16021 + ), + ]; + // phpcs:enable Generic.Files.LineLength + + self::sanityCheckErrorList($error_list); + // Verified the error meets preconditions, now add it. + $error_map = []; + foreach ($error_list as $error) { + $error_type = $error->getType(); + $error_map[$error_type] = $error; + } + + return $error_map; + } + + /** + * @param list $issue_list the declared Issue types + */ + private static function getNextTypeId(array $issue_list, int $invalid_type_id): int + { + for ($id = $invalid_type_id + 1; true; $id++) { + foreach ($issue_list as $error) { + if ($error->getTypeId() === $id) { + continue 2; + } + } + return $id; + } + } + + /** + * @param list $error_list + */ + private static function sanityCheckErrorList(array $error_list): void + { + $error_map = []; + $unique_type_id_set = []; + foreach ($error_list as $error) { + $error_type = $error->getType(); + if (\array_key_exists($error_type, $error_map)) { + throw new AssertionError("Issue of type $error_type has multiple definitions"); + } + + if (\strncmp($error_type, 'Phan', 4) !== 0) { + throw new AssertionError("Issue of type $error_type should begin with 'Phan'"); + } + + $error_type_id = $error->getTypeId(); + if (\array_key_exists($error_type_id, $unique_type_id_set)) { + throw new AssertionError("Multiple issues exist with pylint error id $error_type_id - The next available id is " . + self::getNextTypeId($error_list, $error_type_id)); + } + $unique_type_id_set[$error_type_id] = $error; + $category = $error->getCategory(); + $expected_category_for_type_id_bitpos = (int)\floor($error_type_id / 1000); + $expected_category_for_type_id = 1 << $expected_category_for_type_id_bitpos; + if ($category !== $expected_category_for_type_id) { + throw new AssertionError(\sprintf( + "Expected error %s of type %d to be category %d(1<<%d), got 1<<%d\n", + $error_type, + $error_type_id, + $category, + (int)\round(\log($category, 2)), + $expected_category_for_type_id_bitpos + )); + } + $error_map[$error_type] = $error; + } + } + + /** + * Returns the type name of this issue (e.g. Issue::UndeclaredVariable) + */ + public function getType(): string + { + return $this->type; + } + + /** + * @return int (Unique integer code corresponding to getType()) + */ + public function getTypeId(): int + { + return $this->type_id; + } + + /** + * Returns the category of this issue (e.g. Issue::CATEGORY_UNDEFINED) + */ + public function getCategory(): int + { + return $this->category; + } + + /** + * @return string + * The name of this issue's category + */ + public function getCategoryName(): string + { + return self::getNameForCategory($this->category); + } + + /** + * @return string + * The name of the category + */ + public static function getNameForCategory(int $category): string + { + return self::CATEGORY_NAME[$category] ?? ''; + } + + /** + * Returns the severity of this issue (Issue::SEVERITY_LOW, Issue::SEVERITY_NORMAL, or Issue::SEVERITY_CRITICAL) + */ + public function getSeverity(): int + { + return $this->severity; + } + + /** + * @return string + * A descriptive name of the severity of the issue + */ + public function getSeverityName(): string + { + switch ($this->severity) { + case self::SEVERITY_LOW: + return 'low'; + case self::SEVERITY_NORMAL: + return 'normal'; + case self::SEVERITY_CRITICAL: + return 'critical'; + default: + throw new \AssertionError('Unknown severity ' . $this->severity); + } + } + + /** + * @suppress PhanUnreferencedPublicMethod (no reporters use this right now) + */ + public function getRemediationDifficulty(): int + { + return $this->remediation_difficulty; + } + + /** + * Returns the template text of this issue (e.g. `'Variable ${VARIABLE} is undeclared'`) + */ + public function getTemplate(): string + { + return $this->template; + } + + /** + * Returns the number of arguments expected for the format string $this->getTemplate() + * @suppress PhanAccessReadOnlyProperty lazily computed + */ + public function getExpectedArgumentCount(): int + { + return $this->argument_count ?? $this->argument_count = ConversionSpec::computeExpectedArgumentCount($this->template); + } + + /** + * @return string - template with the information needed to colorize this. + */ + public function getTemplateRaw(): string + { + return $this->template_raw; + } + + /** + * @param list $template_parameters + * @return IssueInstance + */ + public function __invoke( + string $file, + int $line, + array $template_parameters = [], + Suggestion $suggestion = null, + int $column = 0 + ): IssueInstance { + // TODO: Add callable to expanded union types instead + return new IssueInstance( + $this, + $file, + $line, + $template_parameters, + $suggestion, + $column + ); + } + + /** + * @throws InvalidArgumentException + * @suppress PhanPluginRemoveDebugCall this is deliberate + */ + public static function fromType(string $type): Issue + { + $error_map = self::issueMap(); + + if (!isset($error_map[$type])) { + // Print a verbose error so that this isn't silently caught. + \fwrite(\STDERR, "Saw undefined error type $type\n"); + \ob_start(); + \debug_print_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); + \fwrite(\STDERR, \rtrim(\ob_get_clean() ?: "failed to dump backtrace") . \PHP_EOL); + throw new InvalidArgumentException("Undefined error type $type"); + } + + return $error_map[$type]; + } + + /** + * @param string $type + * The type of the issue + * + * @param string $file + * The name of the file where the issue was found + * + * @param int $line + * The line number (start) where the issue was found + * + * @param string|int|float|bool|Type|UnionType|FQSEN|TypedElement|UnaddressableTypedElement ...$template_parameters + * Any template parameters required for the issue + * message + * @suppress PhanUnreferencedPublicMethod + */ + public static function emit( + string $type, + string $file, + int $line, + ...$template_parameters + ): void { + self::emitWithParameters( + $type, + $file, + $line, + $template_parameters + ); + } + + /** + * @param string $type + * The type of the issue + * + * @param string $file + * The name of the file where the issue was found + * + * @param int $line + * The line number (start) where the issue was found + * + * @param list $template_parameters + * Any template parameters required for the issue + * message + * + * @param ?Suggestion $suggestion (optional details on fixing this) + */ + public static function emitWithParameters( + string $type, + string $file, + int $line, + array $template_parameters, + Suggestion $suggestion = null, + int $column = 0 + ): void { + $issue = self::fromType($type); + + self::emitInstance( + $issue($file, $line, $template_parameters, $suggestion, $column) + ); + } + + public const TRACE_BASIC = 'basic'; + public const TRACE_VERBOSE = 'verbose'; + + /** + * @param IssueInstance $issue_instance + * An issue instance to emit + */ + public static function emitInstance( + IssueInstance $issue_instance + ): void { + if (Phan::isExcludedAnalysisFile($issue_instance->getFile())) { + return; + } + if (ConfigPluginSet::instance()->onEmitIssue($issue_instance)) { + return; + } + Phan::getIssueCollector()->collectIssue($issue_instance); + } + + /** + * @param CodeBase $code_base + * The code base within which we're operating + * + * @param Context $context + * The context in which the instance was found + * + * @param IssueInstance $issue_instance + * An issue instance to emit + */ + public static function maybeEmitInstance( + CodeBase $code_base, + Context $context, + IssueInstance $issue_instance + ): void { + // If this issue type has been suppressed in + // the config, ignore it + + $issue = $issue_instance->getIssue(); + if (self::shouldSuppressIssue( + $code_base, + $context, + $issue->getType(), + $issue_instance->getLine(), + $issue_instance->getTemplateParameters(), + $issue_instance->getSuggestion() + )) { + return; + } + + self::emitInstance($issue_instance); + } + + /** + * @param CodeBase $code_base + * The code base within which we're operating + * + * @param Context $context + * The context in which the node we're going to be looking + * at exists. + * + * @param string $issue_type + * The type of issue to emit such as Issue::ParentlessClass + * + * @param int $lineno + * The line number where the issue was found + * + * @param string|int|float|bool|Type|UnionType|FQSEN|TypedElement|UnaddressableTypedElement ...$parameters + * Template parameters for the issue's error message. + * If these are objects, they should define __toString() + */ + public static function maybeEmit( + CodeBase $code_base, + Context $context, + string $issue_type, + int $lineno, + ...$parameters + ): void { + self::maybeEmitWithParameters( + $code_base, + $context, + $issue_type, + $lineno, + $parameters + ); + } + + /** + * @param CodeBase $code_base + * The code base within which we're operating + * + * @param Context $context + * The context in which the node we're going to be looking + * at exists. + * + * @param string $issue_type + * The type of issue to emit such as Issue::ParentlessClass + * + * @param int $lineno + * The line number where the issue was found + * + * @param list $parameters + * @param ?Suggestion $suggestion (optional) + * + * Template parameters for the issue's error message + */ + public static function maybeEmitWithParameters( + CodeBase $code_base, + Context $context, + string $issue_type, + int $lineno, + array $parameters, + Suggestion $suggestion = null, + int $column = 0 + ): void { + if (self::shouldSuppressIssue( + $code_base, + $context, + $issue_type, + $lineno, + $parameters, + $suggestion + )) { + return; + } + + Issue::emitWithParameters( + $issue_type, + $context->getFile(), + $lineno, + $parameters, + $suggestion, + $column + ); + } + + /** + * @param list $parameters + */ + public static function shouldSuppressIssue( + CodeBase $code_base, + Context $context, + string $issue_type, + int $lineno, + array $parameters, + Suggestion $suggestion = null + ): bool { + if (Config::getValue('disable_suppression')) { + return false; + } + // If this issue type has been suppressed in + // the config, ignore it + if (\in_array($issue_type, Config::getValue('suppress_issue_types') ?? [], true)) { + return true; + } + // If a white-list of allowed issue types is defined, + // only emit issues on the white-list + $whitelist_issue_types = Config::getValue('whitelist_issue_types') ?? []; + if (\count($whitelist_issue_types) > 0 && + !\in_array($issue_type, $whitelist_issue_types, true)) { + return true; + } + + if ($context->hasSuppressIssue($code_base, $issue_type)) { + return true; + } + + return ConfigPluginSet::instance()->shouldSuppressIssue( + $code_base, + $context, + $issue_type, + $lineno, + $parameters, + $suggestion + ); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/IssueFixSuggester.php b/bundled-libs/phan/phan/src/Phan/IssueFixSuggester.php new file mode 100644 index 000000000..1fd9f55b2 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/IssueFixSuggester.php @@ -0,0 +1,774 @@ +hasClassWithFQSEN($alternate_fqsen)) { + return false; + } + return $class_closure($code_base->getClassByFQSEN($alternate_fqsen)); + }; + } + + /** + * @return Closure(FullyQualifiedClassName):bool + */ + public static function createFQSENFilterForClasslikeCategories(CodeBase $code_base, bool $allow_class, bool $allow_trait, bool $allow_interface): Closure + { + return self::createFQSENFilterFromClassFilter($code_base, static function (Clazz $class) use ($allow_class, $allow_trait, $allow_interface): bool { + if ($class->isTrait()) { + return $allow_trait; + } elseif ($class->isInterface()) { + return $allow_interface; + } else { + return $allow_class; + } + }); + } + + /** + * Returns a suggestion that suggests a similarly spelled class that has a method with name $method_name + * (Used when trying to invoke a method on a class that does not exist) + */ + public static function suggestSimilarClassForMethod(CodeBase $code_base, Context $context, FullyQualifiedClassName $class_fqsen, string $method_name, bool $is_static): ?Suggestion + { + $filter = null; + if (strtolower($method_name) === '__construct') { + // Constructed objects have to be classes + $filter = self::createFQSENFilterForClasslikeCategories($code_base, true, false, false); + } elseif ($is_static) { + // Static methods can be parts of classes or traits, but not interfaces + $filter = self::createFQSENFilterForClasslikeCategories($code_base, true, true, false); + } + return self::suggestSimilarClass($code_base, $context, $class_fqsen, $filter); + } + + /** + * Returns a suggestion for a global function spelled similarly to $function_fqsen (e.g. if $function_fqsen does not exist) + */ + public static function suggestSimilarGlobalFunction( + CodeBase $code_base, + Context $context, + FullyQualifiedFunctionName $function_fqsen, + bool $suggest_in_global_namespace = true, + string $prefix = "" + ): ?Suggestion { + if ($prefix === '') { + $prefix = self::DEFAULT_FUNCTION_SUGGESTION_PREFIX; + } + $namespace = $function_fqsen->getNamespace(); + $name = $function_fqsen->getName(); + $suggested_fqsens = \array_merge( + $code_base->suggestSimilarGlobalFunctionInOtherNamespace($namespace, $name, $context), + $code_base->suggestSimilarGlobalFunctionInSameNamespace($namespace, $name, $context, $suggest_in_global_namespace), + $code_base->suggestSimilarNewInAnyNamespace($namespace, $name, $context, $suggest_in_global_namespace), + $code_base->suggestSimilarGlobalFunctionInNewerVersion($namespace, $name, $context, $suggest_in_global_namespace) + ); + if (count($suggested_fqsens) === 0) { + return null; + } + + /** + * @param string|FullyQualifiedFunctionName|FullyQualifiedClassName $fqsen + */ + $generate_type_representation = static function ($fqsen): string { + if ($fqsen instanceof FullyQualifiedClassName) { + return "new $fqsen()"; + } + if (is_string($fqsen) && strpos($fqsen, 'added in PHP') !== false) { + return $fqsen; + } + return $fqsen . '()'; + }; + $suggestion_text = $prefix . ' ' . \implode(' or ', \array_map($generate_type_representation, $suggested_fqsens)); + + return Suggestion::fromString($suggestion_text); + } + + public const DEFAULT_CLASS_SUGGESTION_PREFIX = 'Did you mean'; + public const DEFAULT_FUNCTION_SUGGESTION_PREFIX = 'Did you mean'; + + public const CLASS_SUGGEST_ONLY_CLASSES = 0; + public const CLASS_SUGGEST_CLASSES_AND_TYPES = 1; + public const CLASS_SUGGEST_CLASSES_AND_TYPES_AND_VOID = 2; + + /** + * Returns a message suggesting a class name that is similar to the provided undeclared class + * + * @param ?Closure(FullyQualifiedClassName):bool $filter + * @param int $class_suggest_type whether to include non-classes such as 'int', 'callable', etc. + */ + public static function suggestSimilarClass( + CodeBase $code_base, + Context $context, + FullyQualifiedClassName $class_fqsen, + ?Closure $filter = null, + string $prefix = null, + int $class_suggest_type = self::CLASS_SUGGEST_ONLY_CLASSES + ): ?Suggestion { + if (!is_string($prefix) || $prefix === '') { + $prefix = self::DEFAULT_CLASS_SUGGESTION_PREFIX; + } + $suggested_fqsens = \array_merge( + $code_base->suggestSimilarClassInOtherNamespace($class_fqsen, $context), + $code_base->suggestSimilarClassInSameNamespace($class_fqsen, $context, $class_suggest_type) + ); + if ($filter) { + $suggested_fqsens = \array_filter($suggested_fqsens, $filter); + } + $suggested_fqsens = \array_merge(self::suggestStubForClass($class_fqsen), $suggested_fqsens); + + if (count($suggested_fqsens) === 0) { + return null; + } + + /** + * @param FullyQualifiedClassName|string $fqsen + */ + $generate_type_representation = static function ($fqsen) use ($code_base): string { + if (is_string($fqsen)) { + return $fqsen; // Not a class name, e.g. 'int', 'callable', etc. + } + $category = 'classlike'; + if ($code_base->hasClassWithFQSEN($fqsen)) { + $class = $code_base->getClassByFQSEN($fqsen); + if ($class->isInterface()) { + $category = 'interface'; + } elseif ($class->isTrait()) { + $category = 'trait'; + } else { + $category = 'class'; + } + } + return $category . ' ' . $fqsen->__toString(); + }; + $suggestion_text = $prefix . ' ' . \implode(' or ', \array_map($generate_type_representation, $suggested_fqsens)); + + return Suggestion::fromString($suggestion_text); + } + + /** + * @return array{0?:string} an optional suggestion to enable internal stubs to load that class + */ + public static function suggestStubForClass(FullyQualifiedClassName $fqsen): array + { + $class_key = \strtolower(\ltrim($fqsen->__toString(), '\\')); + if (\array_key_exists($class_key, self::getKnownClasses())) { + // Generate the message 'Did you mean "to configure..."' + $message = "to configure a stub with https://github.com/phan/phan/wiki/How-To-Use-Stubs#internal-stubs or to enable the extension providing the class."; + $included_extension_subset = Config::getValue('included_extension_subset'); + if (is_array($included_extension_subset) || Config::getValue('autoload_internal_extension_signatures')) { + $message .= " (are the config settings 'included_extension_subset' and/or 'autoload_internal_extension_signatures' properly set up?)"; + } + return [$message]; + } + return []; + } + + /** + * @return array fetches an incomplete list of classes Phan has known signatures/documentation for + */ + private static function getKnownClasses(): array + { + static $known_classes = null; + if (!is_array($known_classes)) { + $known_classes = MarkupDescription::loadClassDescriptionMap(); + foreach (UnionType::internalFunctionSignatureMap(Config::get_closest_target_php_version_id()) as $fqsen => $_) { + $i = strpos($fqsen, '::'); + if ($i === false) { + continue; + } + $fqsen = strtolower(\substr($fqsen, 0, $i)); + $known_classes[$fqsen] = true; + } + } + return $known_classes; + } + + /** + * Returns a suggestion with similar method names to $wanted_method_name in $class (that exist and are accessible from the usage context), or null. + */ + public static function suggestSimilarMethod(CodeBase $code_base, Context $context, Clazz $class, string $wanted_method_name, bool $is_static): ?Suggestion + { + if (Config::getValue('disable_suggestions')) { + return null; + } + $method_set = self::suggestSimilarMethodMap($code_base, $context, $class, $wanted_method_name, $is_static); + if (count($method_set) === 0) { + return null; + } + uksort($method_set, 'strcmp'); + $suggestions = []; + foreach ($method_set as $method) { + // We lose the original casing of the method name in the array keys, so use $method->getName() + $prefix = $method->isStatic() ? 'expr::' : 'expr->' ; + $suggestions[] = $prefix . $method->getName() . '()'; + } + return Suggestion::fromString( + 'Did you mean ' . \implode(' or ', $suggestions) + ); + } + + /** + * @return array + */ + public static function suggestSimilarMethodMap(CodeBase $code_base, Context $context, Clazz $class, string $wanted_method_name, bool $is_static): array + { + $methods = $class->getMethodMap($code_base); + if (count($methods) > Config::getValue('suggestion_check_limit')) { + return []; + } + $usable_methods = self::filterSimilarMethods($code_base, $context, $methods, $is_static); + return self::getSuggestionsForStringSet($wanted_method_name, $usable_methods); + } + + /** + * @internal + */ + public static function maybeGetClassInCurrentScope(Context $context): ?FullyQualifiedClassName + { + if ($context->isInClassScope()) { + return $context->getClassFQSEN(); + } + return null; + } + + /** + * @param array $methods a list of methods + * @return array a subset of the methods in $methods that are accessible from the current scope. + * @internal + */ + public static function filterSimilarMethods(CodeBase $code_base, Context $context, array $methods, bool $is_static): array + { + $class_fqsen_in_current_scope = self::maybeGetClassInCurrentScope($context); + + $candidates = []; + foreach ($methods as $method_name => $method) { + if ($is_static && !$method->isStatic()) { + // Don't suggest instance methods to replace static methods + continue; + } + if (!$method->isAccessibleFromClass($code_base, $class_fqsen_in_current_scope)) { + // Don't suggest inaccessible private or protected methods. + continue; + } + $candidates[$method_name] = $method; + } + return $candidates; + } + + /** + * @param ?\Closure(FullyQualifiedClassName):bool $filter + */ + public static function suggestSimilarClassForGenericFQSEN(CodeBase $code_base, Context $context, FQSEN $fqsen, ?Closure $filter = null, string $prefix = 'Did you mean'): ?Suggestion + { + if (Config::getValue('disable_suggestions')) { + return null; + } + if (!($fqsen instanceof FullyQualifiedClassName)) { + return null; + } + return self::suggestSimilarClass($code_base, $context, $fqsen, $filter, $prefix); + } + + /** + * Generate a suggestion with similar suggestions (properties or otherwise) to a missing property with class $class and name $wanted_property_name. + */ + public static function suggestSimilarProperty(CodeBase $code_base, Context $context, Clazz $class, string $wanted_property_name, bool $is_static): ?Suggestion + { + if (Config::getValue('disable_suggestions')) { + return null; + } + if (strlen($wanted_property_name) <= 1) { + return null; + } + $property_set = self::suggestSimilarPropertyMap($code_base, $context, $class, $wanted_property_name, $is_static); + $suggestions = []; + if ($is_static) { + if ($class->hasConstantWithName($code_base, $wanted_property_name)) { + $suggestions[] = $class->getFQSEN() . '::' . $wanted_property_name; + } + } + if ($class->hasMethodWithName($code_base, $wanted_property_name, true)) { + $method = $class->getMethodByName($code_base, $wanted_property_name); + $suggestions[] = $class->getFQSEN() . ($method->isStatic() ? '::' : '->') . $wanted_property_name . '()'; + } + foreach ($property_set as $property_name => $_) { + $prefix = $is_static ? 'expr::$' : 'expr->' ; + $suggestions[] = $prefix . $property_name; + } + foreach (self::getVariableNamesInScopeWithSimilarName($context, $wanted_property_name) as $variable_name) { + $suggestions[] = $variable_name; + } + + if (count($suggestions) === 0) { + return null; + } + uksort($suggestions, 'strcmp'); + return Suggestion::fromString( + 'Did you mean ' . \implode(' or ', $suggestions) + ); + } + + /** + * @return array + */ + public static function suggestSimilarPropertyMap(CodeBase $code_base, Context $context, Clazz $class, string $wanted_property_name, bool $is_static): array + { + $property_map = $class->getPropertyMap($code_base); + if (count($property_map) > Config::getValue('suggestion_check_limit')) { + return []; + } + $usable_property_map = self::filterSimilarProperties($code_base, $context, $property_map, $is_static); + return self::getSuggestionsForStringSet($wanted_property_name, $usable_property_map); + } + + /** + * @param array $property_map + * @return array a subset of $property_map that is accessible from the current scope. + * @internal + */ + public static function filterSimilarProperties(CodeBase $code_base, Context $context, array $property_map, bool $is_static): array + { + $class_fqsen_in_current_scope = self::maybeGetClassInCurrentScope($context); + $candidates = []; + foreach ($property_map as $property_name => $property) { + if ($is_static !== $property->isStatic()) { + // Don't suggest instance properties to replace static properties + continue; + } + if (!$property->isAccessibleFromClass($code_base, $class_fqsen_in_current_scope)) { + // Don't suggest inaccessible private or protected properties. + continue; + } + // TODO: Check for access to protected outside of a class + if ($property->isDynamicProperty()) { + // Skip dynamically added properties + continue; + } + $candidates[$property_name] = $property; + } + return $candidates; + } + + /** + * Returns a suggestion with class constants with a similar name to $class_constant_fqsen in the same class, or null + */ + public static function suggestSimilarClassConstant(CodeBase $code_base, Context $context, FullyQualifiedClassConstantName $class_constant_fqsen): ?Suggestion + { + if (Config::getValue('disable_suggestions')) { + return null; + } + $constant_name = $class_constant_fqsen->getName(); + if (strlen($constant_name) <= 1) { + return null; + } + $class_fqsen = $class_constant_fqsen->getFullyQualifiedClassName(); + if (!$code_base->hasClassWithFQSEN($class_fqsen)) { + return null; + } + $class = $code_base->getClassByFQSEN($class_fqsen); + $class_constant_map = self::suggestSimilarClassConstantMap($code_base, $context, $class, $constant_name); + $property_map = self::suggestSimilarPropertyMap($code_base, $context, $class, $constant_name, true); + $method_map = self::suggestSimilarMethodMap($code_base, $context, $class, $constant_name, true); + if (count($class_constant_map) + count($property_map) + count($method_map) === 0) { + return null; + } + uksort($class_constant_map, 'strcmp'); + uksort($property_map, 'strcmp'); + uksort($method_map, 'strcmp'); + $suggestions = []; + foreach ($class_constant_map as $constant_name => $_) { + $suggestions[] = $class_fqsen . '::' . $constant_name; + } + foreach ($property_map as $property_name => $_) { + $suggestions[] = $class_fqsen . '::$' . $property_name; + } + foreach ($method_map as $method) { + $suggestions[] = $class_fqsen . '::' . $method->getName() . '()'; + } + return Suggestion::fromString( + 'Did you mean ' . \implode(' or ', $suggestions) + ); + } + + /** + * @return ?Suggestion with values similar to the given constant + */ + public static function suggestSimilarGlobalConstant(CodeBase $code_base, Context $context, FullyQualifiedGlobalConstantName $fqsen): ?Suggestion + { + if (Config::getValue('disable_suggestions')) { + return null; + } + $constant_name = $fqsen->getName(); + if (strlen($constant_name) <= 1) { + return null; + } + $namespace = $fqsen->getNamespace(); + $suggestions = \array_merge( + self::suggestSimilarFunctionsToConstant($code_base, $context, $fqsen), + self::suggestSimilarClassConstantsToGlobalConstant($code_base, $context, $fqsen), + self::suggestSimilarClassPropertiesToGlobalConstant($code_base, $context, $fqsen), + $code_base->suggestSimilarConstantsToConstant($constant_name), + $code_base->suggestSimilarGlobalConstantForNamespaceAndName($namespace, $constant_name), + $namespace !== '\\' ? $code_base->suggestSimilarGlobalConstantForNamespaceAndName('\\', $constant_name) : [], + self::suggestSimilarVariablesToGlobalConstant($context, $fqsen) + ); + if (count($suggestions) === 0) { + return null; + } + $suggestions = self::deduplicateSuggestions(\array_map('strval', $suggestions)); + \sort($suggestions, \SORT_STRING); + return Suggestion::fromString( + 'Did you mean ' . \implode(' or ', $suggestions) + ); + } + + /** + * @template T + * @param T[] $suggestions + * @return list + */ + private static function deduplicateSuggestions(array $suggestions): array + { + $result = []; + foreach ($suggestions as $suggestion) { + $result[(string)$suggestion] = $suggestion; + } + return \array_values($result); + } + + /** + * @return list + */ + private static function suggestSimilarFunctionsToConstant(CodeBase $code_base, Context $context, FullyQualifiedGlobalConstantName $fqsen): array + { + $suggested_fqsens = $code_base->suggestSimilarGlobalFunctionInOtherNamespace( + $fqsen->getNamespace(), + $fqsen->getName(), + $context, + true + ); + return \array_map(static function (FullyQualifiedFunctionName $fqsen): string { + return $fqsen . '()'; + }, $suggested_fqsens); + } + + /** + * Suggests accessible class constants of the current class that are similar to the passed in global constant FQSEN + * @return list + */ + private static function suggestSimilarClassConstantsToGlobalConstant(CodeBase $code_base, Context $context, FullyQualifiedGlobalConstantName $fqsen): array + { + if (!$context->isInClassScope()) { + return []; + } + if (\ltrim($fqsen->getNamespace(), '\\') !== '') { + return []; + } + try { + $class = $context->getClassInScope($code_base); + $name = $fqsen->getName(); + if ($class->hasConstantWithName($code_base, $name)) { + return ["self::$name"]; + } + } catch (\Exception $_) { + // ignore + } + return []; + } + + /** + * Suggests accessible class properties of the current class that are similar to the passed in global constant FQSEN + * @return list + */ + private static function suggestSimilarClassPropertiesToGlobalConstant(CodeBase $code_base, Context $context, FullyQualifiedGlobalConstantName $fqsen): array + { + if (!$context->isInClassScope()) { + return []; + } + if (\ltrim($fqsen->getNamespace(), '\\') !== '') { + return []; + } + $name = $fqsen->getName(); + try { + $class = $context->getClassInScope($code_base); + if (!$class->hasPropertyWithName($code_base, $name)) { + return []; + } + $property = $class->getPropertyByName($code_base, $name); + if (!$property->isAccessibleFromClass($code_base, $class->getFQSEN())) { + return []; + } + if ($property->isStatic()) { + return ['self::$' . $name]; + } elseif ($context->isInFunctionLikeScope()) { + $current_function = $context->getFunctionLikeInScope($code_base); + if (!$current_function->isStatic()) { + return ['$this->' . $name]; + } + } + } catch (\Exception $_) { + // ignore + } + return []; + } + + /** + * @return list returns array variable names prefixed with '$' with a similar name, or an empty array if that wouldn't make sense or there would be too many suggestions + */ + private static function suggestSimilarVariablesToGlobalConstant(Context $context, FullyQualifiedGlobalConstantName $fqsen): array + { + if ($context->isInGlobalScope()) { + return []; + } + if (\ltrim($fqsen->getNamespace(), '\\') !== '') { + // Give up if requesting a namespaced constant + // TODO: Better heuristics + return []; + } + return self::getVariableNamesInScopeWithSimilarName($context, $fqsen->getName()); + } + + /** + * @return array + */ + private static function suggestSimilarClassConstantMap(CodeBase $code_base, Context $context, Clazz $class, string $constant_name): array + { + $constant_map = $class->getConstantMap($code_base); + if (count($constant_map) > Config::getValue('suggestion_check_limit')) { + return []; + } + $usable_constant_map = self::filterSimilarConstants($code_base, $context, $constant_map); + $result = self::getSuggestionsForStringSet($constant_name, $usable_constant_map); + return $result; + } + + /** + * @param array $constant_map + * @return array a subset of those class constants that are accessible from the current scope. + * @internal + */ + public static function filterSimilarConstants(CodeBase $code_base, Context $context, array $constant_map): array + { + $class_fqsen_in_current_scope = self::maybeGetClassInCurrentScope($context); + + $candidates = []; + foreach ($constant_map as $constant_name => $constant) { + if (!$constant->isAccessibleFromClass($code_base, $class_fqsen_in_current_scope)) { + // Don't suggest inherited private properties + continue; + } + // TODO: Check for access to protected outside of a class + $candidates[$constant_name] = $constant; + } + return $candidates; + } + + /** + * Return a suggestion for variables with similar spellings to $variable_name (which may or may not exist or be used elsewhere), or null. + * + * Suggestions also include accessible properties with similar names (e.g. suggest `$this->context` if `$context` is not declared) + */ + public static function suggestVariableTypoFix(CodeBase $code_base, Context $context, string $variable_name, string $prefix = 'Did you mean'): ?Suggestion + { + if (Config::getValue('disable_suggestions')) { + return null; + } + if ($variable_name === '') { + return null; + } + if (!$context->isInFunctionLikeScope()) { + // Don't bother suggesting globals for now + return null; + } + // Suggest similar variable names in the current scope. + $suggestions = self::getVariableNamesInScopeWithSimilarName($context, $variable_name); + // Suggest instance or static properties of the same name, if accessible + if ($context->isInClassScope()) { + // TODO: Does this need to check for static closures + $class_in_scope = $context->getClassInScope($code_base); + if ($class_in_scope->hasPropertyWithName($code_base, $variable_name)) { + $property = $class_in_scope->getPropertyByName($code_base, $variable_name); + if (self::shouldSuggestProperty($context, $class_in_scope, $property)) { + if ($property->isStatic()) { + $suggestions[] = 'self::$' . $variable_name; + } elseif ($context->isInFunctionLikeScope()) { + $current_function = $context->getFunctionLikeInScope($code_base); + if (!$current_function->isStatic()) { + $suggestions[] = '$this->' . $variable_name; + } + } + } + } + } + // Suggest using the variable if it is defined but not used by the current closure. + $scope = $context->getScope(); + $did_suggest_use_in_closure = false; + while ($scope->isInFunctionLikeScope()) { + $function = $context->withScope($scope)->getFunctionLikeInScope($code_base); + if (!($function instanceof Func) || !$function->isClosure()) { + break; + } + $scope = $function->getContext()->getScope()->getParentScope(); + if ($scope->hasVariableWithName($variable_name)) { + $did_suggest_use_in_closure = true; + $suggestions[] = "(use(\$$variable_name) for {$function->getNameForIssue()} at line {$function->getContext()->getLineNumberStart()})"; + break; + } + } + if (!$did_suggest_use_in_closure && Variable::isHardcodedGlobalVariableWithName($variable_name)) { + $suggestions[] = "(global \$$variable_name)"; + } + if (count($suggestions) === 0) { + return null; + } + \sort($suggestions); + + return Suggestion::fromString( + $prefix . ' ' . \implode(' or ', $suggestions) + ); + } + + private static function shouldSuggestProperty(Context $context, Clazz $class_in_scope, Property $property): bool + { + if ($property->isDynamicProperty()) { + // Don't suggest properties that weren't declared. + return false; + } + if ($property->isPrivate() && $property->getDefiningClassFQSEN() !== $class_in_scope->getFQSEN()) { + // Don't suggest inherited private properties that can't be accessed + // - This doesn't need to be checking if the visibility is protected, + // because it's looking for properties of the current class + return false; + } + if ($property->isStatic()) { + if (!$context->getScope()->hasVariableWithName('this')) { + // Don't suggest $this->prop from a static method or a static closure. + return false; + } + } + return true; + } + + /** + * @return list Suggestions for variable names, prefixed with "$" + */ + private static function getVariableNamesInScopeWithSimilarName(Context $context, string $variable_name): array + { + $suggestions = []; + $variable_candidates = $context->getScope()->getVariableMap(); + if (count($variable_candidates) <= Config::getValue('suggestion_check_limit')) { + $variable_candidates = \array_merge($variable_candidates, Variable::_BUILTIN_SUPERGLOBAL_TYPES); + $variable_suggestions = self::getSuggestionsForStringSet($variable_name, $variable_candidates); + + foreach ($variable_suggestions as $suggested_variable_name => $_) { + $suggestions[] = '$' . $suggested_variable_name; + } + } + return $suggestions; + } + /** + * A very simple way to get the closest case-insensitive string matches. + * + * @param array $potential_candidates + * @return array a subset of $potential_candidates + */ + public static function getSuggestionsForStringSet(string $target, array $potential_candidates): array + { + if (count($potential_candidates) === 0) { + return []; + } + $search_name = strtolower($target); + $target_length = strlen($search_name); + if ($target_length > self::MAX_SUGGESTION_NAME_LENGTH) { + return []; + } + $max_levenshtein_distance = (int)(1 + strlen($search_name) / 6); + $best_matches = []; + $min_found_distance = $max_levenshtein_distance; + + foreach ($potential_candidates as $name => $value) { + $name = (string)$name; + if (\strncasecmp($name, $target, $target_length) === 0) { + // If this has $target as a case-insensitive prefix, then treat it as a fairly good match + // (included with single-character edits) + $distance = $target_length !== strlen($name) ? 1 : 0; + } elseif ($target_length >= 1) { + if (strlen($name) > self::MAX_SUGGESTION_NAME_LENGTH || \abs(strlen($name) - $target_length) > $max_levenshtein_distance) { + continue; + } + $distance = \levenshtein(strtolower($name), $search_name); + } else { + continue; + } + if ($distance <= $min_found_distance) { + if ($distance < $min_found_distance) { + $min_found_distance = $distance; + $best_matches = []; + } + $best_matches[$name] = $value; + } + } + return $best_matches; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/IssueInstance.php b/bundled-libs/phan/phan/src/Phan/IssueInstance.php new file mode 100644 index 000000000..0bc346f33 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/IssueInstance.php @@ -0,0 +1,233 @@ + $template_parameters If this is non-null, this contains the arguments emitted for this instance of the issue. */ + private $template_parameters; + + /** + * @param Issue $issue + * @param string $file + * @param int $line + * @param list $template_parameters + * @param ?Suggestion $suggestion + * @param int $column + * @suppress PhanPluginRemoveDebugAny + */ + public function __construct( + Issue $issue, + string $file, + int $line, + array $template_parameters, + Suggestion $suggestion = null, + int $column = 0 + ) { + $this->issue = $issue; + $this->file = $file; + $this->line = $line; + $this->column = $column; + $this->suggestion = $suggestion; + + if ($issue->getExpectedArgumentCount() !== \count($template_parameters)) { + CLI::printWarningToStderr( + \sprintf("Unexpected argument count for %s('%s'): Expected %d args, got %d\n", $issue->getType(), $issue->getTemplate(), $issue->getExpectedArgumentCount(), \count($template_parameters)) + ); + \ob_start(); + \debug_print_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); + \fwrite(\STDERR, (string)\ob_get_clean()); + } + // color_issue_message will interfere with some formatters, such as xml. + if (Config::getValue('color_issue_messages')) { + $this->message = self::generateColorizedMessage($issue, $template_parameters); + } else { + $this->message = self::generatePlainMessage($issue, $template_parameters); + } + // The terminal color codes are valid utf-8 (all control code bytes <= 127) + $this->message = StringUtil::asSingleLineUtf8($this->message); + + // Fixes #1754 : Some issue template parameters might not be serializable (for passing to ForkPool) + + /** + * @param string|float|int|FQSEN|Type|UnionType|TypedElementInterface|UnaddressableTypedElement $parameter + * @return string|float|int + */ + $this->template_parameters = \array_map(static function ($parameter) { + if (\is_object($parameter)) { + return (string)$parameter; + } + return $parameter; + }, $template_parameters); + } + + /** + * @param list $template_parameters + */ + private static function generatePlainMessage( + Issue $issue, + array $template_parameters + ): string { + $template = $issue->getTemplate(); + + // markdown_issue_messages doesn't make sense with color, unless you add msg + // Not sure if codeclimate supports that. + if (Config::getValue('markdown_issue_messages')) { + $template = \preg_replace( + '/([^ ]*%s[^ ]*)/', + '`\1`', + $template + ); + } + // @phan-suppress-next-line PhanPluginPrintfVariableFormatString the template is provided by Phan/its plugins + return \vsprintf( + $template, + self::normalizeTemplateParameters($template_parameters) + ); + } + + /** + * @param list $template_parameters + */ + private static function generateColorizedMessage( + Issue $issue, + array $template_parameters + ): string { + $template = $issue->getTemplateRaw(); + + $result = Colorizing::colorizeTemplate($template, self::normalizeTemplateParameters($template_parameters)); + return $result; + } + + /** + * @param list $template_parameters + * @return list + */ + private static function normalizeTemplateParameters(array $template_parameters): array + { + foreach ($template_parameters as $i => $parameter) { + if ($parameter instanceof UnionType) { + $parameter = $parameter->__toString(); + if ($parameter === '') { + $parameter = '(empty union type)'; + } + $template_parameters[$i] = $parameter; + } + } + return $template_parameters; + } + + /** + * @return ?Suggestion If this is non-null, this contains suggestions on how to resolve the error. + */ + public function getSuggestion(): ?Suggestion + { + return $this->suggestion; + } + + /** @return list $template_parameters */ + public function getTemplateParameters(): array + { + return $this->template_parameters; + } + + public function getSuggestionMessage(): ?string + { + if (!$this->suggestion) { + return null; + } + $text = $this->suggestion->getMessage(); + if (!StringUtil::isNonZeroLengthString($text)) { + return null; + } + return StringUtil::asSingleLineUtf8($text); + } + + public function getIssue(): Issue + { + return $this->issue; + } + + public function getFile(): string + { + return $this->file; + } + + public function getDisplayedFile(): string + { + if (Config::getValue('absolute_path_issue_messages')) { + return Config::projectPath($this->file); + } + return $this->file; + } + + public function getLine(): int + { + return $this->line; + } + + /** + * @return int the 1-based column, or 0 if the column is unknown. + */ + public function getColumn(): int + { + return $this->column; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getMessageAndMaybeSuggestion(): string + { + $message = $this->message; + $suggestion = $this->getSuggestionMessage(); + if (StringUtil::isNonZeroLengthString($suggestion)) { + return $message . ' (' . $suggestion . ')'; + } + return $message; + } + + public function __toString(): string + { + return "{$this->file}:{$this->line} {$this->getMessageAndMaybeSuggestion()}"; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/AnnotatedUnionType.php b/bundled-libs/phan/phan/src/Phan/Language/AnnotatedUnionType.php new file mode 100644 index 000000000..567c4cc47 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/AnnotatedUnionType.php @@ -0,0 +1,286 @@ +is_possibly_undefined === $is_possibly_undefined) { + return $this; + } + if (!$is_possibly_undefined) { + return UnionType::of($this->getTypeSet(), $this->getRealTypeSet()); + } + $result = clone($this); + $result->is_possibly_undefined = $is_possibly_undefined; + return $result; + } + + /** + * @param bool|1 $is_possibly_undefined + * @suppress PhanAccessReadOnlyProperty this is the only way to set is_possibly_undefined + */ + private function withIsPossiblyUndefinedRaw($is_possibly_undefined): UnionType + { + if ($this->is_possibly_undefined === $is_possibly_undefined) { + return $this; + } + if (!$is_possibly_undefined) { + return UnionType::of($this->getTypeSet(), $this->getRealTypeSet()); + } + $result = clone($this); + $result->is_possibly_undefined = $is_possibly_undefined; + return $result; + } + /** + * @override + * @suppress PhanAccessReadOnlyProperty this is the only way to set is_possibly_undefined + */ + public function withIsDefinitelyUndefined(): UnionType + { + if ($this->is_possibly_undefined === self::DEFINITELY_UNDEFINED) { + return $this; + } + $result = clone($this); + $result->is_possibly_undefined = self::DEFINITELY_UNDEFINED; + return $result; + } + + + public function asSingleScalarValueOrNull() + { + if ($this->is_possibly_undefined) { + return null; + } + return parent::asSingleScalarValueOrNull(); + } + + public function asSingleScalarValueOrNullOrSelf() + { + if ($this->is_possibly_undefined) { + return $this; + } + return parent::asSingleScalarValueOrNullOrSelf(); + } + + public function asValueOrNullOrSelf() + { + if ($this->is_possibly_undefined) { + return $this; + } + return parent::asValueOrNullOrSelf(); + } + + public function isPossiblyUndefined(): bool + { + return (bool) $this->is_possibly_undefined; + } + + public function isDefinitelyUndefined(): bool + { + return $this->is_possibly_undefined === self::DEFINITELY_UNDEFINED; + } + + public function isNull(): bool + { + return $this->is_possibly_undefined === self::DEFINITELY_UNDEFINED || parent::isNull(); + } + + public function isRealTypeNullOrUndefined(): bool + { + return $this->is_possibly_undefined === self::DEFINITELY_UNDEFINED || parent::isRealTypeNullOrUndefined(); + } + + public function __toString(): string + { + $result = parent::__toString(); + if ($this->is_possibly_undefined) { + return $result . '='; + } + return $result; + } + + /** + * Add a type name to the list of types + * @override + */ + public function withType(Type $type): UnionType + { + return parent::withType($type)->withIsPossiblyUndefined(false); + } + + /** + * Remove a type name from the list of types + * @override + */ + public function withoutType(Type $type): UnionType + { + return parent::withoutType($type)->withIsPossiblyUndefined(false); + } + + /** + * Returns a union type which adds the given types to this type + * @override + */ + public function withUnionType(UnionType $union_type): UnionType + { + return parent::withUnionType($union_type)->withIsPossiblyUndefined(false); + } + + /** + * @return bool - True if not empty, not possibly undefined, and at least one type is NullType or nullable. + * XXX consider merging into containsNullable + * @override + */ + public function containsNullableOrUndefined(): bool + { + return $this->is_possibly_undefined || $this->containsNullable(); + } + + /** + * @override + */ + public function containsFalsey(): bool + { + return $this->is_possibly_undefined || parent::containsFalsey(); + } + + /** + * @override + */ + public function generateUniqueId(): string + { + $id = parent::generateUniqueId(); + if ($this->is_possibly_undefined) { + return '(' . $id . ')='; + } + return $id; + } + + /** + * Returns a union type with an empty real type set (including in elements of generic arrays, etc.) + */ + public function eraseRealTypeSetRecursively(): UnionType + { + $type_set = $this->getTypeSet(); + $new_type_set = []; + foreach ($type_set as $type) { + $new_type_set[] = $type->withErasedUnionTypes(); + } + $real_type_set = $this->getRealTypeSet(); + if (!$real_type_set && $new_type_set === $type_set) { + return $this; + } + $result = new AnnotatedUnionType($new_type_set, false, $real_type_set); + // @phan-suppress-next-line PhanAccessReadOnlyProperty + $result->is_possibly_undefined = $this->is_possibly_undefined; + return $result; + } + + public function convertUndefinedToNullable(): UnionType + { + if ($this->is_possibly_undefined) { + return $this->nullableClone()->withIsPossiblyUndefined(false); + } + return $this; + } + + /** + * @override + */ + public function isEqualTo(UnionType $union_type): bool + { + if ($this === $union_type) { + return true; + } + if ($union_type instanceof AnnotatedUnionType) { + if ($this->is_possibly_undefined !== $union_type->is_possibly_undefined) { + return false; + } + } elseif ($this->is_possibly_undefined) { + return false; + } + $type_set = $this->getTypeSet(); + $other_type_set = $union_type->getTypeSet(); + if (\count($type_set) !== \count($other_type_set)) { + return false; + } + foreach ($type_set as $type) { + if (!\in_array($type, $other_type_set, true)) { + return false; + } + } + return true; + } + + public function isIdenticalTo(UnionType $union_type): bool + { + if ($this === $union_type) { + return true; + } + if (!$this->isEqualTo($union_type)) { + return false; + } + $real_type_set = $this->getRealTypeSet(); + $other_real_type_set = $union_type->getRealTypeSet(); + if (\count($real_type_set) !== \count($other_real_type_set)) { + return false; + } + foreach ($real_type_set as $type) { + if (!\in_array($type, $other_real_type_set, true)) { + return false; + } + } + + return true; + } + + /** + * Converts the real part of the union type to a standalone union type + * @override + */ + public function getRealUnionType(): UnionType + { + $real_type_set = $this->getRealTypeSet(); + if ($this->getTypeSet() === $real_type_set) { + return $this; + } + return (new AnnotatedUnionType($real_type_set, true, $real_type_set))->withIsPossiblyUndefinedRaw($this->is_possibly_undefined); + } + + /** + * Converts a phpdoc type into the real union type equivalent. + * @override + */ + public function asRealUnionType(): UnionType + { + $type_set = $this->getTypeSet(); + return (new AnnotatedUnionType($type_set, true, $type_set))->withIsPossiblyUndefinedRaw($this->is_possibly_undefined); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Context.php b/bundled-libs/phan/phan/src/Phan/Language/Context.php new file mode 100644 index 000000000..08f8ca4b1 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Context.php @@ -0,0 +1,1031 @@ +> + * @phan-var associative-array> + * Maps [int flags => [string name/namespace => NamespaceMapEntry(fqsen, is_used)]] + * Note that for \ast\USE_CONST (global constants), this is case-sensitive, + * but the remaining types are case-insensitive (stored with lowercase name). + */ + private $namespace_map = []; + + /** + * @var array> + * @phan-var associative-array> + * Maps [int flags => [string name/namespace => NamespaceMapEntry(fqsen, is_used)]] + * + * (This is used in the analysis phase after the parse phase) + * @see self::$namespace_map + */ + private $parse_namespace_map = []; + + /** + * @var int + * strict_types setting for the file + */ + protected $strict_types = 0; + + /** + * @var list + * A list of nodes of loop statements that have been entered in the current functionlike scope. + */ + protected $loop_nodes = []; + + /** + * @var Scope + * The current scope in this context + */ + private $scope; + + /** + * @var array + * caches union types for a given node + */ + private $cache = []; + + /** + * Create a new context + */ + public function __construct() + { + $this->scope = new GlobalScope(); + } + + /** + * @param string $namespace + * The namespace of the file + * + * @return Context + * A clone of this context with the given value is returned + */ + public function withNamespace(string $namespace): Context + { + $context = clone($this); + $context->namespace = $namespace; + $context->namespace_id += 1; // Assumes namespaces are walked in order + $context->namespace_map = []; + return $context; + } + + /** + * @return string + * The namespace of the file + */ + public function getNamespace(): string + { + return $this->namespace; + } + + /** + * @return int + * The namespace id within the file (incrementing starting from 0) + * Used because a file can have duplicate identical namespace declarations. + */ + public function getNamespaceId(): int + { + return $this->namespace_id; + } + + /** + * @return bool + * True if we have a mapped NS for the given named element + */ + public function hasNamespaceMapFor(int $flags, string $name): bool + { + // Look for the mapping on the part before a + // slash + $name_parts = \explode('\\', $name, 2); + if (\count($name_parts) > 1) { + $name = $name_parts[0]; + // In php, namespaces, functions, and classes are case-insensitive. + // However, constants are almost always case-insensitive. + // The name we're looking for is a namespace(USE_NORMAL). + // The suffix has type $flags + $flags = \ast\flags\USE_NORMAL; + } + if ($flags !== \ast\flags\USE_CONST) { + $name = \strtolower($name); + } + return isset($this->namespace_map[$flags][$name]); + } + + /** + * @return FullyQualifiedGlobalStructuralElement + * The namespace mapped name for the given flags and name + */ + public function getNamespaceMapFor( + int $flags, + string $name + ): FullyQualifiedGlobalStructuralElement { + + // Look for the mapping on the part before a + // slash + $name_parts = \explode('\\', $name, 2); + if (\count($name_parts) > 1) { + $name = $name_parts[0]; + $suffix = $name_parts[1]; + // In php, namespaces, functions, and classes are case-insensitive. + // However, constants are almost always case-insensitive. + // The name we're looking for is a namespace(USE_NORMAL). + // The suffix has type $flags + $map_flags = \ast\flags\USE_NORMAL; + } else { + $suffix = ''; + $map_flags = $flags; + } + if ($map_flags !== \ast\flags\USE_CONST) { + $name_key = \strtolower($name); + } else { + $name_key = $name; + } + + $namespace_map_entry = $this->namespace_map[$map_flags][$name_key] ?? null; + + if (!$namespace_map_entry) { + throw new AssertionError("No namespace defined for name '$name_key'"); + } + $fqsen = $namespace_map_entry->fqsen; + $namespace_map_entry->is_used = true; + + // Create something of the corresponding type (which may or may not be within a suffix) + if (!StringUtil::isNonZeroLengthString($suffix)) { + return $fqsen; + } + + switch ($flags) { + case \ast\flags\USE_NORMAL: + // @phan-suppress-next-line PhanThrowTypeAbsentForCall This and the suffix should have already been validated + return FullyQualifiedClassName::fromFullyQualifiedString( + $fqsen->__toString() . '\\' . $suffix + ); + case \ast\flags\USE_FUNCTION: + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + return FullyQualifiedFunctionName::fromFullyQualifiedString( + $fqsen->__toString() . '\\' . $suffix + ); + case \ast\flags\USE_CONST: + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + return FullyQualifiedGlobalConstantName::fromFullyQualifiedString( + $fqsen->__toString() . '\\' . $suffix + ); + } + + throw new AssertionError("Unknown flag $flags"); + } + + /** + * @return Context + * This context with the given value is returned + * + * TODO: Make code_base mandatory in a subsequent release + */ + public function withNamespaceMap( + int $flags, + string $alias, + FullyQualifiedGlobalStructuralElement $target, + int $lineno, + CodeBase $code_base = null + ): Context { + $original_alias = $alias; + if ($flags !== \ast\flags\USE_CONST) { + $alias = \strtolower($alias); + } else { + $last_part_index = \strrpos($alias, '\\'); + if ($last_part_index !== false) { + // Convert the namespace to lowercase, but not the constant name. + $alias = \strtolower(\substr($alias, 0, $last_part_index + 1)) . \substr($alias, $last_part_index + 1); + } + } + // we may have imported this namespace map from the parse phase, making the target already exist + // TODO: Warn if namespace_map already exists? Then again, `php -l` already does. + $parse_entry = $this->parse_namespace_map[$flags][$alias] ?? null; + if ($parse_entry !== null) { + // We add entries to namespace_map only after encountering them + // This is because statements can appear before 'use Foo\Bar;' (and those don't use the 'use' statement.) + $this->namespace_map[$flags][$alias] = $parse_entry; + return $this; + } + if (isset($this->namespace_map[$flags][$alias])) { + if ($code_base) { + $this->warnDuplicateUse($code_base, $target, $lineno, $flags, $alias); + } + } + $this->namespace_map[$flags][$alias] = new NamespaceMapEntry($target, $original_alias, $lineno); + return $this; + } + + private function warnDuplicateUse(CodeBase $code_base, FullyQualifiedGlobalStructuralElement $target, int $lineno, int $flags, string $alias): void + { + switch ($flags) { + case \ast\flags\USE_FUNCTION: + $issue = Issue::DuplicateUseFunction; + break; + case \ast\flags\USE_CONST: + $issue = Issue::DuplicateUseConstant; + break; + default: + $issue = Issue::DuplicateUseNormal; + break; + } + + Issue::maybeEmit( + $code_base, + $this, + $issue, + $lineno, + $target, + $alias + ); + } + + /** + * @param int $strict_types + * The strict_type setting for the file + * + * @return Context + * This context with the given value is returned + */ + public function withStrictTypes(int $strict_types): Context + { + $this->strict_types = $strict_types; + return $this; + } + + /** + * @return bool + * Returns true if strict_types is set to 1 in this context. + */ + public function isStrictTypes(): bool + { + return (1 === $this->strict_types); + } + + /** + * @return Scope + * An object describing the contents of the current + * scope. + */ + public function getScope(): Scope + { + return $this->scope; + } + + /** + * Set the scope on the context + */ + public function setScope(Scope $scope): void + { + $this->scope = $scope; + // TODO: Less aggressive? ConditionVisitor creates a lot of scopes + $this->cache = []; + } + + /** + * @return Context + * A new context with the given scope + * @phan-pure + */ + public function withScope(Scope $scope): Context + { + $context = clone($this); + $context->setScope($scope); + return $context; + } + + /** + * @phan-pure + */ + public function withEnterLoop(Node $node): Context + { + $context = clone($this); + $context->loop_nodes[] = $node; + return $context; + } + + public function withExitLoop(Node $node): Context + { + $context = clone($this); + + while ($context->loop_nodes) { + if (\array_pop($context->loop_nodes) === $node) { + if (\count($context->loop_nodes) === 0) { + // @phan-suppress-next-line PhanUndeclaredProperty + foreach ($node->phan_deferred_checks ?? [] as $cb) { + $cb($context); + } + } + break; + } + } + return $context; + } + + /** + * @suppress PhanUndeclaredProperty + */ + public function deferCheckToOutermostLoop(Closure $closure): void + { + $node = $this->loop_nodes[0] ?? null; + if ($node) { + if (!isset($node->phan_deferred_checks)) { + $node->phan_deferred_checks = []; + } + $node->phan_deferred_checks[] = $closure; + } + } + + /** + * Is this in a loop of the current function body (or global scope)? + */ + public function isInLoop(): bool + { + return \count($this->loop_nodes) > 0; + } + + /** + * Fetches the innermost loop node. + * @suppress PhanPossiblyFalseTypeReturn + */ + public function getInnermostLoopNode(): Node + { + return \end($this->loop_nodes); + } + + public function withoutLoops(): Context + { + $context = clone($this); + $context->loop_nodes = []; + return $context; + } + + /** + * @return Context + * + * A new context with the a clone of the current scope. + * This is useful when using AssignmentVisitor for things that aren't actually assignment operations. + * (AssignmentVisitor modifies the passed in scope variables in place) + */ + public function withClonedScope(): Context + { + $context = clone($this); + $context->scope = clone($context->scope); + return $context; + } + + /** + * @param Variable $variable + * A variable to add to the scope for the new + * context + * + * @return Context + * A new context based on this with a variable + * as defined by the parameters in scope + * @phan-pure + */ + public function withScopeVariable( + Variable $variable + ): Context { + return $this->withScope( + $this->scope->withVariable($variable) + ); + } + + /** + * @param Variable $variable + * A variable to add to the scope for the new + * context + */ + public function addGlobalScopeVariable(Variable $variable): void + { + $this->scope->addGlobalVariable($variable); + } + + /** + * Add a variable to this context's scope. Note that + * this does not create a new context. You're actually + * injecting the variable into the context. Use with + * caution. + * + * @param Variable $variable + * A variable to inject into this context + */ + public function addScopeVariable( + Variable $variable + ): void { + $this->scope->addVariable($variable); + } + + /** + * Unset a variable in this context's scope. Note that + * this does not create a new context. You're actually + * removing the variable from the context. Use with + * caution. + * + * @param string $variable_name + * The name of a variable to remove from the context. + */ + public function unsetScopeVariable( + string $variable_name + ): void { + $this->scope->unsetVariable($variable_name); + } + + /** + * Returns a string representing this Context for debugging + * @suppress PhanUnreferencedPublicMethod kept around to make it easy to dump variables in a context + */ + public function toDebugString(): string + { + $result = (string)$this; + foreach ($this->scope->getVariableMap() as $variable) { + $result .= "\n{$variable->getDebugRepresentation()}"; + } + return $result; + } + + /** + * @return bool + * True if this context is currently within a class + * scope, else false. + */ + public function isInClassScope(): bool + { + return $this->scope->isInClassScope(); + } + + /** + * @return FullyQualifiedClassName + * A fully-qualified structural element name describing + * the current class in scope. + */ + public function getClassFQSEN(): FullyQualifiedClassName + { + return $this->scope->getClassFQSEN(); + } + + public function getClassFQSENOrNull(): ?FullyQualifiedClassName + { + return $this->scope->getClassFQSENOrNull(); + } + + /** + * @return bool + * True if this context is currently within a property + * scope, else false. + * @suppress PhanUnreferencedPublicMethod + */ + public function isInPropertyScope(): bool + { + return $this->scope->isInPropertyScope(); + } + + /** + * @return FullyQualifiedPropertyName + * A fully-qualified structural element name describing + * the current property in scope. + */ + public function getPropertyFQSEN(): FullyQualifiedPropertyName + { + return $this->scope->getPropertyFQSEN(); + } + + /** + * @param CodeBase $code_base + * The global code base holding all state + * + * @return Clazz + * Get the class in this scope, or fail real hard + * + * @throws CodeBaseException + * Thrown if we can't find the class in scope within the + * given codebase. + */ + public function getClassInScope(CodeBase $code_base): Clazz + { + if (!$this->scope->isInClassScope()) { + throw new AssertionError("Must be in class scope to get class"); + } + + if (!$code_base->hasClassWithFQSEN($this->getClassFQSEN())) { + throw new CodeBaseException( + $this->getClassFQSEN(), + "Cannot find class with FQSEN {$this->getClassFQSEN()} in context {$this}" + ); + } + + return $code_base->getClassByFQSEN( + $this->getClassFQSEN() + ); + } + + /** + * @param CodeBase $code_base + * The global code base holding all state + * + * @return Property + * Get the property in this scope, or fail real hard + * + * @throws CodeBaseException + * Thrown if we can't find the property in scope within the + * given codebase. + */ + public function getPropertyInScope(CodeBase $code_base): Property + { + if (!$this->scope->isInPropertyScope()) { + throw new AssertionError("Must be in property scope to get property"); + } + + $property_fqsen = $this->getPropertyFQSEN(); + if (!$code_base->hasPropertyWithFQSEN($property_fqsen)) { + throw new CodeBaseException( + $property_fqsen, + "Cannot find class with FQSEN {$property_fqsen} in context {$this}" + ); + } + + return $code_base->getPropertyByFQSEN( + $property_fqsen + ); + } + + /** + * @return bool + * True if this context is currently within a method, + * function or closure scope. + */ + public function isInFunctionLikeScope(): bool + { + return $this->scope->isInFunctionLikeScope(); + } + + /** + * @return bool + * True if this context is currently within a method. + */ + public function isInMethodScope(): bool + { + return $this->scope->isInMethodLikeScope(); + } + + /** + * @return FullyQualifiedFunctionLikeName|FullyQualifiedMethodName|FullyQualifiedFunctionName + * A fully-qualified structural element name describing + * the current function or method in scope. + */ + public function getFunctionLikeFQSEN() + { + $scope = $this->scope; + if (!$scope->isInFunctionLikeScope()) { + throw new AssertionError("Must be in function-like scope to get function-like FQSEN"); + } + return $scope->getFunctionLikeFQSEN(); + } + + /** + * @param CodeBase $code_base + * The global code base holding all state + * + * @return Element\Func|Element\Method + * Get the method in this scope or fail real hard + */ + public function getFunctionLikeInScope( + CodeBase $code_base + ): FunctionInterface { + $fqsen = $this->getFunctionLikeFQSEN(); + + if ($fqsen instanceof FullyQualifiedFunctionName) { + if (!$code_base->hasFunctionWithFQSEN($fqsen)) { + throw new RuntimeException("The function $fqsen does not exist, but Phan is in that function's scope"); + } + return $code_base->getFunctionByFQSEN($fqsen); + } + + if ($fqsen instanceof FullyQualifiedMethodName) { + if (!$code_base->hasMethodWithFQSEN($fqsen)) { + throw new RuntimeException("Method $fqsen does not exist"); + } + return $code_base->getMethodByFQSEN($fqsen); + } + + throw new AssertionError("FQSEN must be for a function or method"); + } + + /** + * @return bool + * True if we're within the scope of a class, method, + * function or closure. False if we're in the global + * scope + * @suppress PhanUnreferencedPublicMethod + */ + public function isInElementScope(): bool + { + return $this->scope->isInElementScope(); + } + + /** + * @return bool + * True if we're in the global scope (not in a class, + * method, function, closure). + */ + public function isInGlobalScope(): bool + { + return !$this->scope->isInElementScope(); + } + + /** + * @param CodeBase $code_base + * The code base from which to retrieve the TypedElement + * + * @return TypedElement + * The element whose scope we're in. If we're in the global + * scope this method will go down in flames and take your + * process with it. + * + * @throws CodeBaseException if this was called without first checking + * if this context is in an element scope + */ + public function getElementInScope(CodeBase $code_base): TypedElement + { + if ($this->scope->isInFunctionLikeScope()) { + return $this->getFunctionLikeInScope($code_base); + } elseif ($this->scope->isInPropertyScope()) { + return $this->getPropertyInScope($code_base); + } elseif ($this->scope->isInClassScope()) { + return $this->getClassInScope($code_base); + } + + throw new CodeBaseException( + null, + "Cannot get element in scope if we're in the global scope" + ); + } + + /** + * @param CodeBase $code_base + * The code base from which to retrieve a possible TypedElement + * that contains an issue suppression list + * + * @param string $issue_name + * The name of the issue which is being checked for membership + * in an issue suppression list. + * + * @return bool + * True if issues with the given name are suppressed within + * this context. + */ + public function hasSuppressIssue( + CodeBase $code_base, + string $issue_name + ): bool { + if ($code_base->hasFileLevelSuppression($this->file, $issue_name)) { + return true; + } + if (!$this->scope->isInElementScope()) { + return false; + } + + $element = $this->getElementInScope($code_base); + if ($element instanceof ClassElement) { + $defining_fqsen = $element->getRealDefiningFQSEN(); + if ($defining_fqsen !== $element->getFQSEN()) { + if ($defining_fqsen instanceof FullyQualifiedMethodName) { + $element = $code_base->getMethodByFQSEN($defining_fqsen); + } elseif ($defining_fqsen instanceof FullyQualifiedPropertyName) { + $element = $code_base->getPropertyByFQSEN($defining_fqsen); + } elseif ($defining_fqsen instanceof FullyQualifiedClassConstantName) { + $element = $code_base->getClassConstantByFQSEN($defining_fqsen); + } + } + } + $has_suppress_issue = $element->hasSuppressIssue($issue_name); + + // Increment the suppression use count + if ($has_suppress_issue) { + $element->incrementSuppressIssueCount($issue_name); + } + + return $has_suppress_issue; + } + + /** + * $this->cache is reused for multiple types of caches + * We xor the node ids with the following bits so that the values don't overlap. + * (The node id is based on \spl_object_id(), which is the object ID number. + * + * (This caching scheme makes a reasonable assumption + * that there are less than 1 billion Node objects on 32-bit systems, + * (It'd run out of memory with more than 4 bytes needed per Node) + * and less than (1 << 62) objects on 64-bit systems.) + * + * It also assumes that nodes won't be freed while this Context still exists + * + * 0x00(node_id) is used for getUnionTypeOfNodeIfCached(int $node_id, false) + * 0x10(node_id) is used for getUnionTypeOfNodeIfCached(int $node_id, true) + * 0x01(node_id) is used for getCachedClassListOfNode(int $node_id) + */ + public const HIGH_BIT_1 = (1 << (\PHP_INT_SIZE * 8) - 1); + public const HIGH_BIT_2 = (1 << (\PHP_INT_SIZE * 8) - 2); + + /** + * @param int $node_id \spl_object_id($node) + * @param bool $should_catch_issue_exception the value passed to UnionTypeVisitor + * @suppress PhanPartialTypeMismatchReturn seen with --analyze-twice because $this->cache is reused + */ + public function getUnionTypeOfNodeIfCached(int $node_id, bool $should_catch_issue_exception): ?UnionType + { + if ($should_catch_issue_exception) { + return $this->cache[$node_id] ?? null; + } + return $this->cache[$node_id ^ self::HIGH_BIT_1] ?? null; + } + + /** + * TODO: This may be unsafe? Clear the cache after a function goes out of scope. + * + * A UnionType is only cached if there is no exception. + * + * @param int $node_id \spl_object_id($node) + * @param UnionType $type the type to cache. + * @param bool $should_catch_issue_exception the value passed to UnionTypeVisitor + */ + public function setCachedUnionTypeOfNode(int $node_id, UnionType $type, bool $should_catch_issue_exception): void + { + if (!$should_catch_issue_exception) { + $this->cache[$node_id ^ self::HIGH_BIT_1] = $type; + // If we weren't suppressing exceptions and setCachedUnionTypeOfNode was called, + // that would mean that there were no exceptions to catch. + // So, that means the UnionType for should_catch_issue_exception = true will be the same + } + $this->cache[$node_id] = $type; + } + + /** + * @param int $node_id + * @return ?array{0:UnionType,1:Clazz[]} $result + * @suppress PhanPartialTypeMismatchReturn cache is mixed with other cache objects + */ + public function getCachedClassListOfNode(int $node_id): ?array + { + return $this->cache[$node_id ^ self::HIGH_BIT_2] ?? null; + } + + /** + * TODO: This may be unsafe? Clear the cache after a function goes out of scope. + * @param int $node_id \spl_object_id($node) + * @param array{0:UnionType,1:Clazz[]} $result + */ + public function setCachedClassListOfNode(int $node_id, array $result): void + { + $this->cache[$node_id ^ self::HIGH_BIT_2] = $result; + } + + public function clearCachedUnionTypes(): void + { + $this->cache = []; + } + + /** + * Gets Phan's internal representation of all of the 'use elem;' statements in a namespace. + * Use hasNamespaceMapFor and getNamespaceMapFor instead. + * + * @internal + * + * @return array> maps use kind flags to the entries. + * @phan-return associative-array> maps use kind flags to the entries. + */ + public function getNamespaceMap(): array + { + return $this->namespace_map; + } + + /** + * Warn about any unused \ast\AST_USE or \ast\AST_GROUP_USE nodes (`use Foo\Bar;`) + * This should be called after analyzing the end of a namespace (And before analyzing the next namespace) + * + * @param CodeBase $code_base + * The code base within which we're operating + * + * @internal + */ + public function warnAboutUnusedUseElements(CodeBase $code_base): void + { + foreach ($this->namespace_map as $flags => $entries_for_flag) { + foreach ($entries_for_flag as $namespace_map_entry) { + if ($namespace_map_entry->is_used) { + continue; + } + switch ($flags) { + case \ast\flags\USE_NORMAL: + default: + $issue_type = Issue::UnreferencedUseNormal; + break; + case \ast\flags\USE_FUNCTION: + $issue_type = Issue::UnreferencedUseFunction; + break; + case \ast\flags\USE_CONST: + $issue_type = Issue::UnreferencedUseConstant; + break; + } + Issue::maybeEmit( + $code_base, + $this, + $issue_type, + $namespace_map_entry->lineno, + $namespace_map_entry->original_name, + (string)$namespace_map_entry->fqsen + ); + } + } + } + + /** + * @internal + * @suppress PhanAccessMethodInternal + */ + public function importNamespaceMapFromParsePhase(CodeBase $code_base): void + { + $this->parse_namespace_map = $code_base->getNamespaceMapFromParsePhase($this->file, $this->namespace, $this->namespace_id); + } + + /** + * Copy private properties of $other to this + */ + final protected function copyPropertiesFrom(Context $other): void + { + $this->file = $other->file; + $this->line_number_start = $other->line_number_start; + $this->namespace = $other->namespace; + $this->namespace_id = $other->namespace_id; + $this->namespace_map = $other->namespace_map; + $this->parse_namespace_map = $other->parse_namespace_map; + $this->strict_types = $other->strict_types; + $this->loop_nodes = $other->loop_nodes; + $this->scope = $other->scope; + $this->cache = $other->cache; + } + + /** + * This name is internally used by Phan to track the properties of $this similarly to the way array shapes are represented. + */ + public const VAR_NAME_THIS_PROPERTIES = "phan\0\$this"; + + /** + * Analyzes the side effects of setting the type of $this->property to $type + * @suppress PhanUnreferencedPublicMethod this might be used in the future + */ + public function withThisPropertySetToType(Property $property, UnionType $type): Context + { + $old_union_type = $property->getUnionType(); + if ($this->scope->hasVariableWithName(self::VAR_NAME_THIS_PROPERTIES)) { + $variable = clone($this->scope->getVariableByName(self::VAR_NAME_THIS_PROPERTIES)); + $old_type = $variable->getUnionType(); + $override_type = ArrayShapeType::fromFieldTypes([$property->getName() => $type], false); + $override_type = self::addArrayShapeTypes($override_type, $old_type->getTypeSet()); + + $variable->setUnionType($override_type->asPHPDocUnionType()); + } else { + // There is nothing inferred about any type + + if ($old_union_type->isEqualTo($type)) { + // And this new type is what we already inferred, so there's nothing to do + return $this; + } + $override_type = ArrayShapeType::fromFieldTypes([$property->getName() => $type], false); + $variable = new Variable( + $this, + self::VAR_NAME_THIS_PROPERTIES, + $override_type->asPHPDocUnionType(), + 0 + ); + } + return $this->withScopeVariable($variable); + } + + /** + * Analyzes the side effects of setting the type of $this->property_name to $type + * + * The caller should check if it is necessary to do this. + */ + public function withThisPropertySetToTypeByName(string $property_name, UnionType $type): Context + { + if ($this->scope->hasVariableWithName(self::VAR_NAME_THIS_PROPERTIES)) { + $variable = clone($this->scope->getVariableByName(self::VAR_NAME_THIS_PROPERTIES)); + $old_type = $variable->getUnionType(); + $override_type = ArrayShapeType::fromFieldTypes([$property_name => $type], false); + $override_type = self::addArrayShapeTypes($override_type, $old_type->getTypeSet()); + + $variable->setUnionType($override_type->asPHPDocUnionType()); + } else { + // There is nothing inferred about any type + + $override_type = ArrayShapeType::fromFieldTypes([$property_name => $type], false); + $variable = new Variable( + $this, + self::VAR_NAME_THIS_PROPERTIES, + $override_type->asPHPDocUnionType(), + 0 + ); + } + return $this->withScopeVariable($variable); + } + + /** + * @param list $type_set + */ + private static function addArrayShapeTypes(ArrayShapeType $override_type, array $type_set): ArrayShapeType + { + if (!$type_set) { + return $override_type; + } + $array_shape_type_set = []; + foreach ($type_set as $type) { + if ($type instanceof ArrayShapeType) { + $array_shape_type_set[] = $type; + } + } + if ($array_shape_type_set) { + // Add in all of the locally known types for other properties + $override_type = ArrayShapeType::combineWithPrecedence($override_type, ArrayShapeType::union($array_shape_type_set)); + } + return $override_type; + } + + public function getThisPropertyIfOverridden(string $name): ?UnionType + { + if (!$this->scope->hasVariableWithName(self::VAR_NAME_THIS_PROPERTIES)) { + return null; + } + $types = $this->scope->getVariableByName(self::VAR_NAME_THIS_PROPERTIES)->getUnionType(); + if ($types->isEmpty()) { + return null; + } + + $result = null; + foreach ($types->getTypeSet() as $type) { + if (!$type instanceof ArrayShapeType) { + return null; + } + $extra = $type->getFieldTypes()[$name] ?? null; + if (!$extra || ($extra->isPossiblyUndefined() && !$extra->isDefinitelyUndefined())) { + return null; + } + if ($result) { + '@phan-var UnionType $result'; + $result = $result->withUnionType($extra); + } else { + $result = $extra; + } + } + return $result; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/AddressableElement.php b/bundled-libs/phan/phan/src/Phan/Language/Element/AddressableElement.php new file mode 100644 index 000000000..b07a033e5 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/AddressableElement.php @@ -0,0 +1,366 @@ + + * A list of locations in which this typed structural + * element is referenced from. + * References from the same file and line are deduplicated to save memory. + */ + protected $reference_list = []; + + /** + * @var ?string + * The doc comment of the element + */ + protected $doc_comment; + + /** + * @var bool + * Has this element been hydrated yet? + * (adding information from ancestor classes for more detailed type information) + */ + protected $is_hydrated = false; + + /** + * @param Context $context + * The context in which the structural element lives + * + * @param string $name + * The name of the typed structural element + * + * @param UnionType $type + * A '|' delimited set of types satisfied by this + * typed structural element. + * + * @param int $flags + * The flags property contains node specific flags. It is + * always defined, but for most nodes it is always zero. + * ast\kind_uses_flags() can be used to determine whether + * a certain kind has a meaningful flags value. + * + * @param FQSEN $fqsen + * A fully qualified name for the element + */ + public function __construct( + Context $context, + string $name, + UnionType $type, + int $flags, + FQSEN $fqsen + ) { + parent::__construct( + $context, + $name, + $type, + $flags + ); + + $this->setFQSEN($fqsen); + } + + /** + * @return FQSEN + * The fully-qualified structural element name of this + * structural element + */ + public function getFQSEN() + { + return $this->fqsen; + } + + /** + * @param FQSEN $fqsen + * A fully qualified structural element name to set on + * this element + */ + public function setFQSEN(FQSEN $fqsen): void + { + $this->fqsen = $fqsen; + } + + /** + * @return bool true if this element's visibility + * is strictly more visible than $other (public > protected > private) + */ + public function isStrictlyMoreVisibleThan(AddressableElementInterface $other): bool + { + if ($this->isPrivate()) { + return false; + } // $this is public or protected + + if ($other->isPrivate()) { + return true; + } + + if ($other->isProtected()) { + // True if this is public. + return !$this->isProtected(); + } + // $other is public + return false; + } + + /** + * @return bool + * True if this is a public property + */ + public function isPublic(): bool + { + return !( + $this->isProtected() || $this->isPrivate() + ); + } + + /** + * @return bool + * True if this is a protected element + */ + public function isProtected(): bool + { + return $this->getFlagsHasState(\ast\flags\MODIFIER_PROTECTED); + } + + /** + * @return bool + * True if this is a private element + */ + public function isPrivate(): bool + { + return $this->getFlagsHasState(\ast\flags\MODIFIER_PRIVATE); + } + + /** + * @param CodeBase $code_base (@phan-unused-param, this is used by subclasses) + * The code base in which this element exists. + * + * @return bool + * True if this is marked as an `(at)internal` element + */ + public function isNSInternal(CodeBase $code_base): bool + { + return $this->getPhanFlagsHasState(Flags::IS_NS_INTERNAL); + } + + /** + * Set this element as being `internal`. + */ + public function setIsNSInternal(bool $is_internal): void + { + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_NS_INTERNAL, + $is_internal + )); + } + + /** + * @param CodeBase $code_base (@phan-unused-param, this is used by subclasses) + * The code base in which this element exists. + * + * @return bool + * True if this element is intern + */ + public function isNSInternalAccessFromContext( + CodeBase $code_base, + Context $context + ): bool { + // Figure out which namespace this element is within + $element_namespace = $this->getElementNamespace() ?: '\\'; + + // Get our current namespace from the context + $context_namespace = $context->getNamespace() ?: '\\'; + + // Test to see if the context is within the same + // namespace as where the element is defined + return (0 === \strcasecmp($context_namespace, $element_namespace)); + } + + /** + * @param FileRef $file_ref + * A reference to a location in which this typed structural + * element is referenced. + */ + public function addReference(FileRef $file_ref): void + { + if (Config::get_track_references()) { + // Currently, we don't need to track references to PHP-internal methods/functions/constants + // such as PHP_VERSION, strlen(), Closure::bind(), etc. + // This may change in the future. + if ($this->isPHPInternal()) { + return; + } + $this->reference_list[$file_ref->__toString()] = $file_ref; + } + } + + /** + * Copy addressable references from an element of the same subclass + */ + public function copyReferencesFrom(AddressableElement $element): void + { + if ($this === $element) { + // Should be impossible + return; + } + foreach ($element->reference_list as $key => $file_ref) { + $this->reference_list[$key] = $file_ref; + } + } + + /** + * @return array + * A list of references to this typed structural element. + */ + public function getReferenceList(): array + { + return $this->reference_list; + } + + /** + * @param CodeBase $code_base (@phan-unused-param) + * Some elements may need access to the code base to + * figure out their total reference count. + * + * @return int + * The number of references to this typed structural element + */ + public function getReferenceCount( + CodeBase $code_base + ): int { + return \count($this->reference_list); + } + + /** + * This method must be called before analysis + * begins. + * @override + */ + public function hydrate(CodeBase $code_base): void + { + if ($this->is_hydrated) { // Same as isFirstExecution(), inlined due to being called frequently. + return; + } + $this->is_hydrated = true; + + $this->hydrateOnce($code_base); + } + + /** + * @unused-param $code_base + */ + protected function hydrateOnce(CodeBase $code_base): void + { + // Do nothing unless overridden + } + + /** + * Returns the namespace in which this element was declared + */ + public function getElementNamespace(): string + { + $element_fqsen = $this->getFQSEN(); + if (!$element_fqsen instanceof FullyQualifiedGlobalStructuralElement) { + throw new AssertionError('Expected $this->element_fqsen to be FullyQualifiedGlobalStructuralElement'); + } + + // Figure out which namespace this element is within + return $element_fqsen->getNamespace(); + } + + /** + * Used by daemon mode to restore an element to the state it had before parsing. + * @internal + */ + abstract public function createRestoreCallback(): ?Closure; + + /** + * @param ?string $doc_comment the 'docComment' for this element, if any exists. + */ + public function setDocComment(string $doc_comment = null): void + { + $this->doc_comment = $doc_comment; + } + + /** + * @return ?string the 'docComment' for this element, if any exists. + */ + public function getDocComment(): ?string + { + return $this->doc_comment; + } + + /** + * @return string the reason why this element was deprecated, or null if this could not be determined. + */ + public function getDeprecationReason(): string + { + return $this->memoize(__METHOD__, function (): string { + if (!\is_string($this->doc_comment)) { + return ''; + } + if (!\preg_match('/@deprecated\b/', $this->doc_comment, $matches, \PREG_OFFSET_CAPTURE)) { + return ''; + } + $doc_comment = \preg_replace('@(^/\*)|(\*/$)@D', '', $this->doc_comment); + $lines = \explode("\n", $doc_comment); + foreach ($lines as $i => $line) { + $line = MarkupDescription::trimLine($line); + if (\preg_match('/^\s*@deprecated\b/', $line) > 0) { + $new_lines = MarkupDescription::extractTagSummary($lines, $i); + if (!$new_lines) { + return ''; + } + $new_lines[0] = \preg_replace('/^\s*@deprecated\b\s*/', '', $new_lines[0]); + $reason = \implode(' ', \array_filter(\array_map('trim', $new_lines), static function (string $line): bool { + return $line !== ''; + })); + if ($reason !== '') { + return ' (Deprecated because: ' . $reason . ')'; + } + } + } + + return ''; + }); + } + + /** + * @return string the representation of this FQSEN for issue messages. + * Overridden in some subclasses + * @suppress PhanUnreferencedPublicMethod (inference error?) + */ + public function getRepresentationForIssue(): string + { + return $this->getFQSEN()->__toString(); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/AddressableElementInterface.php b/bundled-libs/phan/phan/src/Phan/Language/Element/AddressableElementInterface.php new file mode 100644 index 000000000..ddacf47f5 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/AddressableElementInterface.php @@ -0,0 +1,156 @@ + protected > private) + */ + public function isStrictlyMoreVisibleThan(AddressableElementInterface $other): bool; + + /** + * @return bool + * True if this is a public property + */ + public function isPublic(): bool; + + /** + * @return bool + * True if this is a protected property + */ + public function isProtected(): bool; + + /** + * @return bool + * True if this is a private property + */ + public function isPrivate(): bool; + + /** + * Track a location $file_ref in which this typed structural element + * is referenced. + * + * @param FileRef $file_ref + */ + public function addReference(FileRef $file_ref): void; + + /** + * @return FileRef[] + * A list of references to this typed structural element. + */ + public function getReferenceList(): array; + + /** + * @param CodeBase $code_base + * Some elements may need access to the code base to + * figure out their total reference count. + * + * @return int + * The number of references to this typed structural element + */ + public function getReferenceCount( + CodeBase $code_base + ): int; + + /** + * @return string For use in the language server protocol. + */ + public function getMarkupDescription(): string; + + /** + * @return ?string the 'docComment' for this element, if any exists. + */ + public function getDocComment(): ?string; + + /** + * @return Context + * The context in which this structural element exists + */ + public function getContext(): Context; + + /** + * @return bool + * True if this element is marked as deprecated + */ + public function isDeprecated(): bool; + + /** + * Set this element as deprecated or not deprecated + * + * @param bool $is_deprecated + */ + public function setIsDeprecated(bool $is_deprecated): void; + + /** + * Set the set of issue names ($suppress_issue_list) to suppress + * + * @param array $suppress_issue_set + */ + public function setSuppressIssueSet(array $suppress_issue_set): void; + + /** + * @return array + * Returns a map from issue name to count of suppressions + */ + public function getSuppressIssueList(): array; + + /** + * Increments the number of times $issue_name was suppressed. + */ + public function incrementSuppressIssueCount(string $issue_name): void; + + /** + * return bool + * True if this element would like to suppress the given + * issue name + */ + public function hasSuppressIssue(string $issue_name): bool; + + /** + * @return bool + * True if this element would like to suppress the given + * issue name. + * + * If this is true, this automatically calls incrementSuppressIssueCount. + * Most callers should use this, except for uses similar to UnusedSuppressionPlugin + */ + public function checkHasSuppressIssueAndIncrementCount(string $issue_name): bool; + + /** + * @return bool + * True if this was an internal PHP object + */ + public function isPHPInternal(): bool; + + /** + * This method must be called before analysis + * begins. + */ + public function hydrate(CodeBase $code_base): void; +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/ClassAliasRecord.php b/bundled-libs/phan/phan/src/Phan/Language/Element/ClassAliasRecord.php new file mode 100644 index 000000000..44db1403d --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/ClassAliasRecord.php @@ -0,0 +1,35 @@ +alias_fqsen = $alias_fqsen; + $this->context = $context; + $this->lineno = $lineno; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/ClassConstant.php b/bundled-libs/phan/phan/src/Phan/Language/Element/ClassConstant.php new file mode 100644 index 000000000..75a4a68a0 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/ClassConstant.php @@ -0,0 +1,179 @@ +setDefiningFQSEN($fqsen); + } + + /** + * Override the default getter to fill in a future + * union type if available. + */ + public function getUnionType(): UnionType + { + $union_type = $this->getFutureUnionType(); + if (!\is_null($union_type)) { + $this->setUnionType($union_type); + } + + return parent::getUnionType(); + } + + /** + * @return FullyQualifiedClassConstantName + * The fully-qualified structural element name of this + * structural element + */ + public function getFQSEN(): FQSEN + { + return $this->fqsen; + } + + public function __toString(): string + { + return $this->getVisibilityName() . ' const ' . $this->name; + } + + /** + * Used for generating issue messages + */ + public function asVisibilityAndFQSENString(): string + { + return $this->getVisibilityName() . ' ' . + $this->getClassFQSEN()->__toString() . + '::' . + $this->name; + } + + public function getMarkupDescription(): string + { + $string = ''; + + if ($this->isProtected()) { + $string .= 'protected '; + } elseif ($this->isPrivate()) { + $string .= 'private '; + } + + $string .= 'const ' . $this->name . ' = '; + $value_node = $this->getNodeForValue(); + $string .= ASTReverter::toShortString($value_node); + return $string; + } + + /** + * Returns the visibility of this class constant + * (either 'public', 'protected', or 'private') + */ + public function getVisibilityName(): string + { + if ($this->isPrivate()) { + return 'private'; + } elseif ($this->isProtected()) { + return 'protected'; + } else { + return 'public'; + } + } + + /** + * Converts this class constant to a stub php snippet that can be used by `tool/make_stubs` + */ + public function toStub(): string + { + $string = ' '; + if ($this->isPrivate()) { + $string .= 'private '; + } elseif ($this->isProtected()) { + $string .= 'protected '; + } + + // For PHP 7.0 compatibility of stubs, + // show public class constants as 'const', not 'public const'. + // Also, PHP modules probably won't have private/protected constants. + $string .= 'const ' . $this->name . ' = '; + $fqsen = $this->getFQSEN()->__toString(); + if (\defined($fqsen)) { + // TODO: Could start using $this->getNodeForValue()? + // NOTE: This is used by tool/make_stubs, which is why it uses reflection instead of getting a node. + $string .= StringUtil::varExportPretty(\constant($fqsen)) . ';'; + } else { + $string .= "null; // could not find"; + } + return $string; + } + + /** + * Set the phpdoc comment associated with this class comment. + */ + public function setComment(?Comment $comment): void + { + $this->comment = $comment; + } + + /** + * Get the phpdoc comment associated with this class comment. + */ + public function getComment(): ?Comment + { + return $this->comment; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/ClassElement.php b/bundled-libs/phan/phan/src/Phan/Language/Element/ClassElement.php new file mode 100644 index 000000000..3e17e3619 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/ClassElement.php @@ -0,0 +1,342 @@ +class_fqsen = $fqsen->getFullyQualifiedClassName(); + } + + /** + * @param FullyQualifiedClassElement $fqsen + * @override + * @suppress PhanParamSignatureMismatch deliberately more specific + */ + public function setFQSEN(FQSEN $fqsen): void + { + if (!($fqsen instanceof FullyQualifiedClassElement)) { + throw new TypeError('Expected $fqsen to be a subclass of Phan\Language\Element\FullyQualifiedClassElement'); + } + parent::setFQSEN($fqsen); + $this->class_fqsen = $fqsen->getFullyQualifiedClassName(); + } + + /** + * @var FullyQualifiedClassElement|null + * The FQSEN of this element where it is originally + * defined. + */ + private $defining_fqsen = null; + + /** + * @return bool + * True if this element has a defining FQSEN defined + */ + public function hasDefiningFQSEN(): bool + { + return ($this->defining_fqsen != null); + } + + /** + * @return FullyQualifiedClassElement + * The FQSEN of the original definition of this class element (before inheritance). + */ + public function getDefiningFQSEN(): FullyQualifiedClassElement + { + if ($this->defining_fqsen === null) { + throw new AssertionError('should check hasDefiningFQSEN'); + } + return $this->defining_fqsen; + } + + /** + * Gets the real defining FQSEN. + * This differs from getDefiningFQSEN() if the definition was from a trait. + * + * @return FullyQualifiedClassElement + */ + public function getRealDefiningFQSEN() + { + return $this->getDefiningFQSEN(); + } + + /** + * @return FullyQualifiedClassName + * The FQSEN of of the class originally defining this class element. + * + * @throws CodeBaseException if this was called without first checking + * if hasDefiningFQSEN() + */ + public function getDefiningClassFQSEN(): FullyQualifiedClassName + { + if (\is_null($this->defining_fqsen)) { + throw new CodeBaseException( + $this->fqsen, + "No defining class for {$this->fqsen}" + ); + } + return $this->defining_fqsen->getFullyQualifiedClassName(); + } + + /** + * Sets the FQSEN of the class element in the location in which + * the element was originally defined. + * + * @param FullyQualifiedClassElement $defining_fqsen + */ + public function setDefiningFQSEN( + FullyQualifiedClassElement $defining_fqsen + ): void { + $this->defining_fqsen = $defining_fqsen; + } + + /** + * @return Clazz + * The class on which this element was originally defined + * @throws CodeBaseException if hasDefiningFQSEN is false + */ + public function getDefiningClass(CodeBase $code_base): Clazz + { + $class_fqsen = $this->getDefiningClassFQSEN(); + + if (!$code_base->hasClassWithFQSEN($class_fqsen)) { + throw new CodeBaseException( + $class_fqsen, + "Defining class $class_fqsen for {$this->fqsen} not found" + ); + } + + return $code_base->getClassByFQSEN($class_fqsen); + } + + /** + * @return FullyQualifiedClassName + * The FQSEN of the class on which this element lives + */ + public function getClassFQSEN(): FullyQualifiedClassName + { + return $this->class_fqsen; + } + + /** + * @param CodeBase $code_base + * The code base with which to look for classes + * + * @return Clazz + * The class that defined this element + * + * @throws CodeBaseException + * An exception may be thrown if we can't find the + * class + */ + public function getClass( + CodeBase $code_base + ): Clazz { + $class_fqsen = $this->class_fqsen; + + if (!$code_base->hasClassWithFQSEN($class_fqsen)) { + throw new CodeBaseException( + $class_fqsen, + "Defining class $class_fqsen for {$this->fqsen} not found" + ); + } + + return $code_base->getClassByFQSEN($class_fqsen); + } + + /** + * @return bool + * True if this element overrides another element + */ + public function isOverride(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_OVERRIDE); + } + + /** + * Sets whether this element overrides another element + * + * @param bool $is_override + * True if this element overrides another element + */ + public function setIsOverride(bool $is_override): void + { + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_OVERRIDE, + $is_override + )); + } + + /** + * @return bool + * True if this is a static element + */ + public function isStatic(): bool + { + return $this->getFlagsHasState(\ast\flags\MODIFIER_STATIC); + } + + /** + * @param CodeBase $code_base + * The code base in which this element exists. + * + * @return bool + * True if this is an internal element + */ + public function isNSInternal(CodeBase $code_base): bool + { + return ( + parent::isNSInternal($code_base) + || $this->getClass($code_base)->isNSInternal($code_base) + ); + } + + public function getElementNamespace(): string + { + // Get the namespace that the class is within + return $this->class_fqsen->getNamespace() ?: '\\'; + } + + /** + * @param CodeBase $code_base used for access checks to protected properties + * @param ?FullyQualifiedClassName $accessing_class_fqsen the class FQSEN of the current scope. + * null if in the global scope. + * @return bool true if this can be accessed from the scope of $accessing_class_fqsen + */ + public function isAccessibleFromClass(CodeBase $code_base, ?FullyQualifiedClassName $accessing_class_fqsen): bool + { + if ($this->isPublic()) { + return true; + } + if (!$accessing_class_fqsen) { + // Accesses from outside class scopes can only access public FQSENs + return false; + } + $defining_fqsen = $this->getDefiningClassFQSEN(); + if ($defining_fqsen === $accessing_class_fqsen) { + return true; + } + $real_defining_fqsen = $this->getRealDefiningFQSEN()->getFullyQualifiedClassName(); + if ($real_defining_fqsen === $accessing_class_fqsen) { + return true; + } + if ($this->isPrivate()) { + if ($code_base->hasClassWithFQSEN($defining_fqsen)) { + $defining_class = $code_base->getClassByFQSEN($defining_fqsen); + foreach ($defining_class->getTraitFQSENList() as $trait_fqsen) { + if ($trait_fqsen === $accessing_class_fqsen) { + return true; + } + } + } + return false; + } + return self::checkCanAccessProtectedElement($code_base, $defining_fqsen, $accessing_class_fqsen); + } + + /** + * Check if a class can access a protected property defined in another class. + * + * Precondition: The property in $defining_fqsen is protected. + */ + private static function checkCanAccessProtectedElement(CodeBase $code_base, FullyQualifiedClassName $defining_fqsen, FullyQualifiedClassName $accessing_class_fqsen): bool + { + $accessing_class_type = $accessing_class_fqsen->asType(); + $type_of_class_of_property = $defining_fqsen->asType(); + + // If the definition of the property is protected, + // then the subclasses of the defining class can access it. + try { + foreach ($accessing_class_type->asExpandedTypes($code_base)->getTypeSet() as $type) { + if ($type->canCastToType($type_of_class_of_property)) { + return true; + } + } + // and base classes of the defining class can access it + foreach ($type_of_class_of_property->asExpandedTypes($code_base)->getTypeSet() as $type) { + if ($type->canCastToType($accessing_class_type)) { + return true; + } + } + } catch (RecursionDepthException $_) { + } + return false; + } + + /** + * @return bool + * True if this class constant is intended to be overridden in non-abstract classes. + */ + public function isPHPDocAbstract(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_PHPDOC_ABSTRACT); + } + + /** + * Records whether or not this class constant is intended to be abstract + */ + public function setIsPHPDocAbstract(bool $is_abstract): void + { + $this->setPhanFlags( + Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_PHPDOC_ABSTRACT, + $is_abstract + ) + ); + } + + /** + * @return bool + * True if this method is intended to be an override of another method (contains (at)override) + */ + public function isOverrideIntended(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_OVERRIDE_INTENDED); + } + + /** + * Sets whether this method is intended to be an override of another method (contains (at)override) + * @param bool $is_override_intended + + */ + public function setIsOverrideIntended(bool $is_override_intended): void + { + $this->setPhanFlags( + Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_OVERRIDE_INTENDED, + $is_override_intended + ) + ); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Clazz.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Clazz.php new file mode 100644 index 000000000..92f050342 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Clazz.php @@ -0,0 +1,3669 @@ + + * A possibly empty list of interfaces implemented + * by this class + */ + private $interface_fqsen_list = []; + + /** + * @var list + * Line numbers for indices of interface_fqsen_list. + */ + private $interface_fqsen_lineno = []; + + /** + * @var list + * A possibly empty list of traits used by this class + */ + private $trait_fqsen_list = []; + + /** + * @var list + * Line numbers for indices of trait_fqsen_list + */ + private $trait_fqsen_lineno = []; + + /** + * @var array + * Maps lowercase fqsen of a method to the trait names which are hidden + * and the trait aliasing info + */ + private $trait_adaptations_map = []; + + /** + * @var bool - hydrate() will check for this to avoid prematurely hydrating while looking for values of class constants. + */ + private $did_finish_parsing = true; + + /** + * @var ?UnionType for Type->asExpandedTypes() + * + * TODO: This won't reverse in daemon mode? + */ + private $additional_union_types = null; + + /** + * An additional id to disambiguate classes on the same line + * https://github.com/phan/phan/issues/1988 + */ + private $decl_id = 0; + + /** + * @var Context + */ + private $internal_context; + + /** + * @var list + */ + private $mixin_types = []; + + /** + * @param Context $context + * The context in which the structural element lives + * + * @param string $name + * The name of the typed structural element + * + * @param UnionType $type + * A '|' delimited set of types satisfied by this + * typed structural element. + * + * @param int $flags + * The flags property contains node specific flags. It is + * always defined, but for most nodes it is always zero. + * ast\kind_uses_flags() can be used to determine whether + * a certain kind has a meaningful flags value. + * + * @param FullyQualifiedClassName $fqsen + * A fully qualified name for this class + * + * @param Type|null $parent_type + * @param list $interface_fqsen_list + * @param list $trait_fqsen_list + */ + public function __construct( + Context $context, + string $name, + UnionType $type, + int $flags, + FullyQualifiedClassName $fqsen, + Type $parent_type = null, + array $interface_fqsen_list = [], + array $trait_fqsen_list = [] + ) { + parent::__construct( + $context, + $name, + $type, + $flags, + $fqsen + ); + + $this->parent_type = $parent_type; + $this->interface_fqsen_list = $interface_fqsen_list; + $this->trait_fqsen_list = $trait_fqsen_list; + + $internal_scope = new ClassScope( + $context->getScope(), + $fqsen, + $flags + ); + $this->setInternalScope($internal_scope); + $this->internal_context = $context->withScope($internal_scope); + } + + private static function getASTFlagsForReflectionProperty(ReflectionProperty $prop): int + { + if ($prop->isPrivate()) { + return \ast\flags\MODIFIER_PRIVATE; + } elseif ($prop->isProtected()) { + return \ast\flags\MODIFIER_PROTECTED; + } + return 0; + } + + /** + * @param CodeBase $code_base + * A reference to the entire code base in which this + * context exists + * + * @param ReflectionClass $class + * A reflection class representing a builtin class. + * + * @return Clazz + * A Class structural element representing the given named + * builtin. + */ + public static function fromReflectionClass( + CodeBase $code_base, + ReflectionClass $class + ): Clazz { + // Build a set of flags based on the constitution + // of the built-in class + $flags = 0; + if ($class->isFinal()) { + $flags = \ast\flags\CLASS_FINAL; + } elseif ($class->isInterface()) { + $flags = \ast\flags\CLASS_INTERFACE; + } elseif ($class->isTrait()) { + $flags = \ast\flags\CLASS_TRAIT; + } + if ($class->isAbstract()) { + $flags |= \ast\flags\CLASS_ABSTRACT; + } + + $context = new Context(); + + $class_name = $class->getName(); + // @phan-suppress-next-line PhanThrowTypeAbsentForCall should be valid if extension is valid + $class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString($class_name); + + // Build a base class element + $clazz = new Clazz( + $context, + $class_name, + UnionType::fromFullyQualifiedRealString('\\' . $class_name), + $flags, + $class_fqsen + ); + + // If this class has a parent class, add it to the + // class info + if (($parent_class = $class->getParentClass())) { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall should be valid if extension is valid + $parent_class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString( + '\\' . $parent_class->getName() + ); + + $parent_type = $parent_class_fqsen->asType(); + + $clazz->setParentType($parent_type); + } + + if ($class_name === "Traversable") { + // Make sure that canCastToExpandedUnionType() works as expected for Traversable and its subclasses + $clazz->addAdditionalType(IterableType::instance(false)); + } + + $class_scope = new ClassScope(new GlobalScope(), $class_fqsen, $flags); + + // Note: If there are multiple calls to Clazz->addProperty(), + // the UnionType from the first one will be used, subsequent calls to addProperty() + // will have no effect. + // As a result, we set the types from Phan's documented internal property types first, + // preferring them over the default values (which may be null, etc.). + foreach (UnionType::internalPropertyMapForClassName( + $clazz->getName() + ) as $property_name => $property_type_string) { + // An asterisk indicates that the class supports + // dynamic properties + if ($property_name === '*') { + $clazz->setHasDynamicProperties(true); + continue; + } + + $property_context = $context->withScope($class_scope); + + $property_type = + UnionType::fromStringInContext( + $property_type_string, + new Context(), + Type::FROM_TYPE + ); + + $property_fqsen = FullyQualifiedPropertyName::make( + $clazz->getFQSEN(), + $property_name + ); + + if ($class->hasProperty($property_name)) { + $reflection_property = $class->getProperty($property_name); + $flags = self::getASTFlagsForReflectionProperty($reflection_property); + $real_type = self::getRealTypeForReflectionProperty($reflection_property); + } else { + $flags = 0; + $real_type = UnionType::empty(); + } + + $property = new Property( + $property_context, + $property_name, + $property_type->withRealTypeSet($real_type->getTypeSet()), + $flags, + $property_fqsen, + $real_type + ); + // Record that Phan has known union types for this internal property, + // so that analysis of assignments to the property can account for it. + $property->setPHPDocUnionType($property_type); + + $clazz->addProperty($code_base, $property, None::instance()); + } + + // n.b.: public properties on internal classes don't get + // listed via reflection until they're set unless + // they have a default value. Therefore, we don't + // bother iterating over `$class->getProperties()` + // `$class->getStaticProperties()`. + + foreach ($class->getDefaultProperties() as $name => $default_value) { + $property_context = $context->withScope($class_scope); + + $property_fqsen = FullyQualifiedPropertyName::make( + $clazz->getFQSEN(), + $name + ); + + if ($clazz->hasPropertyWithName($code_base, $name)) { + continue; + } + if ($class->hasProperty($name)) { + $reflection_property = $class->getProperty($name); + $flags = self::getASTFlagsForReflectionProperty($reflection_property); + $real_type = self::getRealTypeForReflectionProperty($reflection_property); + } else { + $flags = 0; + $real_type = UnionType::empty(); + } + $property = new Property( + $property_context, + $name, + Type::fromObject($default_value)->asPHPDocUnionType()->withRealTypeSet($real_type->getTypeSet()), + $flags, + $property_fqsen, + $real_type + ); + + $clazz->addProperty($code_base, $property, None::instance()); + } + foreach ($class->getProperties() as $reflection_property) { + // In PHP 7.4, it's possible for internal classes to have properties without defaults if they're uninitialized. + $name = $reflection_property->name; + if ($clazz->hasPropertyWithName($code_base, $name)) { + continue; + } + $property_context = $context->withScope($class_scope); + + $property_fqsen = FullyQualifiedPropertyName::make( + $clazz->getFQSEN(), + $name + ); + + $real_type = self::getRealTypeForReflectionProperty($reflection_property); + $property = new Property( + $property_context, + $name, + $real_type->asRealUnionType(), + self::getASTFlagsForReflectionProperty($reflection_property), + $property_fqsen, + $real_type + ); + + $clazz->addProperty($code_base, $property, None::instance()); + } + + foreach ($class->getInterfaceNames() as $name) { + $clazz->addInterfaceClassFQSEN( + // @phan-suppress-next-line PhanThrowTypeAbsentForCall should be valid if extension is valid + FullyQualifiedClassName::fromFullyQualifiedString( + '\\' . $name + ) + ); + } + + foreach ($class->getTraitNames() as $name) { + // TODO: optionally, support getTraitAliases()? This is low importance for internal PHP modules, + // it would be uncommon to see traits in internal PHP modules. + $clazz->addTraitFQSEN( + // @phan-suppress-next-line PhanThrowTypeAbsentForCall should be valid if extension is valid + FullyQualifiedClassName::fromFullyQualifiedString( + '\\' . $name + ) + ); + } + + foreach ($class->getConstants() as $name => $value) { + $constant_fqsen = FullyQualifiedClassConstantName::make( + $clazz->getFQSEN(), + $name + ); + + $constant = new ClassConstant( + $context, + $name, + Type::fromObject($value)->asRealUnionType(), // TODO: These can vary based on OS/build flags + 0, + $constant_fqsen + ); + $constant->setNodeForValue($value); + + $clazz->addConstant($code_base, $constant); + } + + foreach ($class->getMethods() as $reflection_method) { + if ($reflection_method->getDeclaringClass()->name !== $class_name) { + continue; + } + $method_context = $context->withScope($class_scope); + + $method_list = + FunctionFactory::methodListFromReflectionClassAndMethod( + $method_context, + $class, + $reflection_method + ); + + foreach ($method_list as $method) { + $clazz->addMethod($code_base, $method, None::instance()); + } + } + + return $clazz; + } + + /** + * @suppress PhanUndeclaredMethod + */ + private static function getRealTypeForReflectionProperty(ReflectionProperty $property): UnionType + { + if (\PHP_VERSION_ID >= 70400) { + if ($property->hasType()) { + return UnionType::fromReflectionType($property->getType()); + } + } + return UnionType::empty(); + } + + /** + * @param Type $parent_type + * The type of the parent (extended) class of this class. + */ + public function setParentType(Type $parent_type, int $lineno = 0): void + { + if ($this->getInternalScope()->hasAnyTemplateType()) { + // Get a reference to the local list of templated + // types. We'll use this to map templated types on the + // parent to locally templated types. + $template_type_map = + $this->getInternalScope()->getTemplateTypeMap(); + + // Figure out if the given parent type contains any template + // types. + $contains_templated_type = false; + foreach ($parent_type->getTemplateParameterTypeList() as $union_type) { + foreach ($union_type->getTypeSet() as $type) { + if (isset($template_type_map[$type->getName()])) { + $contains_templated_type = true; + break 2; + } + } + } + + // If necessary, map the template parameter type list through the + // local list of templated types. + if ($contains_templated_type) { + $parent_type = Type::fromType( + $parent_type, + \array_map(static function (UnionType $union_type) use ($template_type_map): UnionType { + return UnionType::of( + \array_map(static function (Type $type) use ($template_type_map): Type { + return $template_type_map[$type->getName()] ?? $type; + }, $union_type->getTypeSet()), + [] + ); + }, $parent_type->getTemplateParameterTypeList()) + ); + } + } + + $this->parent_type = $parent_type; + $this->parent_type_lineno = $lineno; + + // Add the parent to the union type of this class + $this->addAdditionalType($parent_type); + } + + /** + * @return bool + * True if this class has a parent class + */ + public function hasParentType(): bool + { + return $this->parent_type !== null; + } + + /** + * @return Option + * If a parent type is defined, get Some, else None. + */ + public function getParentTypeOption(): Option + { + if ($this->parent_type !== null) { + return new Some($this->parent_type); + } + + return None::instance(); + } + + /** + * @return FullyQualifiedClassName + * The parent class of this class if one exists + * + * @throws LogicException + * An exception is thrown if this class has no parent + */ + public function getParentClassFQSEN(): FullyQualifiedClassName + { + if (!$this->parent_type) { + throw new LogicException("Class $this has no parent"); + } + + return FullyQualifiedClassName::fromType($this->parent_type); + } + + /** + * @return Clazz + * The parent class of this class if defined + * + * @throws LogicException|RuntimeException + * An exception is thrown if this class has no parent + */ + public function getParentClass(CodeBase $code_base): Clazz + { + if (!$this->parent_type) { + throw new LogicException("Class $this has no parent"); + } + + $parent_fqsen = FullyQualifiedClassName::fromType($this->parent_type); + + // invoking hasClassWithFQSEN also has the side effect of lazily loading the parent class definition. + if (!$code_base->hasClassWithFQSEN($parent_fqsen)) { + throw new RuntimeException("Failed to load parent Class $parent_fqsen of Class $this"); + } + + return $code_base->getClassByFQSEN( + $parent_fqsen + ); + } + + /** + * @return Clazz + * The parent class of this class if defined (does not trigger class hydration of the parent class, unlike getParentClass) + * + * @throws LogicException|RuntimeException + * An exception is thrown if this class has no parent + */ + private function getParentClassWithoutHydrating(CodeBase $code_base): Clazz + { + if (!$this->parent_type) { + throw new LogicException("Class $this has no parent"); + } + + $parent_fqsen = FullyQualifiedClassName::fromType($this->parent_type); + + // invoking hasClassWithFQSEN also has the side effect of lazily loading the parent class definition. + if (!$code_base->hasClassWithFQSEN($parent_fqsen)) { + throw new RuntimeException("Failed to load parent Class $parent_fqsen of Class $this"); + } + + return $code_base->getClassByFQSENWithoutHydrating( + $parent_fqsen + ); + } + + /** + * @param list $mixin_types + */ + public function setMixinTypes(array $mixin_types): void + { + $this->mixin_types = $mixin_types; + } + + /** + * Is this a subclass of $other? + * + * This only checks parent classes. + * It should not be used for traits or interfaces. + * + * This returns false if $this === $other + * + * @deprecated This may lead to infinite recursion when analyzing invalid code. asExpandedTypes should be used instead. + * @suppress PhanUnreferencedPublicMethod + * @suppress PhanDeprecatedFunction + */ + public function isSubclassOf(CodeBase $code_base, Clazz $other): bool + { + if (!$this->hasParentType()) { + return false; + } + + if (!$code_base->hasClassWithFQSEN( + $this->getParentClassFQSEN() + )) { + // Let this emit an issue elsewhere for the + // parent not existing + return false; + } + + // Get the parent class + $parent = $this->getParentClass($code_base); + + if ($parent === $other) { + return true; + } + + return $parent->isSubclassOf($code_base, $other); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return int + * This class's depth in the class hierarchy + */ + public function getHierarchyDepth(CodeBase $code_base): int + { + if (!$this->hasParentType()) { + return 0; + } + + if (!$code_base->hasClassWithFQSEN( + $this->getParentClassFQSEN() + )) { + // Let this emit an issue elsewhere for the + // parent not existing + return 0; + } + + // Get the parent class + $parent = $this->getParentClass($code_base); + + // Prevent infinite loops + if ($parent === $this) { + return 0; + } + + return (1 + $parent->getHierarchyDepth($code_base)); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return FullyQualifiedClassName + * The FQSEN of the root class on this class's hierarchy + */ + public function getHierarchyRootFQSEN( + CodeBase $code_base + ): FullyQualifiedClassName { + $visited = []; + for ($current = $this; $current->hasParentType(); $current = $parent) { + $fqsen = $current->getFQSEN(); + + if (!$code_base->hasClassWithFQSEN( + $current->getParentClassFQSEN() + )) { + // Let this emit an issue elsewhere for the + // parent not existing + return $fqsen; + } + + // Get the parent class + $parent = $current->getParentClass($code_base); + $visited[$fqsen->__toString()] = true; + + // Prevent infinite loops + if (\array_key_exists($parent->getFQSEN()->__toString(), $visited)) { + return $fqsen; + } + } + return $current->getFQSEN(); + } + + /** + * Add the given FQSEN to the list of implemented + * interfaces for this class. + * + * @param FullyQualifiedClassName $fqsen + */ + public function addInterfaceClassFQSEN(FullyQualifiedClassName $fqsen, int $lineno = 0): void + { + $this->interface_fqsen_lineno[count($this->interface_fqsen_list)] = $lineno; + $this->interface_fqsen_list[] = $fqsen; + + // Add the interface to the union type of this + // class + $this->addAdditionalType($fqsen->asType()); + } + + /** + * Get the list of interfaces implemented by this class + * @return list + */ + public function getInterfaceFQSENList(): array + { + return $this->interface_fqsen_list; + } + + /** + * Add a property to this class + * + * @param CodeBase $code_base + * A reference to the code base in which the ancestor exists + * + * @param Property $property + * The property to copy onto this class + * + * @param Option $type_option + * A possibly defined type used to define template + * parameter types when importing the property + * + * @param bool $from_trait + */ + public function addProperty( + CodeBase $code_base, + Property $property, + Option $type_option, + bool $from_trait = false + ): void { + // Ignore properties we already have + // TODO: warn about private properties in subclass overriding ancestor private property. + $property_name = $property->getName(); + if ($this->hasPropertyWithName($code_base, $property_name)) { + // TODO: Check if trait properties would be inherited first. + // TODO: Figure out semantics and use $from_trait? + self::checkPropertyCompatibility( + $code_base, + $property, + $this->getPropertyByName($code_base, $property_name) + ); + return; + } + + $property_fqsen = FullyQualifiedPropertyName::make( + $this->getFQSEN(), + $property_name + ); + + // TODO: defer template properties until the analysis phase? They might not be parsed or resolved yet. + $original_property_fqsen = $property->getFQSEN(); + if ($original_property_fqsen !== $property_fqsen) { + $property = clone($property); + $property->setFQSEN($property_fqsen); + if ($property->hasStaticInUnionType()) { + $property->inheritStaticUnionType($original_property_fqsen->getFullyQualifiedClassName(), $this->getFQSEN()); + } + + // Private properties of traits are accessible from the class that used that trait + // (as well as from within the trait itself). + // Also, for inheritance purposes, treat protected properties the same way. + if ($from_trait) { + $property->setDefiningFQSEN($property_fqsen); + } + + try { + // If we have a parent type defined, map the property's + // type through it + if ($type_option->isDefined() + && !$property->hasUnresolvedFutureUnionType() + && $property->getUnionType()->hasTemplateType() + ) { + $property->setUnionType( + $property->getUnionType()->withTemplateParameterTypeMap( + $type_option->get()->getTemplateParameterTypeMap( + $code_base + ) + ) + ); + } + } catch (IssueException $exception) { + Issue::maybeEmitInstance( + $code_base, + $property->getContext(), + $exception->getIssueInstance() + ); + } + } + + $code_base->addProperty($property); + } + + private static function checkPropertyCompatibility( + CodeBase $code_base, + Property $inherited_property, + Property $overriding_property + ): void { + $overriding_property->setIsOverride(true); + if ($inherited_property->isFromPHPDoc() || $inherited_property->isDynamicProperty() || + $overriding_property->isFromPHPDoc() || $overriding_property->isDynamicProperty()) { + return; + } + + if ($inherited_property->isStrictlyMoreVisibleThan($overriding_property)) { + if ($inherited_property->isPHPInternal()) { + if (!$overriding_property->checkHasSuppressIssueAndIncrementCount(Issue::PropertyAccessSignatureMismatchInternal)) { + Issue::maybeEmit( + $code_base, + new ElementContext($overriding_property), + Issue::PropertyAccessSignatureMismatchInternal, + $overriding_property->getFileRef()->getLineNumberStart(), + $overriding_property->asVisibilityAndFQSENString(), + $inherited_property->asVisibilityAndFQSENString() + ); + } + } else { + if (!$overriding_property->checkHasSuppressIssueAndIncrementCount(Issue::PropertyAccessSignatureMismatchInternal)) { + Issue::maybeEmit( + $code_base, + new ElementContext($overriding_property), + Issue::PropertyAccessSignatureMismatch, + $overriding_property->getFileRef()->getLineNumberStart(), + $overriding_property, + $inherited_property, + $inherited_property->getFileRef()->getFile(), + $inherited_property->getFileRef()->getLineNumberStart() + ); + } + } + } + // original_property is the one that the class is using. + // We added $property after that (so it likely in a base class, or a trait's property added after this property was added) + if ($overriding_property->isStatic() != $inherited_property->isStatic()) { + Issue::maybeEmit( + $code_base, + new ElementContext($overriding_property), + $overriding_property->isStatic() ? Issue::AccessNonStaticToStaticProperty : Issue::AccessStaticToNonStaticProperty, + $overriding_property->getFileRef()->getLineNumberStart(), + $inherited_property->asPropertyFQSENString(), + $overriding_property->asPropertyFQSENString() + ); + } + } + + /** + * @param array $magic_property_map mapping from property name to property + * @param CodeBase $code_base + * @return bool whether or not we defined the magic property map + */ + public function setMagicPropertyMap( + array $magic_property_map, + CodeBase $code_base + ): bool { + if (count($magic_property_map) === 0) { + return true; // Vacuously true. + } + $class_fqsen = $this->getFQSEN(); + $context = $this->internal_context; + foreach ($magic_property_map as $comment_parameter) { + // $phan_flags can be used to indicate if something is property-read or property-write + $phan_flags = $comment_parameter->getFlags(); + $property_name = $comment_parameter->getName(); + $property_fqsen = FullyQualifiedPropertyName::make( + $class_fqsen, + $property_name + ); + $original_union_type = $comment_parameter->getUnionType(); + $union_type = $original_union_type->withStaticResolvedInContext($context); + $property = new Property( + (clone($context))->withLineNumberStart($comment_parameter->getLine()), + $property_name, + $union_type, + 0, + $property_fqsen, + UnionType::empty() + ); + $property->setPHPDocUnionType($union_type); + if ($original_union_type !== $union_type) { + $phan_flags |= Flags::HAS_STATIC_UNION_TYPE; + } + $property->setPhanFlags($phan_flags | Flags::IS_FROM_PHPDOC); + + $this->addProperty($code_base, $property, None::instance()); + } + return true; + } + + /** + * @param array $magic_method_map mapping from method name to this. + * @param CodeBase $code_base + * @return bool whether or not we defined the magic method map + */ + public function setMagicMethodMap( + array $magic_method_map, + CodeBase $code_base + ): bool { + if (count($magic_method_map) === 0) { + return true; // Vacuously true. + } + $class_fqsen = $this->getFQSEN(); + $context = $this->internal_context; + $is_pure = $this->isPure(); + foreach ($magic_method_map as $comment_method) { + // $flags is the same as the flags for `public` and non-internal? + // Or \ast\flags\MODIFIER_PUBLIC. + $flags = \ast\flags\MODIFIER_PUBLIC; + if ($comment_method->isStatic()) { + $flags |= \ast\flags\MODIFIER_STATIC; + } + $method_name = $comment_method->getName(); + if ($this->hasMethodWithName($code_base, $method_name, true)) { + // No point, and this would hurt inference accuracy. + continue; + } + $method_fqsen = FullyQualifiedMethodName::make( + $class_fqsen, + $method_name + ); + $method_context = (clone($context))->withLineNumberStart($comment_method->getLine()); + $real_parameter_list = \array_map(static function (\Phan\Language\Element\Comment\Parameter $parameter) use ($method_context): Parameter { + return $parameter->asRealParameter($method_context); + }, $comment_method->getParameterList()); + $method = new Method( + $method_context, + $method_name, + $comment_method->getUnionType(), + $flags, + $method_fqsen, + $real_parameter_list + ); + + $method->setRealParameterList($real_parameter_list); + $method->setNumberOfRequiredParameters($comment_method->getNumberOfRequiredParameters()); + $method->setNumberOfOptionalParameters($comment_method->getNumberOfOptionalParameters()); + $method->setIsFromPHPDoc(true); + if ($is_pure && !$comment_method->isStatic()) { + $method->setIsPure(); + } + + $this->addMethod($code_base, $method, None::instance()); + } + return true; + } + + public function hasPropertyWithName( + CodeBase $code_base, + string $name + ): bool { + return $code_base->hasPropertyWithFQSEN( + FullyQualifiedPropertyName::make( + $this->getFQSEN(), + $name + ) + ); + } + + /** + * Returns the property $name of this class. + * @see self::hasPropertyWithName() + */ + public function getPropertyByName( + CodeBase $code_base, + string $name + ): Property { + return $code_base->getPropertyByFQSEN( + FullyQualifiedPropertyName::make( + $this->getFQSEN(), + $name + ) + ); + } + + /** + * @param CodeBase $code_base + * A reference to the entire code base in which the + * property exists. + * + * @param string $name + * The name of the property + * + * @param Context $context + * The context of the caller requesting the property + * + * @return Property + * A property with the given name. + * Callers can check if the property is read-only when writing, + * or write-only when reading. + * + * @throws IssueException + * An exception may be thrown if the caller does not + * have access to the given property from the given + * context + */ + public function getPropertyByNameInContext( + CodeBase $code_base, + string $name, + Context $context, + bool $is_static, + Node $node = null, + bool $is_known_assignment = false + ): Property { + + // Get the FQSEN of the property we're looking for + $property_fqsen = FullyQualifiedPropertyName::make( + $this->getFQSEN(), + $name + ); + + $property = null; + + // Figure out if we have the property and + // figure out if the property is accessible. + $is_property_accessible = false; + if ($code_base->hasPropertyWithFQSEN($property_fqsen)) { + $property = $code_base->getPropertyByFQSEN( + $property_fqsen + ); + if ($is_static !== $property->isStatic()) { + if ($is_static) { + throw new IssueException( + Issue::fromType(Issue::AccessPropertyNonStaticAsStatic)( + $context->getFile(), + $context->getLineNumberStart(), + [$property->asPropertyFQSENString()] + ) + ); + } else { + throw new IssueException( + Issue::fromType(Issue::AccessPropertyStaticAsNonStatic)( + $context->getFile(), + $context->getLineNumberStart(), + [$property->asPropertyFQSENString()] + ) + ); + } + } + + $is_property_accessible = $property->isAccessibleFromClass( + $code_base, + $context->getClassFQSENOrNull() + ); + } + if ($is_static && $property) { + // If the property is from a trait, the (different) defining FQSEN is the FQSEN of the class using the FQSEN, not the trait. + $defining_fqsen = $property->getDefiningFQSEN(); + if ($defining_fqsen !== $property_fqsen) { + if ($code_base->hasPropertyWithFQSEN($defining_fqsen)) { + $property = $code_base->getPropertyByFQSEN($defining_fqsen); + } + } + } + + // If the property exists and is accessible, return it + if ($is_property_accessible) { + // @phan-suppress-next-line PhanTypeMismatchReturnNullable is_property_accessible ensures that this is non-null + return $property; + } + + // Check to see if we can use a __get magic method + // TODO: What about __set? + if (!$is_static && $this->hasMethodWithName($code_base, '__get', true)) { + $method = $this->getMethodByName($code_base, '__get'); + + // Make sure the magic method is accessible + // TODO: Add defined at %s:%d for the property definition + if ($method->isPrivate()) { + throw new IssueException( + Issue::fromType(Issue::AccessPropertyPrivate)( + $context->getFile(), + $context->getLineNumberStart(), + [ + $property ? $property->asPropertyFQSENString() : $property_fqsen, + $method->getContext()->getFile(), + $method->getContext()->getLineNumberStart() + ] + ) + ); + } elseif ($method->isProtected()) { + if (!self::isAccessToElementOfThis($node)) { + throw new IssueException( + Issue::fromType(Issue::AccessPropertyProtected)( + $context->getFile(), + $context->getLineNumberStart(), + [ + $property ? $property->asPropertyFQSENString() : $property_fqsen, + $method->getContext()->getFile(), + $method->getContext()->getLineNumberStart() + ] + ) + ); + } + } + + $property = new Property( + $context, + $name, + $method->getUnionType(), + 0, + $property_fqsen, + UnionType::empty() + ); + $property->setIsDynamicProperty(true); + + $this->addProperty($code_base, $property, None::instance()); + + return $property; + } elseif ($property) { + // If we have a property, but it's inaccessible, emit + // an issue + if ($property->isPrivate()) { + throw new IssueException( + Issue::fromType(Issue::AccessPropertyPrivate)( + $context->getFile(), + $context->getLineNumberStart(), + [$property->asPropertyFQSENString(), $property->getContext()->getFile(), $property->getContext()->getLineNumberStart() ], + $this->suggestGettersOrSetters($code_base, $context, $property, $is_known_assignment) + ) + ); + } + if ($property->isProtected()) { + if (self::isAccessToElementOfThis($node)) { + return $property; + } + throw new IssueException( + Issue::fromType(Issue::AccessPropertyProtected)( + $context->getFile(), + $context->getLineNumberStart(), + [$property->asPropertyFQSENString(), $property->getContext()->getFile(), $property->getContext()->getLineNumberStart() ], + $this->suggestGettersOrSetters($code_base, $context, $property, $is_known_assignment) + ) + ); + } + } + + // Check to see if missing properties are allowed + // or we're working with a class with dynamic + // properties such as stdClass. + if (!$is_static && (Config::getValue('allow_missing_properties') + || $this->hasDynamicProperties($code_base)) + ) { + $property = new Property( + $context, + $name, + UnionType::empty(), + 0, + $property_fqsen, + UnionType::empty() + ); + $property->setIsDynamicProperty(true); + + $this->addProperty($code_base, $property, None::instance()); + + return $property; + } + + throw new IssueException( + Issue::fromType(Issue::UndeclaredProperty)( + $context->getFile(), + $context->getLineNumberStart(), + [$this->getFQSEN() . ($is_static ? '::$' : '->') . $name], + IssueFixSuggester::suggestSimilarProperty($code_base, $context, $this, $name, $is_static) + ) + ); + } + + private function suggestGettersOrSetters(CodeBase $code_base, Context $context, Property $property, bool $is_known_assignment): ?Suggestion + { + if ($is_known_assignment) { + return $this->suggestSetters($code_base, $context, $property); + } else { + return $this->suggestGetters($code_base, $context, $property); + } + } + + private function suggestSetters(CodeBase $code_base, Context $context, Property $property): ?Suggestion + { + $getters = $this->getSettersMap($code_base)[$property->getName()] ?? []; + if (!$getters) { + return null; + } + $suggestions = []; + // @phan-suppress-next-line PhanAccessMethodInternal + $class_fqsen_in_current_scope = IssueFixSuggester::maybeGetClassInCurrentScope($context); + foreach ($getters as $method) { + if ($method->isAccessibleFromClass($code_base, $class_fqsen_in_current_scope)) { + $suggestions[] = $method->getRepresentationForIssue(); + } + } + if (!$suggestions) { + return null; + } + \sort($suggestions, \SORT_STRING); + return Suggestion::fromString('Did you mean ' . \implode(' or ', $suggestions)); + } + + /** + * @return array> maps property names to setters for that property + */ + private function getSettersMap(CodeBase $code_base): array + { + return $this->memoize( + __METHOD__, + /** + * @return array> maps property names to setters for that property (both instance and static properties) + */ + function () use ($code_base): array { + if ($this->isPHPInternal()) { + return []; + } + $setters = []; + foreach ($this->getMethodMap($code_base) as $method) { + if ($method->isStatic()) { + continue; + } + if ($method->getNumberOfParameters() === 0) { + continue; + } + $node = $method->getNode()->children['stmts'] ?? null; + if (!$node instanceof Node) { + continue; + } + $first_parameter = $method->getParameterList()[0] ?? null; + if (!$first_parameter) { + // func_get + continue; + } + $fetched_property_name = self::computeSetPropertyName($node, $first_parameter->getName()); + if (is_string($fetched_property_name)) { + $setters[$fetched_property_name][] = $method; + } + } + return $setters; + } + ); + } + + private function suggestGetters(CodeBase $code_base, Context $context, Property $property): ?Suggestion + { + $getters = $this->getGettersMap($code_base)[$property->getName()] ?? []; + if (!$getters) { + return null; + } + $suggestions = []; + // @phan-suppress-next-line PhanAccessMethodInternal + $class_fqsen_in_current_scope = IssueFixSuggester::maybeGetClassInCurrentScope($context); + foreach ($getters as $method) { + if ($method->isAccessibleFromClass($code_base, $class_fqsen_in_current_scope)) { + $suggestions[] = $method->getRepresentationForIssue(); + } + } + if (!$suggestions) { + return null; + } + return Suggestion::fromString('Did you mean ' . \implode(' or ', $suggestions)); + } + + /** + * @return array> maps property names to getters for that property + */ + public function getGettersMap(CodeBase $code_base): array + { + if ($this->isInterface()) { + return []; + } + return $this->memoize( + __METHOD__, + /** + * @return array> maps property names to getters for that property (for instance properties) + */ + function () use ($code_base): array { + if ($this->isPHPInternal()) { + return []; + } + + // Hydrate the class so that getters from ancestor classes will also be accessible + $this->hydrate($code_base); + $getters = []; + foreach ($this->getMethodMap($code_base) as $method) { + if ($method->isStatic()) { + // TODO support static getters for static properties + continue; + } + $node = $method->getNode()->children['stmts'] ?? null; + if (!$node instanceof Node) { + continue; + } + $fetched_property_name = self::computeFetchedPropertyName($node); + if (is_string($fetched_property_name)) { + $getters[$fetched_property_name][] = $method; + } + } + return $getters; + } + ); + } + + private static function computeFetchedPropertyName(Node $node): ?string + { + if (count($node->children) !== 1) { + return null; + } + $stmt = $node->children[0]; + if (!$stmt instanceof Node || $stmt->kind !== ast\AST_RETURN) { + return null; + } + return self::getPropName($stmt->children['expr']); + } + + /** + * Returns the name of the instance property set to the parameter with name $expected_parameter_name, if this is a setter + */ + private static function computeSetPropertyName(Node $node, string $expected_parameter_name): ?string + { + if (count($node->children) !== 1) { + return null; + } + $stmt = $node->children[0]; + if (!$stmt instanceof Node || $stmt->kind !== ast\AST_ASSIGN) { + return null; + } + $prop_name = self::getPropName($stmt->children['var']); + if (!is_string($prop_name)) { + return null; + } + $expr = $stmt->children['expr']; + if (!$expr instanceof Node || $expr->kind !== ast\AST_VAR) { + return null; + } + if ($expr->children['name'] === $expected_parameter_name) { + return $prop_name; + } + return null; + } + + /** + * @param Node|string|int|float|null $node + */ + private static function getPropName($node): ?string + { + if (!$node instanceof Node) { + return null; + } + if ($node->kind !== ast\AST_PROP) { + return null; + } + $obj = $node->children['expr']; + if (!($obj instanceof Node && $obj->kind === ast\AST_VAR && + $obj->children['name'] === 'this')) { + return null; + } + $prop = $node->children['prop']; + return is_string($prop) ? $prop : null; + } + + /** + * Returns true if this is an access to a property or method of self/static/$this + * + * @param ?Node $node + */ + public static function isAccessToElementOfThis(?Node $node): bool + { + if (!($node instanceof Node)) { + return false; + } + $node = $node->children['expr'] ?? $node->children['class']; + if (!($node instanceof Node)) { + return false; + } + switch ($node->kind) { + case ast\AST_VAR: + $name = $node->children['name']; + return is_string($name) && $name === 'this'; + case ast\AST_CONST: + $name = $node->children['name']->children['name'] ?? null; + return is_string($name) && \strcasecmp($name, 'static') === 0; + default: + return false; + } + } + + /** + * @return array + * The list of properties on this class + */ + public function getPropertyMap(CodeBase $code_base): array + { + return $code_base->getPropertyMapByFullyQualifiedClassName( + $this->getFQSEN() + ); + } + + /** + * Inherit a class constant from an ancestor class + */ + public function inheritConstant( + CodeBase $code_base, + ClassConstant $constant + ): void { + $constant_fqsen = FullyQualifiedClassConstantName::make( + $this->getFQSEN(), + $constant->getName() + ); + + if ($code_base->hasClassConstantWithFQSEN($constant_fqsen)) { + // If the constant with that name already exists, mark it as an override. + $overriding_constant = $code_base->getClassConstantByFQSEN($constant_fqsen); + $overriding_constant->setIsOverride(true); + self::checkConstantCompatibility( + $code_base, + $constant, + $code_base->getClassConstantByFQSEN( + $constant_fqsen + ) + ); + return; + } + // Warn if inheriting a class constant declared as @abstract without overriding it. + // Optionally, could check if other interfaces declared class constants with the same value, but low priority. + if ($constant->isPHPDocAbstract() && !$constant->isPrivate() && !$this->isAbstract() && $this->isClass()) { + Issue::maybeEmit( + $code_base, + $this->getContext(), + Issue::CommentAbstractOnInheritedConstant, + $this->getContext()->getLineNumberStart(), + $this->getFQSEN(), + $constant->getRealDefiningFQSEN(), + $constant->getContext()->getFile(), + $constant->getContext()->getLineNumberStart(), + '@abstract' + ); + } + + // Update the FQSEN if it's not associated with this + // class yet (always true) + if ($constant->getFQSEN() !== $constant_fqsen) { + $constant = clone($constant); + $constant->setFQSEN($constant_fqsen); + } + + $code_base->addClassConstant($constant); + } + + private static function checkConstantCompatibility( + CodeBase $code_base, + ClassConstant $inherited_constant, + ClassConstant $overriding_constant + ): void { + // Traits don't have constants, thankfully, so the logic is simple. + if ($inherited_constant->isStrictlyMoreVisibleThan($overriding_constant)) { + if ($inherited_constant->isPHPInternal()) { + if (!$overriding_constant->checkHasSuppressIssueAndIncrementCount(Issue::ConstantAccessSignatureMismatchInternal)) { + Issue::maybeEmit( + $code_base, + $overriding_constant->getContext(), + Issue::ConstantAccessSignatureMismatchInternal, + $overriding_constant->getFileRef()->getLineNumberStart(), + $overriding_constant->asVisibilityAndFQSENString(), + $inherited_constant->asVisibilityAndFQSENString() + ); + } + } else { + if (!$overriding_constant->checkHasSuppressIssueAndIncrementCount(Issue::ConstantAccessSignatureMismatchInternal)) { + Issue::maybeEmit( + $code_base, + $overriding_constant->getContext(), + Issue::ConstantAccessSignatureMismatch, + $overriding_constant->getFileRef()->getLineNumberStart(), + $overriding_constant->asVisibilityAndFQSENString(), + $inherited_constant->asVisibilityAndFQSENString(), + $inherited_constant->getFileRef()->getFile(), + $inherited_constant->getFileRef()->getLineNumberStart() + ); + } + } + } + } + + + /** + * Add a class constant + */ + public function addConstant( + CodeBase $code_base, + ClassConstant $constant + ): void { + $constant_fqsen = FullyQualifiedClassConstantName::make( + $this->getFQSEN(), + $constant->getName() + ); + + // Update the FQSEN if it's not associated with this + // class yet + if ($constant->getFQSEN() !== $constant_fqsen) { + $constant = clone($constant); + $constant->setFQSEN($constant_fqsen); + } + + $code_base->addClassConstant($constant); + } + + /** + * @return bool + * True if a constant with the given name is defined + * on this class. + */ + public function hasConstantWithName( + CodeBase $code_base, + string $name + ): bool { + if ($code_base->hasClassConstantWithFQSEN( + FullyQualifiedClassConstantName::make( + $this->getFQSEN(), + $name + ) + )) { + return true; + } + if (!$this->hydrateConstantsIndicatingFirstTime($code_base)) { + return false; + } + return $code_base->hasClassConstantWithFQSEN( + FullyQualifiedClassConstantName::make( + $this->getFQSEN(), + $name + ) + ); + } + + /** + * @param CodeBase $code_base + * A reference to the entire code base in which the + * property exists. + * + * @param string $name + * The name of the class constant + * + * @param Context $context + * The context of the caller requesting the property + * + * @return ClassConstant + * A constant with the given name + * + * @throws IssueException + * An exception may be thrown if the caller does not + * have access to the given property from the given + * context + */ + public function getConstantByNameInContext( + CodeBase $code_base, + string $name, + Context $context + ): ClassConstant { + + $constant_fqsen = FullyQualifiedClassConstantName::make( + $this->getFQSEN(), + $name + ); + + if (!$code_base->hasClassConstantWithFQSEN($constant_fqsen)) { + throw new IssueException( + Issue::fromType(Issue::UndeclaredConstantOfClass)( + $context->getFile(), + $context->getLineNumberStart(), + [ + $this->getFQSEN() . '::' . $constant_fqsen + ], + IssueFixSuggester::suggestSimilarClassConstant($code_base, $context, $constant_fqsen) + ) + ); + } + + $constant = $code_base->getClassConstantByFQSEN( + $constant_fqsen + ); + + if ($constant->isPublic()) { + // Most constants are public, check that first. + return $constant; + } + + // Visibility checks for private/protected class constants: + + // Are we within a class referring to the class + // itself? + $is_local_access = ( + $context->isInClassScope() + && $context->getClassInScope($code_base) === $constant->getClass($code_base) + ); + + if ($is_local_access) { + // Classes can always access constants declared in the same class + return $constant; + } + + if ($constant->isPrivate()) { + // This is attempting to access a private constant from outside of the class + throw new IssueException( + Issue::fromType(Issue::AccessClassConstantPrivate)( + $context->getFile(), + $context->getLineNumberStart(), + [ + (string)$constant_fqsen, + $constant->getContext()->getFile(), + $constant->getContext()->getLineNumberStart() + ] + ) + ); + } + + // We now know that $constant is a protected constant + + // Are we within a class or an extending sub-class + // referring to the class? + $is_remote_access = $context->isInClassScope() + && $context->getClassInScope($code_base) + ->getUnionType()->canCastToExpandedUnionType( + $this->getUnionType(), + $code_base + ); + + if (!$is_remote_access) { + // And the access is not from anywhere on the class hierarchy, so throw + throw new IssueException( + Issue::fromType(Issue::AccessClassConstantProtected)( + $context->getFile(), + $context->getLineNumberStart(), + [ + (string)$constant_fqsen, + $constant->getContext()->getFile(), + $constant->getContext()->getLineNumberStart() + ] + ) + ); + } + + // Valid access to a protected constant. + return $constant; + } + + /** + * @return array + * The constants associated with this class + */ + public function getConstantMap(CodeBase $code_base): array + { + return $code_base->getClassConstantMapByFullyQualifiedClassName( + $this->getFQSEN() + ); + } + + /** + * Add a method to this class + * + * @param CodeBase $code_base + * A reference to the code base in which the ancestor exists + * + * @param Method $method + * The method to copy onto this class + * + * @param Option $type_option + * A possibly defined type used to define template + * parameter types when importing the method + */ + public function addMethod( + CodeBase $code_base, + Method $method, + Option $type_option + ): void { + $method_fqsen = FullyQualifiedMethodName::make( + $this->getFQSEN(), + $method->getName(), + $method->getFQSEN()->getAlternateId() + ); + + $is_override = $code_base->hasMethodWithFQSEN($method_fqsen); + // Don't overwrite overridden methods with + // parent methods + if ($is_override) { + // Note that we're overriding something + // (but only do this if it's abstract) + // TODO: Consider all permutations of abstract and real methods on classes, interfaces, and traits. + $existing_method = + $code_base->getMethodByFQSEN($method_fqsen); + $existing_method_defining_fqsen = $existing_method->getDefiningFQSEN(); + // Note: For private/protected methods, the defining FQSEN is set to the FQSEN of the inheriting class. + // So, when multiple traits are inherited, they may have identical defining FQSENs, but some may be abstract, and others may be implemented. + if ($method->getDefiningFQSEN() === $existing_method_defining_fqsen) { + if ($method->isAbstract() === $existing_method->isAbstract()) { + return; + } + } else { + self::markMethodAsOverridden($code_base, $existing_method_defining_fqsen); + } + + if ($existing_method->getRealDefiningFQSEN() === $method->getRealDefiningFQSEN()) { + return; + } + if ($existing_method->getRealDefiningFQSEN() === $method_fqsen || $method->isAbstract() || !$existing_method->isAbstract() || $existing_method->isNewConstructor()) { + // TODO: What if both of these are abstract, and those get combined into an abstract class? + // Should phan check compatibility of the abstract methods it inherits? + $existing_method->setIsOverride(true); + // TODO: What happens for protected methods and traits with getDefiningFQSEN + self::markMethodAsOverridden($code_base, $method->getDefiningFQSEN()); + + // Don't add the method since it was already added + return; + } elseif ($method->getRealDefiningFQSEN() === $method_fqsen) { + $method->setIsOverride(true); + // TODO: What happens for traits with getDefiningFQSEN + self::markMethodAsOverridden($code_base, $existing_method->getDefiningFQSEN()); + } + } + + if ($method->getFQSEN() !== $method_fqsen) { + $original_method = $method; + $method = clone($method); + $method->setFQSEN($method_fqsen); + // When we inherit it from the ancestor class, it may be an override in the ancestor class, + // but that doesn't imply it's an override in *this* class. + $method->setIsOverride($is_override); + $method->setIsOverriddenByAnother(false); + + // Clone the parameter list, so that modifying the parameters on the first call won't modify the others. + $method->cloneParameterList(); + $method->ensureClonesReturnType($original_method); + + // If we have a parent type defined, map the method's + // return type and parameter types through it + if ($type_option->isDefined()) { + // Map the method's return type + if ($method->getUnionType()->hasTemplateType()) { + $method->setUnionType( + $method->getUnionType()->withTemplateParameterTypeMap( + $type_option->get()->getTemplateParameterTypeMap( + $code_base + ) + ) + ); + } + + // Map each method parameter + $method->setParameterList( + \array_map(static function (Parameter $parameter) use ($type_option, $code_base): Parameter { + + if (!$parameter->getUnionType()->hasTemplateType()) { + return $parameter; + } + + $mapped_parameter = clone($parameter); + + $mapped_parameter->setUnionType( + $mapped_parameter->getUnionType()->withTemplateParameterTypeMap( + $type_option->get()->getTemplateParameterTypeMap( + $code_base + ) + ) + ); + + return $mapped_parameter; + }, $method->getParameterList()) + ); + } + } + if ($method->hasYield()) { + // There's no phpdoc standard for template types of Generators at the moment. + $new_type = UnionType::fromFullyQualifiedRealString('\\Generator'); + if (!$new_type->canCastToUnionType($method->getUnionType())) { + $method->setUnionType($new_type); + } + } + + // Methods defined on interfaces are always abstract, but don't have that flag set. + // NOTE: __construct is special for the following reasons: + // 1. We automatically add __construct to class-like definitions (Not sure why it's done for interfaces) + // 2. If it's abstract, then PHP would enforce that signatures are compatible + if ($this->isInterface() && !$method->isNewConstructor()) { + $method->setFlags(Flags::bitVectorWithState($method->getFlags(), \ast\flags\MODIFIER_ABSTRACT, true)); + } + + if ($is_override) { + $method->setIsOverride(true); + } + + $code_base->addMethod($method); + } + + /** + * @param bool $is_direct_invocation @phan-mandatory-param + * @return bool + * True if this class has a method with the given name + */ + public function hasMethodWithName( + CodeBase $code_base, + string $name, + bool $is_direct_invocation = false + ): bool { + // All classes have a constructor even if it hasn't + // been declared yet + if (!$is_direct_invocation && ('__construct' === \strtolower($name) && !$this->isTrait())) { + return true; + } + + $method_fqsen = FullyQualifiedMethodName::make( + $this->getFQSEN(), + $name + ); + + if ($code_base->hasMethodWithFQSEN($method_fqsen)) { + return true; + } + if (!$this->hydrateIndicatingFirstTime($code_base)) { + return false; + } + return $code_base->hasMethodWithFQSEN($method_fqsen); + } + + /** + * @return Method + * The method with the given name + * + * @throws CodeBaseException if the method (or a placeholder) could not be found (or created) + */ + public function getMethodByName( + CodeBase $code_base, + string $name + ): Method { + $method_fqsen = FullyQualifiedMethodName::make( + $this->getFQSEN(), + $name + ); + + if (!$code_base->hasMethodWithFQSEN($method_fqsen)) { + if ('__construct' === $name) { + // Create a default constructor if it's requested + // but doesn't exist yet + $default_constructor = + Method::defaultConstructorForClass( + $this, + $code_base + ); + + $this->addMethod($code_base, $default_constructor, $this->getParentTypeOption()); + + return $default_constructor; + } + + throw new CodeBaseException( + $method_fqsen, + "Method with name $name does not exist for class {$this->getFQSEN()}." + ); + } + + return $code_base->getMethodByFQSEN($method_fqsen); + } + + /** + * @return array + * A list of methods on this class + */ + public function getMethodMap(CodeBase $code_base): array + { + return $code_base->getMethodMapByFullyQualifiedClassName( + $this->getFQSEN() + ); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return bool + * True if this class has a magic '__call' method + */ + public function hasCallMethod(CodeBase $code_base): bool + { + return $this->hasMethodWithName($code_base, '__call', true); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return bool + * True if this class has a magic '__call' method, + * and (at)phan-forbid-undeclared-magic-methods doesn't exist on this class or ancestors + */ + public function allowsCallingUndeclaredInstanceMethod(CodeBase $code_base): bool + { + return $this->hasCallMethod($code_base) && + !$this->getForbidUndeclaredMagicMethods($code_base); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return Method + * The magic `__call` method + */ + public function getCallMethod(CodeBase $code_base): Method + { + return self::makeCallMethodCloneForCaller($this->getMethodByName($code_base, '__call')); + } + + private static function makeCallMethodCloneForCaller(Method $method): Method + { + $clone = new Method( + $method->getContext(), + $method->getName(), + $method->getUnionType(), + $method->getFlags(), + $method->getFQSEN(), + [ + new VariadicParameter($method->getContext(), 'args', UnionType::empty(), 0) + ] + ); + $clone->setPhanFlags($method->getPhanFlags()); + return $clone; + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return bool + * True if this class has a magic '__callStatic' method + */ + public function hasCallStaticMethod(CodeBase $code_base): bool + { + return $this->hasMethodWithName($code_base, '__callStatic', true); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return bool + * True if this class has a magic '__callStatic' method, + * and (at)phan-forbid-undeclared-magic-methods doesn't exist on this class or ancestors. + */ + public function allowsCallingUndeclaredStaticMethod(CodeBase $code_base): bool + { + return $this->hasCallStaticMethod($code_base) && + !$this->getForbidUndeclaredMagicMethods($code_base); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return Method + * The magic `__callStatic` method + */ + public function getCallStaticMethod(CodeBase $code_base): Method + { + return self::makeCallMethodCloneForCaller($this->getMethodByName($code_base, '__callStatic')); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return bool + * True if this class has a magic '__get' method + */ + public function hasGetMethod(CodeBase $code_base): bool + { + return $this->hasMethodWithName($code_base, '__get', true); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return bool + * True if this class has a magic '__set' method + */ + public function hasSetMethod(CodeBase $code_base): bool + { + return $this->hasMethodWithName($code_base, '__set', true); + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return bool + * True if this class has a magic '__get' or '__set' + * method + * @suppress PhanUnreferencedPublicMethod + */ + public function hasGetOrSetMethod(CodeBase $code_base): bool + { + return ( + $this->hasGetMethod($code_base) + || $this->hasSetMethod($code_base) + ); + } + + public function addTraitFQSEN(FullyQualifiedClassName $fqsen, int $lineno = 0): void + { + $this->trait_fqsen_lineno[count($this->trait_fqsen_list)] = $lineno; + $this->trait_fqsen_list[] = $fqsen; + + // Add the trait to the union type of this class + $this->addAdditionalType($fqsen->asType()); + } + + public function addTraitAdaptations(TraitAdaptations $trait_adaptations): void + { + $key = \strtolower($trait_adaptations->getTraitFQSEN()->__toString()); + $old_adaptations = $this->trait_adaptations_map[$key] ?? null; + if ($old_adaptations) { + $old_adaptations->alias_methods += $trait_adaptations->alias_methods; + $old_adaptations->hidden_methods += $trait_adaptations->hidden_methods; + } else { + $this->trait_adaptations_map[$key] = $trait_adaptations; + } + } + + /** + * @return list + * A list of FQSENs for included traits + */ + public function getTraitFQSENList(): array + { + return $this->trait_fqsen_list; + } + + /** + * True if this class calls its parent constructor + */ + public function isParentConstructorCalled(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_PARENT_CONSTRUCTOR_CALLED); + } + + public function setIsParentConstructorCalled( + bool $is_parent_constructor_called + ): void { + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_PARENT_CONSTRUCTOR_CALLED, + $is_parent_constructor_called + )); + } + + /** + * Check if this class or its ancestors forbids undeclared magic properties. + */ + public function getForbidUndeclaredMagicProperties(CodeBase $code_base): bool + { + return $this->hasFlagsRecursive($code_base, Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES); + } + + /** + * Set whether undeclared magic properties are forbidden + * (properties accessed through __get or __set, with no (at)property annotation on parent class) + * @param bool $forbid_undeclared_dynamic_properties - set to true to forbid. + * @suppress PhanUnreferencedPublicMethod + */ + public function setForbidUndeclaredMagicProperties( + bool $forbid_undeclared_dynamic_properties + ): void { + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES, + $forbid_undeclared_dynamic_properties + )); + } + + /** + * Check if this class or its ancestors forbids undeclared magic methods. + */ + public function getForbidUndeclaredMagicMethods(CodeBase $code_base): bool + { + return $this->hasFlagsRecursive($code_base, Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS); + } + + /** + * Set whether undeclared magic methods are forbidden + * (methods accessed through __call or __callStatic, with no (at)method annotation on class) + * @param bool $forbid_undeclared_magic_methods - set to true to forbid. + * @suppress PhanUnreferencedPublicMethod + */ + public function setForbidUndeclaredMagicMethods( + bool $forbid_undeclared_magic_methods + ): void { + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS, + $forbid_undeclared_magic_methods + )); + } + + /** + * Returns whether this class is `(at)immutable` + * + * This will warn if instance properties of instances of the class will not change after the object is constructed. + * - Methods of (at)immutable classes may change external state (e.g. perform I/O, modify other objects) + */ + public function isImmutable(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_READ_ONLY); + } + + /** + * Returns whether this class is `(at)pure` + * + * This will warn if instance properties of instances of the class will not change after the object is constructed. + * - Methods of (at)immutable classes may change external state (e.g. perform I/O, modify other objects) + */ + public function isPure(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_SIDE_EFFECT_FREE); + } + + /** + * @return bool + * True if this class has dynamic properties. (e.g. stdClass) + */ + public function hasDynamicProperties(CodeBase $code_base): bool + { + return $this->hasFlagsRecursive($code_base, Flags::CLASS_HAS_DYNAMIC_PROPERTIES); + } + + private function hasFlagsRecursive(CodeBase $code_base, int $flags): bool + { + $current = $this; + $checked = []; + while (true) { + if ($current->getPhanFlagsHasState($flags)) { + return true; + } + if (!$current->hasParentType() || !$code_base->hasClassWithFQSEN($current->getParentClassFQSEN())) { + return false; + } + $checked[$current->getFQSEN()->__toString()] = true; + $current = $current->getParentClass($code_base); + if (\array_key_exists($current->getFQSEN()->__toString(), $checked)) { + // Prevent infinite recursion. + return false; + } + } + } + + public function setHasDynamicProperties( + bool $has_dynamic_properties + ): void { + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::CLASS_HAS_DYNAMIC_PROPERTIES, + $has_dynamic_properties + )); + } + + /** + * @return bool + * True if this is a final class + */ + public function isFinal(): bool + { + return $this->getFlagsHasState(\ast\flags\CLASS_FINAL); + } + + /** + * @return bool + * True if this is an abstract class + */ + public function isAbstract(): bool + { + return $this->getFlagsHasState(\ast\flags\CLASS_ABSTRACT); + } + + /** + * @return bool + * True if this is an interface + */ + public function isInterface(): bool + { + return $this->getFlagsHasState(\ast\flags\CLASS_INTERFACE); + } + + /** + * @return bool + * True if this is a class (i.e. neither a trait nor an interface) + */ + public function isClass(): bool + { + return ($this->getFlags() & (ast\flags\CLASS_INTERFACE | ast\flags\CLASS_TRAIT)) === 0; + } + + /** + * @return bool + * True if this class is a trait + */ + public function isTrait(): bool + { + return $this->getFlagsHasState(\ast\flags\CLASS_TRAIT); + } + + /** + * @return bool + * True if this class is anonymous + */ + public function isAnonymous(): bool + { + return ($this->getFlags() & \ast\flags\CLASS_ANONYMOUS) > 0; + } + + /** + * @return FullyQualifiedClassName + */ + public function getFQSEN() + { + return $this->fqsen; + } + + /** + * @return list + */ + public function getNonParentAncestorFQSENList(): array + { + return \array_merge( + $this->interface_fqsen_list, + $this->trait_fqsen_list + ); + } + + /** + * @return list + */ + public function getAncestorFQSENList(): array + { + $ancestor_list = $this->getNonParentAncestorFQSENList(); + + if ($this->hasParentType()) { + $ancestor_list[] = $this->getParentClassFQSEN(); + } + + return $ancestor_list; + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @param list $fqsen_list + * A list of class FQSENs to turn into a list of + * Clazz objects + * + * @return list + */ + private static function getClassListFromFQSENList( + CodeBase $code_base, + array $fqsen_list + ): array { + $class_list = []; + foreach ($fqsen_list as $fqsen) { + if ($code_base->hasClassWithFQSEN($fqsen)) { + $class_list[] = $code_base->getClassByFQSEN($fqsen); + } + } + return $class_list; + } + + /** + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + * + * @return list + */ + public function getAncestorClassList(CodeBase $code_base): array + { + return self::getClassListFromFQSENList( + $code_base, + $this->getAncestorFQSENList() + ); + } + + /** + * Add class constants from all ancestors (parents, traits, ...) + * to this class + * + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + */ + public function importConstantsFromAncestorClasses(CodeBase $code_base): void + { + if (!$this->isFirstExecution(__METHOD__)) { + return; + } + + foreach ($this->interface_fqsen_list as $fqsen) { + if (!$code_base->hasClassWithFQSEN($fqsen)) { + continue; + } + + $ancestor = $code_base->getClassByFQSENWithoutHydrating($fqsen); + $this->importConstantsFromAncestorClass( + $code_base, + $ancestor + ); + } + + foreach ($this->trait_fqsen_list as $fqsen) { + if (!$code_base->hasClassWithFQSEN($fqsen)) { + continue; + } + + $ancestor = $code_base->getClassByFQSENWithoutHydrating($fqsen); + $this->importConstantsFromAncestorClass( + $code_base, + $ancestor + ); + } + + // Copy information from the parent(s) + $this->importConstantsFromParentClass($code_base); + } + + /** + * Add properties, constants and methods from all + * ancestors (parents, traits, ...) to this class + * + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + */ + public function importAncestorClasses(CodeBase $code_base): void + { + if (!$this->isFirstExecution(__METHOD__)) { + return; + } + $this->importConstantsFromAncestorClasses($code_base); + + foreach ($this->interface_fqsen_list as $i => $fqsen) { + if (!$code_base->hasClassWithFQSEN($fqsen)) { + continue; + } + + $ancestor = $code_base->getClassByFQSEN($fqsen); + + if (!$ancestor->isInterface()) { + $this->emitWrongInheritanceCategoryWarning($code_base, $ancestor, 'Interface', $this->interface_fqsen_lineno[$i] ?? 0); + } + + $this->importAncestorClass( + $code_base, + $ancestor, + None::instance() + ); + } + + foreach ($this->trait_fqsen_list as $i => $fqsen) { + if (!$code_base->hasClassWithFQSEN($fqsen)) { + continue; + } + + $ancestor = $code_base->getClassByFQSEN($fqsen); + if (!$ancestor->isTrait()) { + $this->emitWrongInheritanceCategoryWarning($code_base, $ancestor, 'Trait', $this->trait_fqsen_lineno[$i] ?? 0); + } + + $this->importAncestorClass( + $code_base, + $ancestor, + None::instance() + ); + } + + // Copy information from the parent(s) + $this->importParentClass($code_base); + + foreach ($this->mixin_types as $type) { + $this->importMixin($code_base, $type); + } + } + + public function getLinenoOfAncestorReference(FullyQualifiedClassName $fqsen): int + { + $class_line = $this->getFileRef()->getLineNumberStart(); + foreach ($this->interface_fqsen_list as $i => $interface_fqsen) { + if ($interface_fqsen === $fqsen) { + return $this->interface_fqsen_lineno[$i] ?? $class_line; + } + } + foreach ($this->trait_fqsen_list as $i => $trait_fqsen) { + if ($trait_fqsen === $fqsen) { + return $this->trait_fqsen_lineno[$i] ?? $class_line; + } + } + return $class_line; + } + + /** + * Import all methods of the other type as magic methods. + */ + private function importMixin(CodeBase $code_base, Type $type): void + { + $fqsen = FullyQualifiedClassName::fromType($type); + if (!$code_base->hasClassWithFQSEN($fqsen) || $fqsen === $this->fqsen) { + Issue::maybeEmit( + $code_base, + $this->internal_context, + Issue::InvalidMixin, + $this->internal_context->getLineNumberStart(), + $type + ); + return; + } + $class = $code_base->getClassByFQSEN($fqsen); + foreach ($class->getMethodMap($code_base) as $name => $method) { + if ($method->isMagic() || !$method->isPublic()) { + // Skip __invoke, and private/protected methods + continue; + } + if ($this->hasMethodWithName($code_base, $name, true)) { + continue; + } + // Treat it as if all of the methods were added, with their real and phpdoc union types. + $this->addMethod($code_base, $method->asPHPDocMethod($this), None::instance()); + } + foreach ($class->getPropertyMap($code_base) as $name => $property) { + if (!$property->isPublic() || $property->isStatic()) { + // Skip private/protected/static properties. There's no __getStatic(). + continue; + } + if ($property->isDynamicProperty()) { + continue; + } + if ($this->hasPropertyWithName($code_base, $name)) { + continue; + } + // Treat it as if all of the properties were added, with their real and phpdoc union types. + // TODO: Finalize behavior for edge cases such as `static` and templates in union types + $new_property = clone($property); + $new_property->setFQSEN(FullyQualifiedPropertyName::make($this->getFQSEN(), $name)); + $new_property->setPhanFlags($new_property->getPhanFlags() | Flags::IS_FROM_PHPDOC); + $this->addProperty($code_base, $new_property, None::instance()); + } + } + + /** + * Add constants from the parent of this class + * + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + */ + private function importConstantsFromParentClass(CodeBase $code_base): void + { + if (!$this->isFirstExecution(__METHOD__)) { + return; + } + + if (!$this->hasParentType()) { + return; + } + + if ($this->getParentClassFQSEN() === $this->getFQSEN()) { + return; + } + + // Let the parent class finder worry about this + if (!$code_base->hasClassWithFQSEN( + $this->getParentClassFQSEN() + )) { + return; + } + + // Get the parent class + $parent = $this->getParentClassWithoutHydrating($code_base); + + // import constants from that class + $this->importConstantsFromAncestorClass($code_base, $parent); + } + + /** + * Add properties, constants and methods from the + * parent of this class + * + * @param CodeBase $code_base + * The entire code base from which we'll find ancestor + * details + */ + private function importParentClass(CodeBase $code_base): void + { + if (!$this->isFirstExecution(__METHOD__)) { + return; + } + + if (!$this->hasParentType()) { + return; + } + + if ($this->getParentClassFQSEN() === $this->getFQSEN()) { + return; + } + + // Let the parent class finder worry about this + if (!$code_base->hasClassWithFQSEN( + $this->getParentClassFQSEN() + )) { + return; + } + + // Get the parent class + $parent = $this->getParentClass($code_base); + + if (!$parent->isClass()) { + $this->emitWrongInheritanceCategoryWarning($code_base, $parent, 'Class', $this->parent_type_lineno); + } + if ($parent->isFinal()) { + $this->emitExtendsFinalClassWarning($code_base, $parent); + } + + // Tell the parent to import its own parents first + + // Import elements from the parent + $this->importAncestorClass( + $code_base, + $parent, + $this->getParentTypeOption() + ); + } + + private function emitWrongInheritanceCategoryWarning( + CodeBase $code_base, + Clazz $ancestor, + string $expected_inheritance_category, + int $lineno + ): void { + $context = $this->getContext(); + if ($ancestor->isPHPInternal()) { + if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::AccessWrongInheritanceCategoryInternal)) { + Issue::maybeEmit( + $code_base, + $context, + Issue::AccessWrongInheritanceCategoryInternal, + $lineno ?: $context->getLineNumberStart(), + (string)$ancestor, + $expected_inheritance_category + ); + } + } else { + if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::AccessWrongInheritanceCategory)) { + Issue::maybeEmit( + $code_base, + $context, + Issue::AccessWrongInheritanceCategory, + $lineno ?: $context->getLineNumberStart(), + (string)$ancestor, + $ancestor->getFileRef()->getFile(), + $ancestor->getFileRef()->getLineNumberStart(), + $expected_inheritance_category + ); + } + } + } + + private function emitExtendsFinalClassWarning( + CodeBase $code_base, + Clazz $ancestor + ): void { + $context = $this->getContext(); + if ($ancestor->isPHPInternal()) { + if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::AccessExtendsFinalClassInternal)) { + Issue::maybeEmit( + $code_base, + $context, + Issue::AccessExtendsFinalClassInternal, + $this->parent_type_lineno ?: $context->getLineNumberStart(), + (string)$ancestor->getFQSEN() + ); + } + } else { + if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::AccessExtendsFinalClass)) { + Issue::maybeEmit( + $code_base, + $context, + Issue::AccessExtendsFinalClass, + $this->parent_type_lineno ?: $context->getLineNumberStart(), + (string)$ancestor->getFQSEN(), + $ancestor->getFileRef()->getFile(), + $ancestor->getFileRef()->getLineNumberStart() + ); + } + } + } + + /** + * Add constants from the given class to this. + * + * @param CodeBase $code_base + * A reference to the code base in which the ancestor exists + * + * @param Clazz $class + * A class to import from + */ + public function importConstantsFromAncestorClass( + CodeBase $code_base, + Clazz $class + ): void { + $key = \strtolower((string)$class->getFQSEN()); + if (!$this->isFirstExecution( + __METHOD__ . ':' . $key + )) { + return; + } + + $class->addReference($this->getContext()); + + // Make sure that the class imports its parents' constants first + // (And **only** the constants) + $class->hydrateConstants($code_base); + + // Copy constants + foreach ($class->getConstantMap($code_base) as $constant) { + $this->inheritConstant($code_base, $constant); + } + } + + /** + * @param FileRef $file_ref + * A reference to a location in which this typed structural + * element is referenced. + * @override + */ + public function addReference(FileRef $file_ref): void + { + if (Config::get_track_references()) { + // Currently, we don't need to track references to PHP-internal methods/functions/constants + // such as PHP_VERSION, strlen(), Closure::bind(), etc. + // This may change in the future. + if ($this->isPHPInternal()) { + return; + } + if ($file_ref instanceof Context) { + if ($file_ref->getClassFQSENOrNull() === $this->fqsen) { + // Don't count references declared within MyClass as references to MyClass for dead code detection + return; + } + } + $this->reference_list[$file_ref->__toString()] = $file_ref; + } + } + + /** + * Add properties, constants and methods from the given + * class to this. + * + * @param CodeBase $code_base + * A reference to the code base in which the ancestor exists + * + * @param Clazz $class + * A class to import from + * + * @param Option $type_option + * A possibly defined ancestor type used to define template + * parameter types when importing ancestor properties and + * methods + */ + public function importAncestorClass( + CodeBase $code_base, + Clazz $class, + Option $type_option + ): void { + $class_fqsen = $class->getFQSEN(); + $key = \strtolower($class_fqsen->__toString()); + if (!$this->isFirstExecution( + __METHOD__ . ':' . $key + )) { + return; + } + $next_class_fqsen = $class_fqsen->withAlternateId($class_fqsen->getAlternateId() + 1); + if (!$this->isPHPInternal() && $code_base->hasClassWithFQSEN($next_class_fqsen)) { + $this->warnAboutAmbiguousInheritance($code_base, $class, $next_class_fqsen); + } + + // Constants should have been imported earlier, but call it again just in case + $this->importConstantsFromAncestorClass($code_base, $class); + + // Make sure that the class imports its parents first + // NOTE: We already imported constants from $class in importConstantsFromAncestorClass + $class->hydrate($code_base); + $is_trait = $class->isTrait(); + $trait_adaptations = $is_trait ? ($this->trait_adaptations_map[$key] ?? null) : null; + + // Copy properties + foreach ($class->getPropertyMap($code_base) as $property) { + if ($property->isPHPDocAbstract() && !$property->isPrivate() && + $this->isClass() && !$this->isAbstract() && !$this->hasPropertyWithName($code_base, $property->getName())) { + Issue::maybeEmit( + $code_base, + $this->getContext(), + Issue::CommentAbstractOnInheritedProperty, + $this->getContext()->getLineNumberStart(), + $this->getFQSEN(), + $property->getRealDefiningFQSEN(), + $property->getContext()->getFile(), + $property->getContext()->getLineNumberStart(), + '@abstract' + ); + } + + // TODO: check for conflicts in visibility and default values for traits. + // TODO: Check for ancestor classes with the same private property? + $this->addProperty( + $code_base, + $property, + $type_option, + $is_trait + ); + } + + // Copy methods + foreach ($class->getMethodMap($code_base) as $method) { + if (!\is_null($trait_adaptations) && count($trait_adaptations->hidden_methods) > 0) { + $method_name_key = \strtolower($method->getName()); + if (isset($trait_adaptations->hidden_methods[$method_name_key])) { + // TODO: Record that the method was hidden, and check later on that all method that were hidden were actually defined? + continue; + } + } + // Workaround: For private methods, copy the method with a new defining class. + // If you import a trait's private method, it becomes private **to the class which used the trait** in PHP code. + // (But preserving the defining FQSEN is fine for this) + if ($is_trait) { + $method = $this->adaptInheritedMethodFromTrait($method); + } + $this->addMethod( + $code_base, + $method, + $type_option + ); + } + + if (!\is_null($trait_adaptations)) { + $this->importTraitAdaptations($code_base, $class, $trait_adaptations, $type_option); + } + } + + private function adaptInheritedMethodFromTrait(Method $method): Method + { + $method_flags = $method->getFlags(); + if (Flags::bitVectorHasState($method_flags, \ast\flags\MODIFIER_PRIVATE)) { + $method = $method->createUseAlias($this, $method->getName(), \ast\flags\MODIFIER_PRIVATE); + } elseif (Flags::bitVectorHasState($method_flags, \ast\flags\MODIFIER_PROTECTED)) { + $method = $method->createUseAlias($this, $method->getName(), \ast\flags\MODIFIER_PROTECTED); + } else { + $method = $method->createUseAlias($this, $method->getName(), \ast\flags\MODIFIER_PUBLIC); + } + $context = $this->getContext()->withScope($this->internal_scope); + $method->setUnionType( + $method->getUnionTypeWithUnmodifiedStatic()->withSelfResolvedInContext($context) + ); + $method->setRealReturnType( + $method->getRealReturnType()->withSelfResolvedInContext($context) + ); + $parameter_list = $method->getParameterList(); + $changed = false; + foreach ($parameter_list as $i => $parameter) { + $old_type = $parameter->getNonVariadicUnionType(); + $type = $old_type->withSelfResolvedInContext($context); + if ($type->hasStaticType()) { + $type = $type->withType($this->getFQSEN()->asType()); + } + if ($old_type !== $type) { + $changed = true; + $parameter = clone($parameter); + $parameter->setUnionType($type); + $parameter_list[$i] = $parameter; + } + } + if ($changed) { + $method->setParameterList($parameter_list); + } + + $real_parameter_list = $method->getRealParameterList(); + $changed = false; + foreach ($real_parameter_list as $i => $parameter) { + $old_type = $parameter->getNonVariadicUnionType(); + $type = $old_type->withSelfResolvedInContext($context); + if ($type->hasStaticType()) { + $type = $type->withType($this->getFQSEN()->asType()); + } + if ($old_type !== $type) { + $changed = true; + $parameter = clone($parameter); + $parameter->setUnionType($type); + $real_parameter_list[$i] = $parameter; + } + } + if ($changed) { + $method->setRealParameterList($parameter_list); + } + + return $method; + } + + /** + * @param CodeBase $code_base + * @param Clazz $class + * @param TraitAdaptations $trait_adaptations + * @param Option $type_option + * A possibly defined ancestor type used to define template + * parameter types when importing ancestor properties and + * methods + */ + private function importTraitAdaptations( + CodeBase $code_base, + Clazz $class, + TraitAdaptations $trait_adaptations, + Option $type_option + ): void { + foreach ($trait_adaptations->alias_methods ?? [] as $alias_method_name => $original_trait_alias_source) { + $source_method_name = $original_trait_alias_source->getSourceMethodName(); + if ($class->hasMethodWithName($code_base, $source_method_name, true)) { + $source_method = $class->getMethodByName($code_base, $source_method_name); + } else { + $source_method = null; + } + if (!$source_method || $source_method->isFromPHPDoc()) { + Issue::maybeEmit( + $code_base, + $this->getContext(), + Issue::UndeclaredAliasedMethodOfTrait, + $original_trait_alias_source->getAliasLineno(), // TODO: Track line number in TraitAdaptation + \sprintf('%s::%s', (string)$this->getFQSEN(), $alias_method_name), + \sprintf('%s::%s', (string)$class->getFQSEN(), $source_method_name), + $class->getName() + ); + continue; + } + $alias_method = $source_method->createUseAlias( + $this, + $alias_method_name, + $original_trait_alias_source->getAliasVisibilityFlags() + ); + $this->addMethod($code_base, $alias_method, $type_option); + } + } + + private function warnAboutAmbiguousInheritance( + CodeBase $code_base, + Clazz $inherited_class, + FullyQualifiedClassName $alternate_class_fqsen + ): void { + $alternate_class = $code_base->getClassByFQSEN($alternate_class_fqsen); + if ($inherited_class->isTrait()) { + $issue_type = Issue::RedefinedUsedTrait; + } elseif ($inherited_class->isInterface()) { + $issue_type = Issue::RedefinedInheritedInterface; + } else { + $issue_type = Issue::RedefinedExtendedClass; + } + if ($this->checkHasSuppressIssueAndIncrementCount($issue_type)) { + return; + } + $first_context = $inherited_class->getContext(); + $second_context = $alternate_class->getContext(); + + Issue::maybeEmit( + $code_base, + $this->getContext(), + $issue_type, + $this->getContext()->getLineNumberStart(), + $this->getFQSEN(), + $inherited_class->__toString(), + $first_context->getFile(), + $first_context->getLineNumberStart(), + $second_context->getFile(), + $second_context->getLineNumberStart() + ); + } + + /** + * @return int + * The number of references to this typed structural element + */ + public function getReferenceCount( + CodeBase $code_base + ): int { + $count = parent::getReferenceCount($code_base); + + /** + * A function that maps a list of elements to the + * total reference count for all elements + * @param array $list + */ + $list_count = function (array $list): int { + return \array_reduce($list, function ( + int $count, + ClassElement $element + ): int { + foreach ($element->reference_list as $reference) { + if ($reference instanceof Context && $reference->getClassFQSENOrNull() === $this->fqsen) { + continue; + } + $count++; + } + return $count; + }, 0); + }; + + // Sum up counts for all dependent elements + $count += $list_count($this->getPropertyMap($code_base)); + $count += $list_count($this->getMethodMap($code_base)); + $count += $list_count($this->getConstantMap($code_base)); + + return $count; + } + + /** + * @return bool + * True if this class contains generic types + */ + public function isGeneric(): bool + { + return $this->getInternalScope()->hasAnyTemplateType(); + } + + /** + * @return array + * The set of all template types parameterizing this generic + * class + */ + public function getTemplateTypeMap(): array + { + return $this->getInternalScope()->getTemplateTypeMap(); + } + + /** + * @return string + * A string describing this class + */ + public function __toString(): string + { + $string = ''; + + if ($this->isFinal()) { + $string .= 'final '; + } + + if ($this->isAbstract()) { + $string .= 'abstract '; + } + + if ($this->isInterface()) { + $string .= 'Interface '; + } elseif ($this->isTrait()) { + $string .= 'Trait '; + } else { + $string .= 'Class '; + } + + $string .= (string)$this->getFQSEN()->getCanonicalFQSEN(); + + return $string; + } + + private function toStubSignature(CodeBase $code_base): string + { + $string = ''; + + if ($this->isFinal()) { + $string .= 'final '; + } + + if ($this->isAbstract() && !$this->isInterface()) { + $string .= 'abstract '; + } + + if ($this->isInterface()) { + $string .= 'interface '; + } elseif ($this->isTrait()) { + $string .= 'trait '; + } else { + $string .= 'class '; + } + + $string .= $this->getFQSEN()->getName(); + + $extend_types = []; + $implements_types = []; + $parent_implements_types = []; + + if ($this->parent_type) { + $extend_types[] = FullyQualifiedClassName::fromType($this->parent_type); + $parent_class = $this->getParentClass($code_base); + $parent_implements_types = $parent_class->interface_fqsen_list; + } + + if (count($this->interface_fqsen_list) > 0) { + if ($this->isInterface()) { + $extend_types = \array_merge($extend_types, $this->interface_fqsen_list); + } else { + $implements_types = $this->interface_fqsen_list; + if (count($parent_implements_types) > 0) { + $implements_types = \array_diff($implements_types, $parent_implements_types); + } + } + } + if (count($extend_types) > 0) { + $string .= ' extends ' . \implode(', ', $extend_types); + } + if (count($implements_types) > 0) { + $string .= ' implements ' . \implode(', ', $implements_types); + } + return $string; + } + + public function getMarkupDescription(): string + { + $fqsen = $this->getFQSEN(); + $string = ''; + $namespace = \ltrim($fqsen->getNamespace(), '\\'); + if ($namespace !== '') { + // Render the namespace one line above the class + $string .= "namespace $namespace;\n"; + } + + if ($this->isFinal()) { + $string .= 'final '; + } + + if ($this->isAbstract() && !$this->isInterface()) { + $string .= 'abstract '; + } + + if ($this->isInterface()) { + $string .= 'interface '; + } elseif ($this->isTrait()) { + $string .= 'trait '; + } else { + $string .= 'class '; + } + + if ($this->isAnonymous()) { + $string .= 'anonymous_class'; + } else { + $string .= $fqsen->getName(); + } + return $string; + } + + + /** + * @suppress PhanUnreferencedPublicMethod (toStubInfo is used by callers for more flexibility) + */ + public function toStub(CodeBase $code_base): string + { + [$namespace, $string] = $this->toStubInfo($code_base); + $namespace_text = $namespace === '' ? '' : "$namespace "; + $string = \sprintf("namespace %s{\n%s}\n", $namespace_text, $string); + return $string; + } + + /** @return array{0:string,1:string} [string $namespace, string $text] */ + public function toStubInfo(CodeBase $code_base): array + { + $signature = $this->toStubSignature($code_base); + + $stub = $signature; + + $stub .= " {"; + + $constant_map = $this->getConstantMap($code_base); + if (count($constant_map) > 0) { + $stub .= "\n\n // constants\n"; + $stub .= \implode("\n", \array_map(static function (ClassConstant $constant): string { + return $constant->toStub(); + }, $constant_map)); + } + + $property_map = $this->getPropertyMap($code_base); + if (count($property_map) > 0) { + $stub .= "\n\n // properties\n"; + + $stub .= \implode("\n", \array_map(static function (Property $property): string { + return $property->toStub(); + }, $property_map)); + } + $reflection_class = new \ReflectionClass((string)$this->getFQSEN()); + $method_map = \array_filter($this->getMethodMap($code_base), static function (Method $method) use ($reflection_class): bool { + if ($method->getFQSEN()->isAlternate()) { + return false; + } + $reflection_method = $reflection_class->getMethod($method->getName()); + if ($reflection_method->class !== $reflection_class->name) { + return false; + } + return true; + }); + if (count($method_map) > 0) { + $stub .= "\n\n // methods\n"; + + $is_interface = $this->isInterface(); + $stub .= \implode("\n", \array_map(static function (Method $method) use ($is_interface): string { + return $method->toStub($is_interface); + }, $method_map)); + } + + $stub .= "\n}\n\n"; + $namespace = \ltrim($this->getFQSEN()->getNamespace(), '\\'); + return [$namespace, $stub]; + } + + protected function hydrateConstantsOnce(CodeBase $code_base): void + { + foreach ($this->getAncestorFQSENList() as $fqsen) { + if ($code_base->hasClassWithFQSEN($fqsen)) { + $code_base->getClassByFQSENWithoutHydrating( + $fqsen + )->hydrateConstants($code_base); + } + } + + // Create the 'class' constant + $class_constant_value = \ltrim($this->getFQSEN()->__toString(), '\\'); + $class_constant = new ClassConstant( + $this->getContext(), + 'class', + LiteralStringType::instanceForValue( + $class_constant_value, + false + )->asRealUnionType(), + 0, + FullyQualifiedClassConstantName::make( + $this->getFQSEN(), + 'class' + ) + ); + $class_constant->setNodeForValue($class_constant_value); + $this->addConstant($code_base, $class_constant); + + // Add variable '$this' to the scope + $this->getInternalScope()->addVariable( + new Variable( + $this->getContext(), + 'this', + StaticType::instance(false)->asRealUnionType(), + 0 + ) + ); + + // Fetch the constants declared within the class, to check if they have override annotations later. + $original_declared_class_constants = $this->getConstantMap($code_base); + + // Load parent methods, properties, constants + $this->importConstantsFromAncestorClasses($code_base); + + self::analyzeClassConstantOverrides($code_base, $original_declared_class_constants); + } + + /** + * This method must be called before analysis + * begins. + */ + protected function hydrateOnce(CodeBase $code_base): void + { + // Ensure that we hydrate constants before hydrating properties and methods + $this->hydrateConstants($code_base); + + foreach ($this->getAncestorFQSENList() as $fqsen) { + if ($code_base->hasClassWithFQSEN($fqsen)) { + $code_base->getClassByFQSENWithoutHydrating( + $fqsen + )->hydrate($code_base); + } + } + + // Fetch the properties declared within the class, to check if they have override annotations later. + $original_declared_properties = $this->getPropertyMap($code_base); + + $this->importAncestorClasses($code_base); + + self::analyzePropertyOverrides($code_base, $original_declared_properties); + + // Make sure there are no abstract methods on non-abstract classes + AbstractMethodAnalyzer::analyzeAbstractMethodsAreImplemented( + $code_base, + $this + ); + } + + /** + * @param ClassConstant[] $original_declared_class_constants + */ + private static function analyzeClassConstantOverrides(CodeBase $code_base, array $original_declared_class_constants): void + { + foreach ($original_declared_class_constants as $constant) { + if ($constant->isOverrideIntended() && !$constant->isOverride()) { + if ($constant->checkHasSuppressIssueAndIncrementCount(Issue::CommentOverrideOnNonOverrideConstant)) { + continue; + } + $context = $constant->getContext(); + Issue::maybeEmit( + $code_base, + $context, + Issue::CommentOverrideOnNonOverrideConstant, + $context->getLineNumberStart(), + (string)$constant->getFQSEN() + ); + } + } + } + + /** + * @param array $original_declared_properties + */ + private static function analyzePropertyOverrides(CodeBase $code_base, array $original_declared_properties): void + { + foreach ($original_declared_properties as $property) { + if ($property->isOverrideIntended() && !$property->isOverride()) { + if ($property->checkHasSuppressIssueAndIncrementCount(Issue::CommentOverrideOnNonOverrideProperty)) { + continue; + } + $context = $property->getContext(); + Issue::maybeEmit( + $code_base, + $context, + Issue::CommentOverrideOnNonOverrideProperty, + $context->getLineNumberStart(), + (string)$property->getFQSEN() + ); + } + } + } + + /** + * This method should be called after hydration + * @throws RecursionDepthException for deep class hierarchies + */ + final public function analyze(CodeBase $code_base): void + { + if ($this->isPHPInternal()) { + return; + } + + // Make sure the parent classes exist + ClassInheritanceAnalyzer::analyzeClassInheritance( + $code_base, + $this + ); + + DuplicateClassAnalyzer::analyzeDuplicateClass( + $code_base, + $this + ); + + ParentConstructorCalledAnalyzer::analyzeParentConstructorCalled( + $code_base, + $this + ); + + PropertyTypesAnalyzer::analyzePropertyTypes( + $code_base, + $this + ); + + ClassConstantTypesAnalyzer::analyzeClassConstantTypes( + $code_base, + $this + ); + + // Analyze this class to make sure that we don't have conflicting + // types between similar inherited methods. + CompositionAnalyzer::analyzeComposition( + $code_base, + $this + ); + + $this->analyzeInheritedMethods($code_base); + + // Let any configured plugins analyze the class + ConfigPluginSet::instance()->analyzeClass( + $code_base, + $this + ); + } + + private function analyzeInheritedMethods(CodeBase $code_base): void + { + if ($this->isClass() && !$this->isAbstract()) { + foreach ($this->getMethodMap($code_base) as $method) { + if ($method->getRealDefiningFQSEN() === $method->getFQSEN()) { + continue; + } + if ($method->isPHPDocAbstract() && !$method->isPrivate()) { + Issue::maybeEmit( + $code_base, + $this->getContext(), + Issue::CommentAbstractOnInheritedMethod, + $this->getContext()->getLineNumberStart(), + $this->getFQSEN(), + $method->getRealDefiningFQSEN(), + $method->getContext()->getFile(), + $method->getContext()->getLineNumberStart(), + '@abstract' + ); + } + } + } + } + + public function setDidFinishParsing(bool $did_finish_parsing): void + { + $this->did_finish_parsing = $did_finish_parsing; + } + + /** + * @var bool have the class constants been hydrated + * (must be done before hydrating properties and methods to avoid recursive dependencies) + */ + protected $are_constants_hydrated; + + /** + * This method must be called before analysis + * begins. It hydrates constants, but not properties/methods. + */ + protected function hydrateConstants(CodeBase $code_base): void + { + if (!$this->did_finish_parsing) { + return; + } + if ($this->are_constants_hydrated) { // Same as isFirstExecution(), inlined due to being called frequently. + return; + } + if (!$code_base->shouldHydrateRequestedElements()) { + return; + } + $this->are_constants_hydrated = true; + + $this->hydrateConstantsOnce($code_base); + } + + /** + * This method must be called before analysis begins. + * This is identical to hydrateConstants(), + * but returns true only if this is the first time the element was hydrated. + * (i.e. true if there may be newly added constants) + */ + public function hydrateConstantsIndicatingFirstTime(CodeBase $code_base): bool + { + if (!$this->did_finish_parsing) { // Is **this** class fully parsed + return false; + } + if ($this->are_constants_hydrated) { // Same as isFirstExecution(), inlined due to being called frequently. + return false; + } + if (!$code_base->shouldHydrateRequestedElements()) { + return false; + } + $this->are_constants_hydrated = true; + + $this->hydrateConstantsOnce($code_base); + return true; + } + + /** + * This method must be called before analysis + * begins. + * @override + */ + public function hydrate(CodeBase $code_base): void + { + if (!$this->did_finish_parsing) { + return; + } + if ($this->is_hydrated) { // Same as isFirstExecution(), inlined due to being called frequently. + return; + } + if (!$code_base->shouldHydrateRequestedElements()) { + return; + } + $this->is_hydrated = true; + + $this->hydrateOnce($code_base); + } + + /** + * This method must be called before analysis begins. + * This is identical to hydrate(), but returns true only if this is the first time the element was hydrated. + * @internal + */ + public function hydrateIndicatingFirstTime(CodeBase $code_base): bool + { + if (!$this->did_finish_parsing) { + return false; + } + if ($this->is_hydrated) { // Same as isFirstExecution(), inlined due to being called frequently. + return false; + } + if (!$code_base->shouldHydrateRequestedElements()) { + return false; + } + $this->is_hydrated = true; + + $this->hydrateOnce($code_base); + return true; + } + + /** + * Used by daemon mode to restore an element to the state it had before parsing. + */ + public function createRestoreCallback(): Closure + { + // NOTE: Properties, Methods, and closures are restored separately. + $original_this = clone($this); + $original_union_type = $this->getUnionType(); + + return function () use ($original_union_type, $original_this): void { + // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach this is intentionally iterating over private properties of the clone. + foreach ($original_this as $key => $value) { + $this->{$key} = $value; + } + $this->setUnionType($original_union_type); + $this->memoizeFlushAll(); + }; + } + + public function addAdditionalType(Type $type): void + { + $this->additional_union_types = ($this->additional_union_types ?? UnionType::empty())->withType($type); + } + + public function getAdditionalTypes(): ?UnionType + { + return $this->additional_union_types; + } + + /** + * @param array $template_parameter_type_map + */ + public function resolveParentTemplateType(array $template_parameter_type_map): UnionType + { + if (\count($template_parameter_type_map) === 0) { + return UnionType::empty(); + } + if ($this->parent_type === null) { + return UnionType::empty(); + } + if (!$this->parent_type->hasTemplateParameterTypes()) { + return UnionType::empty(); + } + $parent_template_parameter_type_list = $this->parent_type->getTemplateParameterTypeList(); + $changed = false; + foreach ($parent_template_parameter_type_list as $i => $template_type) { + $new_template_type = $template_type->withTemplateParameterTypeMap($template_parameter_type_map); + if ($template_type === $new_template_type) { + continue; + } + $parent_template_parameter_type_list[$i] = $new_template_type; + $changed = true; + } + if (!$changed) { + return UnionType::empty(); + } + return Type::fromType($this->parent_type, $parent_template_parameter_type_list)->asPHPDocUnionType(); + } + + /** + * @return array + */ + public function getPropertyMapExcludingDynamicAndMagicProperties(CodeBase $code_base): array + { + return $this->memoize(__METHOD__, /** @return array */ function () use ($code_base): array { + // TODO: This won't work if a class declares both a real property and a magic property of the same name. + // Low priority because that is uncommon + return \array_filter( + $this->getPropertyMap($code_base), + static function (Property $property): bool { + return !$property->isDynamicOrFromPHPDoc(); + } + ); + }); + } + + public const CAN_ITERATE_STATUS_NO_PROPERTIES = 0; + public const CAN_ITERATE_STATUS_NO_ACCESSIBLE_PROPERTIES = 1; + public const CAN_ITERATE_STATUS_HAS_ACCESSIBLE_PROPERTIES = 2; + + /** + * Returns an enum value (self::CAN_ITERATE_STATUS_*) indicating whether + * analyzed code iterating over an instance of this class has potential bugs. + * (and what type of bug it would be) + */ + public function checkCanIterateFromContext( + CodeBase $code_base, + Context $context + ): int { + $accessing_class = $context->getClassFQSENOrNull(); + return $this->memoize( + 'can_iterate:' . (string)$accessing_class, + function () use ($accessing_class, $code_base): int { + $properties = $this->getPropertyMapExcludingDynamicAndMagicProperties($code_base); + foreach ($properties as $property) { + if ($property->isAccessibleFromClass($code_base, $accessing_class)) { + return self::CAN_ITERATE_STATUS_HAS_ACCESSIBLE_PROPERTIES; + } + } + if (count($properties) > 0) { + return self::CAN_ITERATE_STATUS_NO_ACCESSIBLE_PROPERTIES; + } + return self::CAN_ITERATE_STATUS_NO_PROPERTIES; + } + ); + } + + /** + * @return list, Context):UnionType> + */ + public function getGenericConstructorBuilder(CodeBase $code_base): array + { + return $this->memoize( + 'template_type_resolvers', + /** + * @return list):UnionType> + */ + function () use ($code_base): array { + // Get the constructor so that we can figure out what + // template types we're going to be mapping + $constructor_method = + $this->getMethodByName($code_base, '__construct'); + + $template_type_resolvers = []; + foreach ($this->getTemplateTypeMap() as $template_type) { + $template_type_resolver = $constructor_method->getTemplateTypeExtractorClosure( + $code_base, + $template_type + ); + if (!$template_type_resolver) { + // PhanTemplateTypeNotDeclaredInFunctionParams can be suppressed both on the class and on __construct() + if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::TemplateTypeNotDeclaredInFunctionParams)) { + Issue::maybeEmit( + $code_base, + $constructor_method->getContext(), + Issue::GenericConstructorTypes, + $constructor_method->getContext()->getLineNumberStart(), + $template_type, + $this->getFQSEN() + ); + } + /** @param list<\ast\Node|mixed> $unused_arg_list */ + $template_type_resolver = static function (array $unused_arg_list): UnionType { + return MixedType::instance(false)->asPHPDocUnionType(); + }; + } + $template_type_resolvers[] = $template_type_resolver; + } + return $template_type_resolvers; + } + ); + } + + /** + * Given the FQSEN of an ancestor class and an element definition, + * return the overridden element's definition or null if this didn't override anything. + * + * TODO: Handle renamed elements from traits. + * + * @return ?ClassElement if non-null, this is of the same type as $element + */ + public static function getAncestorElement(CodeBase $code_base, FullyQualifiedClassName $ancestor_fqsen, ClassElement $element): ?ClassElement + { + if (!$code_base->hasClassWithFQSEN($ancestor_fqsen)) { + return null; + } + $ancestor_class = $code_base->getClassByFQSEN($ancestor_fqsen); + $name = $element->getName(); + if ($element instanceof Method) { + if (!$ancestor_class->hasMethodWithName($code_base, $name, true)) { + return null; + } + return $ancestor_class->getMethodByName($code_base, $name); + } elseif ($element instanceof ClassConstant) { + if (!$ancestor_class->hasConstantWithName($code_base, $name)) { + return null; + } + $constant_fqsen = FullyQualifiedClassConstantName::make( + $ancestor_fqsen, + $name + ); + return $code_base->getClassConstantByFQSEN($constant_fqsen); + } elseif ($element instanceof Property) { + if (!$ancestor_class->hasPropertyWithName($code_base, $name)) { + return null; + } + return $ancestor_class->getPropertyByName($code_base, $name); + } + return null; + } + + private static function markMethodAsOverridden(CodeBase $code_base, FullyQualifiedMethodName $method_fqsen): void + { + if (!$code_base->hasMethodWithFQSEN($method_fqsen)) { + return; + } + $method = $code_base->getMethodByFQSEN($method_fqsen); + $method->setIsOverriddenByAnother(true); + } + + /** + * Sets the declaration id of the node containing this user-defined class + */ + public function setDeclId(int $id): void + { + $this->decl_id = $id; + } + + /** + * Gets the declaration id of the node containing this user-defined class. + * Returns 0 for internal classes. + */ + public function getDeclId(): int + { + return $this->decl_id; + } + + /** + * Returns a context with the internal scope of this class (including suppression info) + * Equivalent to $clazz->getContext()->withScope($clazz->getInternalScope()) + * + * TODO: Use this for more issues about class and class-like declarations. + */ + public function getInternalContext(): Context + { + return $this->internal_context; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/ClosedScopeElement.php b/bundled-libs/phan/phan/src/Phan/Language/Element/ClosedScopeElement.php new file mode 100644 index 000000000..84995b427 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/ClosedScopeElement.php @@ -0,0 +1,37 @@ +internal_scope = $internal_scope; + } + + /** + * @return ClosedScope + * The internal scope of this closed scope element + */ + public function getInternalScope(): ClosedScope + { + return $this->internal_scope; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Comment.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment.php new file mode 100644 index 000000000..31ac53f50 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment.php @@ -0,0 +1,770 @@ + 'class', + self::ON_VAR => 'variable', + self::ON_PROPERTY => 'property', + self::ON_CONST => 'constant', + self::ON_METHOD => 'method', + self::ON_FUNCTION => 'function', + ]; + + /** + * @var int - contains a subset of flags to set on elements + * Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES + * Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS + * Flags::IS_READ_ONLY + * Flags::IS_WRITE_ONLY + * Flags::IS_DEPRECATED + */ + protected $comment_flags = 0; + + /** + * @var list + * A list of CommentParameters from var declarations + */ + protected $variable_list = []; + + /** + * @var list + * A list of CommentParameters from param declarations + */ + protected $parameter_list = []; + + /** + * @var array + * A map from variable name to CommentParameters from + * param declarations + */ + protected $parameter_map = []; + + /** + * @var list + * A list of template types parameterizing a generic class + */ + protected $template_type_list = []; + + /** + * @var Option|None + * Classes may specify their inherited type explicitly + * via `(at)inherits Type`. + */ + protected $inherited_type; + + /** + * @var ReturnComment|null + * the representation of an (at)return directive + */ + protected $return_comment = null; + + /** + * @var array + * A set of issue types to be suppressed + */ + protected $suppress_issue_set = []; + + /** + * @var array + * A mapping from magic property parameters to types. + */ + protected $magic_property_map = []; + + /** + * @var array + * A mapping from magic methods to parsed parameters, name, and return types. + */ + protected $magic_method_map = []; + + /** + * @var UnionType a list of types for (at)throws annotations + */ + protected $throw_union_type; + + /** + * @var Option|None + * An optional class name defined by an (at)phan-closure-scope directive. + * (overrides the class in which it is analyzed) + */ + protected $closure_scope; + + /** + * @var array + * An optional assertion on a parameter's type + */ + protected $param_assertion_map = []; + + /** + * @var list + * A list of mixins used by this class + */ + protected $mixin_types = []; + + /** + * A private constructor meant to ingest a parsed comment + * docblock. + * + * @param int $comment_flags uses the following flags + * - Flags::IS_DEPRECATED + * Set to true if the comment contains a 'deprecated' + * directive. + * - Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES + * - Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS + * + * @param list $variable_list + * + * @param list $parameter_list + * + * @param list $template_type_list + * A list of template types parameterizing a generic class + * + * @param Option|None $inherited_type (Note: some issues with templates and narrowing signature types to phpdoc type, added None as a workaround) + * An override on the type of the extended class + * + * @param ?ReturnComment $return_comment + * + * @param array $suppress_issue_set + * A set of tags for error type to be suppressed + * + * @param list $magic_property_list + * + * @param list $magic_method_list + * + * @param array $phan_overrides + * + * @param Option|None $closure_scope + * For closures: Allows us to document the class of the object + * to which a closure will be bound. + * + * @param UnionType $throw_union_type + * + * @param array $param_assertion_map + * + * @internal + */ + public function __construct( + int $comment_flags, + array $variable_list, + array $parameter_list, + array $template_type_list, + Option $inherited_type, + $return_comment, + array $suppress_issue_set, + array $magic_property_list, + array $magic_method_list, + array $phan_overrides, + Option $closure_scope, + UnionType $throw_union_type, + array $param_assertion_map, + CodeBase $code_base, + Context $context + ) { + $this->comment_flags = $comment_flags; + $this->variable_list = $variable_list; + $this->parameter_list = $parameter_list; + $this->template_type_list = $template_type_list; + $this->inherited_type = $inherited_type; + $this->return_comment = $return_comment; + $this->suppress_issue_set = $suppress_issue_set; + $this->closure_scope = $closure_scope; + $this->throw_union_type = $throw_union_type; + $this->param_assertion_map = $param_assertion_map; + + foreach ($this->parameter_list as $i => $parameter) { + $name = $parameter->getName(); + if (StringUtil::isNonZeroLengthString($name)) { + if (isset($this->parameter_map[$name])) { + Issue::maybeEmit( + $code_base, + $context, + Issue::CommentDuplicateParam, + $parameter->getLineno(), + $name + ); + // @phan-suppress-next-line PhanAccessMethodInternal, PhanPluginUnknownObjectMethodCall + $parameter->addUnionType($this->parameter_map[$name]->getUnionType()); + } + // Add it to the named map + $this->parameter_map[$name] = $parameter; + + // Remove it from the offset map + unset($this->parameter_list[$i]); + } + } + foreach ($magic_property_list as $property) { + $name = $property->getName(); + if (StringUtil::isNonZeroLengthString($name)) { + if (isset($this->magic_property_map[$name])) { + // Emit warning for duplicates. + Issue::maybeEmit( + $code_base, + $context, + Issue::CommentDuplicateMagicProperty, + $property->getLine(), + $name + ); + } + // Add it to the named map + $this->magic_property_map[$name] = $property; + } + } + foreach ($magic_method_list as $method) { + $name = $method->getName(); + if (StringUtil::isNonZeroLengthString($name)) { + if (isset($this->magic_method_map[$name])) { + // Emit warning for duplicates. + Issue::maybeEmit( + $code_base, + $context, + Issue::CommentDuplicateMagicMethod, + $method->getLine(), + $name + ); + } + // Add it to the named map + $this->magic_method_map[$name] = $method; + } + } + // @phan-suppress-next-line PhanSideEffectFreeForeachBody applyOverride is annotated as @phan-pure due to the catch-all annotation, so phan treats this like it has no side effects. + foreach ($phan_overrides as $key => $override_value) { + $this->applyOverride($key, $override_value); + } + if (isset($phan_overrides['real-return'])) { + $this->applyRealReturnOverride($phan_overrides['real-return']); + } + } + + private function applyRealReturnOverride(ReturnComment $real_return_comment): void + { + $old_comment = $this->return_comment; + if (!$old_comment) { + $this->return_comment = $real_return_comment; + return; + } + $return_type = $old_comment->getType()->withRealTypeSet($real_return_comment->getType()->getRealTypeSet()); + $this->return_comment = new ReturnComment($return_type, $old_comment->getLineno()); + } + + /** + * @param mixed $value + */ + private function applyOverride(string $key, $value): void + { + switch ($key) { + case 'param': + foreach ($value as $parameter) { + '@phan-var CommentParameter $parameter'; + $name = $parameter->getName(); + if ($name !== '') { + // Add it to the named map + // TODO: could check that @phan-param is compatible with the original @param + $this->parameter_map[$name] = $parameter; + } + } + return; + case 'real-return': + return; + case 'return': + // TODO: could check that @phan-return is compatible with the original @return + $this->return_comment = $value; + return; + case 'var': + // TODO: Remove pre-existing entries. + $this->mergeVariableList($value); + return; + case 'property': + foreach ($value as $property) { + '@phan-var CommentProperty $property'; + $name = $property->getName(); + if ($name !== '') { + // Override or add the entry in the named map + $this->magic_property_map[$name] = $property; + } + } + return; + case 'method': + foreach ($value as $method) { + '@phan-var CommentMethod $method'; + $name = $method->getName(); + if ($name !== '') { + // Override or add the entry in the named map (probably always has a name) + $this->magic_method_map[$name] = $method; + } + } + return; + case 'template': + $this->template_type_list = $value; + return; + case 'inherits': + case 'extends': + $this->inherited_type = $value; + return; + case 'mixin': + $this->mixin_types = $value; + return; + } + } + + /** + * @param list $override_comment_vars + * A list of CommentParameters from var declarations + */ + private function mergeVariableList(array $override_comment_vars): void + { + $known_names = []; + foreach ($override_comment_vars as $override_var) { + $known_names[$override_var->getName()] = true; + } + foreach ($this->variable_list as $i => $var) { + if (isset($known_names[$var->getName()])) { + unset($this->variable_list[$i]); + } + } + $this->variable_list = \array_merge($this->variable_list, $override_comment_vars); + } + + + /** + * @param string $comment full text of doc comment + * @param CodeBase $code_base + * @param Context $context + * @param int $comment_type self::ON_* (the type of comment this is) + * @return Comment + * A comment built by parsing the given doc block + * string. + * + * suppress PhanTypeMismatchArgument - Still need to work out issues with prefer_narrowed_phpdoc_param_type + */ + public static function fromStringInContext( + string $comment, + CodeBase $code_base, + Context $context, + int $lineno, + int $comment_type + ): Comment { + + // Don't parse the comment if this doesn't need to. + if ($comment === '' || !Config::getValue('read_type_annotations') || \strpos($comment, '@') === false) { + return NullComment::instance(); + } + + // @phan-suppress-next-line PhanAccessMethodInternal + return (new Builder( + $comment, + $code_base, + $context, + $lineno, + $comment_type + ))->build(); + } + + // TODO: Is `@return &array` valid phpdoc2? + + /** + * @return bool + * Set to true if the comment contains a 'deprecated' + * directive. + */ + public function isDeprecated(): bool + { + return ($this->comment_flags & Flags::IS_DEPRECATED) !== 0; + } + + /** + * @return bool + * Set to true if the comment contains an 'override' + * directive. + */ + public function isOverrideIntended(): bool + { + return ($this->comment_flags & Flags::IS_OVERRIDE_INTENDED) !== 0; + } + + /** + * @return bool + * Set to true if the comment contains an 'abstract' directive. + */ + public function isPHPDocAbstract(): bool + { + return ($this->comment_flags & Flags::IS_PHPDOC_ABSTRACT) !== 0; + } + + /** + * @return bool + * Set to true if the comment contains an 'internal' + * directive. + */ + public function isNSInternal(): bool + { + return ($this->comment_flags & Flags::IS_NS_INTERNAL) !== 0; + } + + /** + * @return bool + * Set to true if the comment contains an 'phan-pure' + * directive. + * (or phan-read-only + phan-external-mutation-free, eventually) + */ + public function isPure(): bool + { + return ($this->comment_flags & Flags::IS_SIDE_EFFECT_FREE) === Flags::IS_SIDE_EFFECT_FREE; + } + + private const FLAGS_FOR_PROPERTY = + Flags::IS_NS_INTERNAL | + Flags::IS_DEPRECATED | + Flags::IS_READ_ONLY | + Flags::IS_WRITE_ONLY | + Flags::IS_PHPDOC_ABSTRACT | + Flags::IS_OVERRIDE_INTENDED; + + /** + * Gets the subset of the bitmask that applies to properties. + */ + public function getPhanFlagsForProperty(): int + { + return $this->comment_flags & self::FLAGS_FOR_PROPERTY; + } + + private const FLAGS_FOR_CLASS = + Flags::IS_NS_INTERNAL | + Flags::IS_DEPRECATED | + Flags::IS_SIDE_EFFECT_FREE | + Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS | + Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES | + Flags::IS_CONSTRUCTOR_USED_FOR_SIDE_EFFECTS; + + /** + * Gets the subset of the bitmask that applies to classes. + */ + public function getPhanFlagsForClass(): int + { + return $this->comment_flags & self::FLAGS_FOR_CLASS; + } + + private const FLAGS_FOR_METHOD = + Flags::IS_NS_INTERNAL | + Flags::IS_DEPRECATED | + Flags::HARDCODED_RETURN_TYPE | + Flags::IS_SIDE_EFFECT_FREE | + Flags::IS_PHPDOC_ABSTRACT | + Flags::IS_OVERRIDE_INTENDED; + + /** + * Gets the subset of the bitmask that applies to methods. + */ + public function getPhanFlagsForMethod(): int + { + return $this->comment_flags & self::FLAGS_FOR_METHOD; + } + + /** + * @return bool + * Set to true if the comment contains a 'phan-forbid-undeclared-magic-properties' + * directive. + * @suppress PhanUnreferencedPublicMethod + */ + public function getForbidUndeclaredMagicProperties(): bool + { + return ($this->comment_flags & Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES) !== 0; + } + + /** + * @return bool + * Set to true if the comment contains a 'phan-forbid-undeclared-magic-methods' + * directive. + * @suppress PhanUnreferencedPublicMethod + */ + public function getForbidUndeclaredMagicMethods(): bool + { + return ($this->comment_flags & Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS) !== 0; + } + + /** + * @return UnionType + * A UnionType defined by a (at)return directive + */ + public function getReturnType(): UnionType + { + if (!$this->return_comment) { + throw new AssertionError('Should check hasReturnUnionType'); + } + return $this->return_comment->getType(); + } + + /** + * @return int + * A line of a (at)return directive + */ + public function getReturnLineno(): int + { + if (!$this->return_comment) { + throw new AssertionError('Should check hasReturnUnionType'); + } + return $this->return_comment->getLineno(); + } + + /** + * @return bool + * True if this doc block contains a (at)return + * directive specifying a type. + */ + public function hasReturnUnionType(): bool + { + return $this->return_comment !== null; + } + + /** + * @return Option + * An optional Type defined by a (at)phan-closure-scope + * directive specifying a single type. + * + * @suppress PhanPartialTypeMismatchReturn (Null) + */ + public function getClosureScopeOption(): Option + { + return $this->closure_scope; + } + + /** + * @return list (The leftover parameters without a name) + * + * @suppress PhanUnreferencedPublicMethod + */ + public function getParameterList(): array + { + return $this->parameter_list; + } + + /** + * @return array (maps the names of parameters to their values. Does not include parameters which didn't provide names) + */ + public function getParameterMap(): array + { + return $this->parameter_map; + } + + /** + * @return list + * A list of template types parameterizing a generic class + */ + public function getTemplateTypeList(): array + { + return $this->template_type_list; + } + + /** + * @return Option + * An optional type declaring what a class extends. + * @suppress PhanPartialTypeMismatchReturn (Null) + */ + public function getInheritedTypeOption(): Option + { + return $this->inherited_type; + } + + /** + * @return list + * An optional type declaring the mixins used by a class. + */ + public function getMixinTypes(): array + { + return $this->mixin_types; + } + + /** + * @return array + * A set of issue names like 'PhanUnreferencedPublicMethod' to suppress. + * If the values of fields are 0, the suppressions were not used yet. + */ + public function getSuppressIssueSet(): array + { + return $this->suppress_issue_set; + } + + /** + * @return bool + * True if we have a parameter at the given offset + */ + public function hasParameterWithNameOrOffset( + string $name, + int $offset + ): bool { + if (isset($this->parameter_map[$name])) { + return true; + } + + return isset($this->parameter_list[$offset]); + } + + /** + * @return CommentParameter + * The parameter at the given offset + */ + public function getParameterWithNameOrOffset( + string $name, + int $offset + ): CommentParameter { + if (isset($this->parameter_map[$name])) { + return $this->parameter_map[$name]; + } + + return $this->parameter_list[$offset]; + } + + /** + * @unused + * @return bool + * True if we have a magic property with the given name + */ + public function hasMagicPropertyWithName( + string $name + ): bool { + return isset($this->magic_property_map[$name]); + } + + /** + * Returns the magic property with the given name. + * May or may not have a type. + * @unused + * @suppress PhanUnreferencedPublicMethod not used right now, but making it available for plugins + */ + public function getMagicPropertyWithName( + string $name + ): CommentProperty { + return $this->magic_property_map[$name]; + } + + /** + * @return array map from parameter name to parameter + */ + public function getMagicPropertyMap(): array + { + return $this->magic_property_map; + } + + /** + * @return array map from method name to method info + */ + public function getMagicMethodMap(): array + { + return $this->magic_method_map; + } + + /** + * @return UnionType list of types for throws statements + */ + public function getThrowsUnionType(): UnionType + { + return $this->throw_union_type; + } + + /** + * @return list the list of (at)var annotations + */ + public function getVariableList(): array + { + return $this->variable_list; + } + + /** + * @return array maps parameter names to assertions about those parameters + */ + public function getParamAssertionMap(): array + { + return $this->param_assertion_map; + } + + public function __toString(): string + { + // TODO: add new properties of Comment to this method + // (magic methods, magic properties, custom @phan directives, etc.)) + $string = "/**\n"; + + if (($this->comment_flags & Flags::IS_DEPRECATED) !== 0) { + $string .= " * @deprecated\n"; + } + + foreach ($this->variable_list as $variable) { + $string .= " * @var $variable\n"; + } + + foreach (\array_merge($this->parameter_map, $this->parameter_list) as $parameter) { + $string .= " * @param $parameter\n"; + } + + if ($this->return_comment) { + $string .= " * @return {$this->return_comment->getType()}\n"; + } + foreach ($this->throw_union_type->getTypeSet() as $type) { + $string .= " * @throws {$type}\n"; + } + + $string .= " */\n"; + + return $string; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Assertion.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Assertion.php new file mode 100644 index 000000000..45cc9ac22 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Assertion.php @@ -0,0 +1,38 @@ +union_type = $union_type; + $this->param_name = $param_name; + $this->assertion_type = $assertion_type; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Builder.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Builder.php new file mode 100644 index 000000000..fbd1f0098 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Builder.php @@ -0,0 +1,1526 @@ + the list of lines of the doc comment */ + public $lines; + /** @var int count($this->lines) */ + public $comment_lines_count; + /** @var CodeBase The code base within which we're operating. */ + public $code_base; + /** @var Context the context of the parser at the comment we're reading */ + public $context; + /** @var int the line number of the element this doc comment belongs to */ + public $lineno; + /** @var int an enum value from Comment::ON_* */ + public $comment_type; + /** @var list the list of extracted (at)var annotations*/ + public $variable_list = []; + /** @var list the list of extracted (at)param annotations */ + public $parameter_list = []; + /** @var array the list of extracted (at)template annotations */ + public $template_type_list = []; + /** @var Option the (at)inherits annotation */ + public $inherited_type; + // TODO: Warn about multiple (at)returns + /** @var ?ReturnComment the (at)return annotation details */ + public $return_comment; + /** + * @var array the set of issue names from (at)suppress annotations + */ + public $suppress_issue_set = []; + /** @var list the list of (at)property annotations (and property-read, property-write) */ + public $magic_property_list = []; + /** @var list the list of (at)method annotations */ + public $magic_method_list = []; + /** @var Option the type a closure will be bound to */ + public $closure_scope; + /** @var int combination of flags from \Phan\Flags */ + public $comment_flags = 0; + /** @var array annotations for Phan that override the standardized version of those annotations. Used for compatibility with other tools. */ + public $phan_overrides = []; + /** @var UnionType the union type of the set of (at)throws annotations */ + public $throw_union_type; + /** @var array assertions about each parameter */ + public $param_assertion_map = []; + + /** @var bool did we add template types already */ + protected $did_add_template_types; + + /** + * A list of issues detected in the comment being built. + * This is stored instead of immediately emitting the issue because later lines might suppress these issues. + * + * @var list,3:?Suggestion}> + */ + private $issues = []; + + public function __construct( + string $comment, + CodeBase $code_base, + Context $context, + int $lineno, + int $comment_type, + bool $did_add_template_types = false + ) { + $this->comment = $comment; + $this->lines = \explode("\n", $comment); + $this->comment_lines_count = \count($this->lines); + $this->code_base = $code_base; + $this->context = $context; + $this->lineno = $lineno; + $this->comment_type = $comment_type; + $this->did_add_template_types = $did_add_template_types; + + $this->inherited_type = None::instance(); + $this->return_comment = null; + $this->closure_scope = None::instance(); + $this->throw_union_type = UnionType::empty(); + } + + /** @internal */ + public const PARAM_COMMENT_REGEX = + '/@(?:phan-)?(param|var)\b\s*(' . UnionType::union_type_regex . ')?(?:\s*(\.\.\.)?\s*&?(?:\\$' . self::WORD_REGEX . '))?/'; + + /** @internal */ + public const UNUSED_PARAM_COMMENT_REGEX = + '/@(?:phan-)?unused-param\b\s*(' . UnionType::union_type_regex . ')?(?:\s*(\.\.\.)?\s*&?(?:\\$' . self::WORD_REGEX . '))/'; + + /** + * @param string $line + * An individual line of a comment + * + * @param bool $is_var + * True if this is parsing a variable, false if parsing a parameter. + * + * @return Parameter + * A Parameter associated with a line that has a var + * or param reference. + * + * TODO: account for difference between (at)var and (at)param + */ + private function parameterFromCommentLine( + string $line, + bool $is_var, + int $i + ): Parameter { + $matched = \preg_match(self::PARAM_COMMENT_REGEX, $line, $match); + // Parse https://docs.phpdoc.org/references/phpdoc/tags/param.html + // Exceptions: Deliberately allow "&" in "@param int &$x" when documenting references. + // Warn if there is neither a union type nor a variable + if ($matched && (isset($match[2]) || isset($match[17]))) { + if (!isset($match[2])) { + return new Parameter('', UnionType::empty(), $this->guessActualLineLocation($i)); + } + if (!$is_var && !isset($match[17])) { + $this->checkParamWithoutVarName($line, $match[0], $match[2], $i); + } + $original_type = $match[2]; + + $is_variadic = ($match[16] ?? '') === '...'; + + if ($is_var && $is_variadic) { + $variable_name = ''; // "@var int ...$x" is nonsense and invalid phpdoc. + } else { + $variable_name = $match[17] ?? ''; + if ($is_var && $variable_name === '' && $this->comment_type === Comment::ON_PROPERTY) { + $end_offset = (int)\strpos($line, $match[0]) + \strlen($match[0]); + $char_at_end_offset = $line[$end_offset] ?? ' '; + if (\ord($char_at_end_offset) > 32 && !\preg_match('@^\*+/$@D', (string)\substr($line, $end_offset))) { // Not a control character or space + $this->emitIssue( + Issue::UnextractableAnnotationSuffix, + $this->guessActualLineLocation($i), + \trim($line), + $original_type, + $char_at_end_offset + ); + } + } + } + // Fix typos or non-standard phpdoc tags, according to the user's configuration. + // Does nothing by default. + $type = self::rewritePHPDocType($original_type); + + // If the type looks like a variable name, make it an + // empty type so that other stuff can match it. We can't + // just skip it or we'd mess up the parameter order. + if (0 !== \strpos($type, '$')) { + $union_type = + UnionType::fromStringInContext( + $type, + $this->context, + Type::FROM_PHPDOC, + $this->code_base + ); + } else { + $union_type = UnionType::empty(); + } + $is_output_parameter = \strpos($line, '@phan-output-reference') !== false; + $is_ignored_parameter = \strpos($line, '@phan-ignore-reference') !== false; + $is_mandatory_in_phpdoc = \strpos($line, '@phan-mandatory-param') !== false; + + return new Parameter( + $variable_name, + $union_type, + $this->guessActualLineLocation($i), + $is_variadic, + false, // has_default_value + $is_output_parameter, + $is_ignored_parameter, + $is_mandatory_in_phpdoc + ); + } + + // Don't warn about @param $x Description of $x goes here + // TODO: extract doc comment of @param &$x? + // TODO: Use the right for the name of the comment parameter? + // (don't see a benefit, would create a type if it was (at)var on a function-like) + if (!\preg_match('/@(param|var)\s+(\.\.\.)?\s*(\\$\S+)/', $line)) { + $this->emitIssue( + Issue::UnextractableAnnotation, + $this->guessActualLineLocation($i), + \trim($line) + ); + } + + return new Parameter('', UnionType::empty()); + } + + /** @internal */ + public const RETURN_COMMENT_REGEX = '/@(?:phan-)?(?:real-)?(?:return|throws)\s+(&\s*)?(' . UnionType::union_type_regex_or_this . ')/'; + + /** + * @param string $line + * An individual line of a comment + * + * @return UnionType + * The declared return type + */ + private function returnTypeFromCommentLine( + string $line, + int $i + ): UnionType { + $return_union_type_string = ''; + + if (\preg_match(self::RETURN_COMMENT_REGEX, $line, $match)) { + $return_union_type_string = $match[2]; + $raw_match = $match[0]; + $end_offset = (int)\strpos($line, $raw_match) + \strlen($raw_match); + $char_at_end_offset = $line[$end_offset] ?? ' '; + if (\ord($char_at_end_offset) > 32 && !\preg_match('@^\*+/$@D', (string)\substr($line, $end_offset))) { // Not a control character or space + $this->emitIssue( + Issue::UnextractableAnnotationSuffix, + $this->guessActualLineLocation($i), + \trim($line), + $return_union_type_string, + $char_at_end_offset + ); + } + } else { + $this->emitIssue( + Issue::UnextractableAnnotation, + $this->guessActualLineLocation($i), + \trim($line) + ); + } + // Not emitting any issues about failing to extract, e.g. `@return - Description of what this returns` is a valid comment. + $return_union_type_string = self::rewritePHPDocType($return_union_type_string); + + $return_union_type = UnionType::fromStringInContext( + $return_union_type_string, + $this->context, + Type::FROM_PHPDOC, + $this->code_base + ); + + return $return_union_type; + } + + private static function rewritePHPDocType( + string $original_type + ): string { + // TODO: Would need to pass in CodeBase to emit an issue: + $type = Config::getValue('phpdoc_type_mapping')[\strtolower($original_type)] ?? null; + if (\is_string($type)) { + return $type; + } + return $original_type; + } + + + /** + * This should be uncommon: $line is a parameter for which a parameter name could not be parsed + */ + private function checkParamWithoutVarName( + string $line, + string $raw_match, + string $union_type_string, + int $i + ): void { + + $match_offset = \strpos($line, $raw_match); + $end_offset = $match_offset + \strlen($raw_match); + + $char_at_end_offset = $line[$end_offset] ?? ' '; + $issue_line = $this->guessActualLineLocation($i); + if (\ord($char_at_end_offset) > 32) { // Not a control character or space + $this->emitIssue( + Issue::UnextractableAnnotationSuffix, + $issue_line, + \trim($line), + $union_type_string, + $char_at_end_offset + ); + } + + $this->emitIssue( + Issue::UnextractableAnnotationElementName, + $issue_line, + \trim($line), + $union_type_string + ); + } + + /** + * Extracts information from the doc comment instance, + * parses it, and creates a Comment representing the extracted information. + */ + public function build(): Comment + { + foreach ($this->lines as $i => $line) { + if (\strpos($line, '@') === false) { + continue; + } + $this->parseCommentLine($i, \trim($line)); + } + + if (\count($this->template_type_list)) { + if (!$this->did_add_template_types) { + return $this->buildWithTemplateTypes(); + } + } + if ($this->issues) { + $this->emitDeferredIssues(); + } + + if (!$this->comment_flags && + !$this->return_comment && + !$this->parameter_list && + !$this->variable_list && + !$this->template_type_list && + $this->inherited_type instanceof None && + !$this->suppress_issue_set && + !$this->magic_property_list && + !$this->magic_method_list && + !$this->phan_overrides && + $this->closure_scope instanceof None && + $this->throw_union_type->isEmpty() && + !$this->param_assertion_map + ) { + // Don't create an extra object if the string contained `@` but nothing of use was actually extracted. + return NullComment::instance(); + } + // @phan-suppress-next-line PhanAccessMethodInternal + return new Comment( + $this->comment_flags, + $this->variable_list, + $this->parameter_list, + \array_values($this->template_type_list), + $this->inherited_type, + $this->return_comment, + $this->suppress_issue_set, + $this->magic_property_list, + $this->magic_method_list, + $this->phan_overrides, + $this->closure_scope, + $this->throw_union_type, + $this->param_assertion_map, + // NOTE: The code base and context are used for emitting issues, and are not saved + $this->code_base, + $this->context + ); + } + + private function buildWithTemplateTypes(): Comment + { + $old_scope = $this->context->getScope(); + $new_scope = new TemplateScope($old_scope, $this->template_type_list); + $new_context = $this->context->withScope($new_scope); + // $result = Type::fromStringInContext('T', $new_context, Type::FROM_PHPDOC, $this->code_base); + return (new self( + $this->comment, + $this->code_base, + $new_context, + $this->lineno, + $this->comment_type, + true + ))->build(); + } + + private function parseCommentLine(int $i, string $line): void + { + // https://secure.php.net/manual/en/regexp.reference.internal-options.php + // (?i) makes this case-sensitive, (?-1) makes it case-insensitive + // phpcs:ignore Generic.Files.LineLength.MaxExceeded + if (\preg_match('/@((?i)param|deprecated|var|return|throws|throw|returns|inherits|extends|suppress|unused-param|phan-[a-z0-9_-]*(?-i)|method|property|property-read|property-write|abstract|template|PhanClosureScope|readonly|mixin|seal-(?:methods|properties))(?:[^a-zA-Z0-9_\x7f-\xff-]|$)/D', $line, $matches)) { + $case_sensitive_type = $matches[1]; + $type = \strtolower($case_sensitive_type); + + switch ($type) { + case 'param': + $this->parseParamLine($i, $line); + break; + case 'unused-param': + $this->parseUnusedParamLine($i, $line); + break; + case 'var': + $this->maybeParseVarLine($i, $line); + break; + case 'template': + $this->maybeParseTemplateType($i, $line); + break; + case 'inherits': + case 'extends': + $this->maybeParseInherits($i, $line, $type); + break; + case 'return': + $this->maybeParseReturn($i, $line); + break; + case 'returns': + $this->emitIssue( + Issue::MisspelledAnnotation, + $this->guessActualLineLocation($i), + '@returns', + 'Did you mean @return?' + ); + break; + case 'throws': + $this->maybeParseThrows($i, $line); + break; + case 'throw': + $this->emitIssue( + Issue::MisspelledAnnotation, + $this->guessActualLineLocation($i), + '@throw', + 'Did you mean @throws?' + ); + break; + case 'suppress': + $this->maybeParseSuppress($i, $line); + break; + case 'property': + case 'property-read': + case 'property-write': + $this->maybeParseProperty($i, $line); + break; + case 'method': + $this->maybeParseMethod($i, $line); + break; + case 'phanclosurescope': + case 'phan-closure-scope': + $this->maybeParsePhanClosureScope($i, $line); + break; + case 'readonly': + $this->setPhanAccessFlag($i, false, 'readonly'); + break; + case 'mixin': + $this->parseMixin($i, $line, 'mixin'); + break; + case 'seal-properties': + if ($this->checkCompatible('@seal-properties', [Comment::ON_CLASS], $i)) { + $this->comment_flags |= Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES; + } + return; + case 'seal-methods': + if ($this->checkCompatible('@seal-methods', [Comment::ON_CLASS], $i)) { + $this->comment_flags |= Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS; + } + return; + case 'deprecated': + if (\preg_match('/@deprecated\b/', $line, $match)) { + $this->comment_flags |= Flags::IS_DEPRECATED; + } + break; + case 'abstract': + if (\preg_match('/@abstract\b/', $line, $match)) { + $this->comment_flags |= Flags::IS_PHPDOC_ABSTRACT; + } + break; + default: + if (\strpos($type, 'phan-') === 0) { + $this->maybeParsePhanCustomAnnotation($i, $line, $type, $case_sensitive_type); + } + break; + } + } + + if (\strpos($line, '@internal') !== false) { + if (\preg_match('/@internal\b/', $line, $match)) { + $this->comment_flags |= Flags::IS_NS_INTERNAL; + } + } + + if (\strpos($line, 'verride') !== false) { + if (\preg_match('/@([Oo]verride)\b/', $line, $match)) { + // TODO: split class const and global const. + if ($this->checkCompatible('@override', [Comment::ON_METHOD, Comment::ON_CONST, Comment::ON_PROPERTY], $i)) { + $this->comment_flags |= Flags::IS_OVERRIDE_INTENDED; + } + } + } + } + + private function parseMixin(int $i, string $line, string $annotation_name): void + { + if (!Config::getValue('read_mixin_annotations')) { + return; + } + if (!$this->checkCompatible('@' . $annotation_name, [Comment::ON_CLASS], $i)) { + return; + } + if (\preg_match('/@(phan-)?mixin\s+(\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*)\b/', $line, $matches)) { + $type_string = $matches[2]; + $type = Type::fromStringInContext( + $type_string, + $this->context, + Type::FROM_PHPDOC, + $this->code_base + ); + // TODO: Warn about invalid mixins + if ($type->isObjectWithKnownFQSEN()) { + $this->phan_overrides['mixin'][] = $type; + } else { + Issue::maybeEmit( + $this->code_base, + $this->context, + Issue::InvalidMixin, + $this->guessActualLineLocation($i), + $type + ); + } + } + } + + private function parseParamLine(int $i, string $line): void + { + if (!$this->checkCompatible('@param', Comment::FUNCTION_LIKE, $i)) { + return; + } + $param = self::parameterFromCommentLine($line, false, $i); + $this->parameter_list[] = $param; + } + + private function parseUnusedParamLine(int $i, string $line): void + { + if (!$this->checkCompatible('@unused-param', Comment::FUNCTION_LIKE, $i)) { + return; + } + if (\preg_match(self::UNUSED_PARAM_COMMENT_REGEX, $line, $match)) { + // Currently only used in VariableTrackerPlugin + $name = $match[16]; + $this->phan_overrides['unused-param'][$name] = $name; + } else { + $this->emitIssue( + Issue::UnextractableAnnotation, + $this->guessActualLineLocation($i), + \trim($line) + ); + } + } + + private function maybeParseVarLine(int $i, string $line): void + { + if (!$this->checkCompatible('@var', Comment::HAS_VAR_ANNOTATION, $i)) { + return; + } + $comment_var = self::parameterFromCommentLine($line, true, $i); + if (\in_array($this->comment_type, Comment::FUNCTION_LIKE, true)) { + if ($comment_var->getName() !== '') { + $this->variable_list[] = $comment_var; + } else { + $this->emitIssue( + Issue::UnextractableAnnotation, + $this->guessActualLineLocation($i), + \trim($line) + ); + } + } else { + $this->variable_list[] = $comment_var; + } + } + + private function maybeParseTemplateType(int $i, string $line): void + { + // Make sure support for generic types is enabled + if (Config::getValue('generic_types_enabled')) { + if ($this->checkCompatible('@template', Comment::HAS_TEMPLATE_ANNOTATION, $i)) { + $template_type = $this->templateTypeFromCommentLine($line); + if ($template_type) { + $this->template_type_list[$template_type->getName()] = $template_type; + } + } + } + } + + private function maybeParseInherits(int $i, string $line, string $type): void + { + if (!$this->checkCompatible('@' . $type, [Comment::ON_CLASS], $i)) { + return; + } + // Make sure support for generic types is enabled + if (Config::getValue('generic_types_enabled')) { + $this->inherited_type = $this->inheritsFromCommentLine($line); + } + } + + private function maybeParsePhanInherits(int $i, string $line, string $type): void + { + if (!$this->checkCompatible('@' . $type, [Comment::ON_CLASS], $i)) { + return; + } + // Make sure support for generic types is enabled + if (Config::getValue('generic_types_enabled')) { + $this->phan_overrides['inherits'] = $this->inheritsFromCommentLine($line); + } + } + + /** + * @internal + */ + public const ASSERT_REGEX = '/@phan-assert(?:(-true-condition|-false-condition)|\s+(!?)(' . UnionType::union_type_regex . '))\s+\$' . self::WORD_REGEX . '/'; + private function assertFromCommentLine(string $line): ?Assertion + { + if (!\preg_match(self::ASSERT_REGEX, $line, $match)) { + return null; + } + $extra_text = $match[1]; + if (\strlen($extra_text) > 0) { + $assertion_type = $extra_text === '-true-condition' ? Assertion::IS_TRUE : Assertion::IS_FALSE; + $union_type = UnionType::empty(); + } else { + $assertion_type = $match[2] === '!' ? Assertion::IS_NOT_OF_TYPE : Assertion::IS_OF_TYPE; + $type_string = $match[3]; + $union_type = UnionType::fromStringInContext( + $type_string, + $this->context, + Type::FROM_PHPDOC, + $this->code_base + ); + } + $param_name = $match[17]; + + return new Assertion($union_type, $param_name, $assertion_type); + } + + private function maybeParsePhanAssert(int $i, string $line): void + { + if (!$this->checkCompatible('@phan-assert', Comment::FUNCTION_LIKE, $i)) { + return; + } + // Make sure support for generic types is enabled + $assert = $this->assertFromCommentLine($line); + if ($assert) { + $this->param_assertion_map[$assert->param_name] = $assert; + } + } + + private function setPhanAccessFlag(int $i, bool $write_only, string $name): void + { + // Make sure support for generic types is enabled + if ($this->comment_type === Comment::ON_PROPERTY) { + $this->comment_flags |= ($write_only ? Flags::IS_WRITE_ONLY : Flags::IS_READ_ONLY); + return; + } + if ($write_only) { + $this->checkCompatible("@$name", [Comment::ON_PROPERTY], $i); + return; + } + if ($this->checkCompatible("@$name", [Comment::ON_PROPERTY, Comment::ON_CLASS], $i)) { + $this->comment_flags |= Flags::IS_READ_ONLY; + } + } + + private function maybeParseReturn(int $i, string $line): void + { + if (!$this->checkCompatible('@return', Comment::FUNCTION_LIKE, $i)) { + return; + } + $return_comment = $this->return_comment; + $new_type = $this->returnTypeFromCommentLine($line, $i); + if ($return_comment) { + $return_comment->setType($return_comment->getType()->withUnionType($new_type)); + } else { + $this->return_comment = new ReturnComment($new_type, $this->guessActualLineLocation($i)); + } + } + + private function maybeParseThrows(int $i, string $line): void + { + if (!$this->checkCompatible('@throws', Comment::FUNCTION_LIKE, $i)) { + return; + } + $this->throw_union_type = $this->throw_union_type->withUnionType( + $this->returnTypeFromCommentLine($line, $i) + ); + } + + private function maybeParseSuppress(int $i, string $line): void + { + $suppress_issue_types = $this->suppressIssuesFromCommentLine($line); + if (count($suppress_issue_types) > 0) { + foreach ($suppress_issue_types as $issue_type) { + $this->suppress_issue_set[$issue_type] = 0; + } + } else { + $this->emitIssue( + Issue::UnextractableAnnotation, + $this->guessActualLineLocation($i), + \trim($line) + ); + } + } + + private function maybeParseProperty(int $i, string $line): void + { + if (!$this->checkCompatible('@property', [Comment::ON_CLASS], $i)) { + return; + } + // Make sure support for magic properties is enabled. + if (Config::getValue('read_magic_property_annotations')) { + $magic_property = $this->magicPropertyFromCommentLine($line, $i); + if ($magic_property !== null) { + $this->magic_property_list[] = $magic_property; + } + } + } + + private function maybeParseMethod(int $i, string $line): void + { + // Make sure support for magic methods is enabled. + if (Config::getValue('read_magic_method_annotations')) { + if (!$this->checkCompatible('@method', [Comment::ON_CLASS], $i)) { + return; + } + $magic_method = $this->magicMethodFromCommentLine($line, $i); + if ($magic_method !== null) { + $this->magic_method_list[] = $magic_method; + } + } + } + + private function maybeParsePhanClosureScope(int $i, string $line): void + { + // TODO: different type for closures + if ($this->checkCompatible('@phan-closure-scope', Comment::FUNCTION_LIKE, $i)) { + $this->closure_scope = $this->getPhanClosureScopeFromCommentLine($line, $i); + } + } + + private function maybeParsePhanCustomAnnotation(int $i, string $line, string $type, string $case_sensitive_type): void + { + switch ($type) { + case 'phan-forbid-undeclared-magic-properties': + if ($this->checkCompatible('@phan-forbid-undeclared-magic-properties', [Comment::ON_CLASS], $i)) { + $this->comment_flags |= Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES; + } + return; + case 'phan-hardcode-return-type': + if ($this->checkCompatible('@phan-hardcode-return-type', Comment::FUNCTION_LIKE, $i)) { + $this->comment_flags |= Flags::HARDCODED_RETURN_TYPE; + } + return; + case 'phan-forbid-undeclared-magic-methods': + if ($this->checkCompatible('@phan-forbid-undeclared-magic-methods', [Comment::ON_CLASS], $i)) { + $this->comment_flags |= Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS; + } + return; + case 'phan-closure-scope': + if ($this->checkCompatible('@phan-closure-scope', Comment::FUNCTION_LIKE, $i)) { + $this->closure_scope = $this->getPhanClosureScopeFromCommentLine($line, $i); + } + return; + case 'phan-param': + if ($this->checkCompatible('@phan-param', Comment::FUNCTION_LIKE, $i)) { + $this->phan_overrides['param'][] = + $this->parameterFromCommentLine($line, false, $i); + } + return; + case 'phan-real-return': + if ($this->checkCompatible('@phan-real-return', Comment::FUNCTION_LIKE, $i)) { + $this->phan_overrides['real-return'] = new ReturnComment($this->returnTypeFromCommentLine($line, $i)->asRealUnionType(), $this->guessActualLineLocation($i)); + } + return; + case 'phan-return': + if ($this->checkCompatible('@phan-return', Comment::FUNCTION_LIKE, $i)) { + $this->phan_overrides['return'] = new ReturnComment($this->returnTypeFromCommentLine($line, $i), $this->guessActualLineLocation($i)); + } + return; + case 'phan-override': + if ($this->checkCompatible('@override', [Comment::ON_METHOD, Comment::ON_CONST], $i)) { + $this->comment_flags |= Flags::IS_OVERRIDE_INTENDED; + } + return; + case 'phan-abstract': + $this->comment_flags |= Flags::IS_PHPDOC_ABSTRACT; + return; + case 'phan-var': + if (!$this->checkCompatible('@phan-var', Comment::HAS_VAR_ANNOTATION, $i)) { + return; + } + $comment_var = $this->parameterFromCommentLine($line, true, $i); + if (\in_array($this->comment_type, Comment::FUNCTION_LIKE, true)) { + if ($comment_var->getName() !== '') { + $this->phan_overrides['var'][] = $comment_var; + } else { + $this->emitIssue( + Issue::UnextractableAnnotation, + $this->guessActualLineLocation($i), + \trim($line) + ); + } + } else { + $this->phan_overrides['var'][] = $comment_var; + } + return; + case 'phan-file-suppress': + // See BuiltinSuppressionPlugin + return; + case 'phan-unused-param': + $this->parseUnusedParamLine($i, $line); + return; + case 'phan-suppress': + $this->maybeParseSuppress($i, $line); + return; + case 'phan-property': + case 'phan-property-read': + case 'phan-property-write': + $this->parsePhanProperty($i, $line); + return; + case 'phan-pure': + case 'phan-side-effect-free': + // phan-side-effect-free = "phan-immutable" + "phan-external-mutation-free". + // - Note that there is no way for Phan to use phan-external-mutation-free for analysis on its own right now, + // so that annotation doesn't exist. + // + // Note that phan-side-effect-free is recommended over phan-pure to avoid confusion. + // Functions with this annotation are not + if ($this->checkCompatible('@' . $type, \array_merge(Comment::FUNCTION_LIKE, [Comment::ON_CLASS]), $i)) { + $this->comment_flags |= Flags::IS_SIDE_EFFECT_FREE; + } + return; + case 'phan-immutable': + $this->setPhanAccessFlag($i, false, 'phan-immutable'); + break; + case 'phan-method': + $this->parsePhanMethod($i, $line); + return; + case 'phan-suppress-next-line': + case 'phan-suppress-next-next-line': + case 'phan-suppress-current-line': + case 'phan-suppress-previous-line': + // Do nothing, see BuiltinSuppressionPlugin + return; + case 'phan-template': + $this->maybeParseTemplateType($i, $line); + return; + case 'phan-inherits': + case 'phan-extends': + $this->maybeParsePhanInherits($i, $line, (string)\substr($type, 5)); + return; + case 'phan-read-only': + $this->setPhanAccessFlag($i, false, 'phan-read-only'); + return; + case 'phan-write-only': + $this->setPhanAccessFlag($i, true, 'phan-write-only'); + return; + case 'phan-transient': + // Do nothing, see SleepCheckerPlugin + return; + case 'phan-assert': + case 'phan-assert-true-condition': + case 'phan-assert-false-condition': + $this->maybeParsePhanAssert($i, $line); + return; + case 'phan-constructor-used-for-side-effects': + if ($this->checkCompatible('@' . $type, [Comment::ON_CLASS], $i)) { + $this->comment_flags |= Flags::IS_CONSTRUCTOR_USED_FOR_SIDE_EFFECTS; + } + return; + case 'phan-mixin': + $this->parseMixin($i, $line, 'phan-mixin'); + return; + default: + $this->emitIssueWithSuggestion( + Issue::MisspelledAnnotation, + $this->guessActualLineLocation($i), + [ + '@' . $case_sensitive_type, + "The annotations that this version of Phan supports can be seen by running 'phan --help-annotations' or by visiting https://github.com/phan/phan/wiki/Annotating-Your-Source-Code", + ], + self::generateSuggestionForMisspelledAnnotation($case_sensitive_type) + ); + return; + } + } + + private static function generateSuggestionForMisspelledAnnotation(string $annotation): ?Suggestion + { + $suggestions = IssueFixSuggester::getSuggestionsForStringSet('@' . $annotation, self::SUPPORTED_ANNOTATIONS); + if (!$suggestions) { + return null; + } + return Suggestion::fromString('Did you mean ' . \implode(' or ', \array_keys($suggestions))); + } + + /** + * Maps supported annotations starting with phan- to the empty string or a description + */ + public const SUPPORTED_ANNOTATIONS = [ + '@phan-assert' => '', + '@phan-assert-true-condition' => '', + '@phan-assert-false-condition' => '', + '@phan-closure-scope' => '', + '@phan-constructor-used-for-side-effects' => '', + '@phan-extends' => '', + '@phan-file-suppress' => '', + '@phan-forbid-undeclared-magic-methods' => '', + '@phan-forbid-undeclared-magic-properties' => '', + '@phan-hardcode-return-type' => '', + '@phan-inherits' => '', + '@phan-method' => '', + '@phan-mixin' => '', + '@phan-override' => '', + '@phan-param' => '', + '@phan-property' => '', + '@phan-property-read' => '', + '@phan-property-write' => '', + '@phan-pure' => '', + '@phan-read-only' => '', + '@phan-return' => '', + '@phan-real-return' => '', + '@phan-suppress' => '', + '@phan-suppress-current-line' => '', + '@phan-suppress-next-line' => '', + '@phan-suppress-next-next-line' => '', + '@phan-suppress-previous-line' => '', + '@phan-template' => '', + '@phan-var' => '', + '@phan-write-only' => '', + ]; + + private function parsePhanProperty(int $i, string $line): void + { + if (!$this->checkCompatible('@phan-property', [Comment::ON_CLASS], $i)) { + return; + } + // Make sure support for magic properties is enabled. + if (!Config::getValue('read_magic_property_annotations')) { + return; + } + $magic_property = $this->magicPropertyFromCommentLine($line, $i); + if ($magic_property !== null) { + $this->phan_overrides['property'][] = $magic_property; + } + } + + private function parsePhanMethod(int $i, string $line): void + { + if (!$this->checkCompatible('@phan-method', [Comment::ON_CLASS], $i)) { + return; + } + // Make sure support for magic methods is enabled. + if (!Config::getValue('read_magic_method_annotations')) { + return; + } + $magic_method = $this->magicMethodFromCommentLine($line, $i); + if ($magic_method !== null) { + $this->phan_overrides['method'][] = $magic_method; + } + } + + private function guessActualLineLocation(int $i): int + { + $path = Config::projectPath($this->context->getFile()); + $entry = FileCache::getEntry($path); + $declaration_lineno = $this->lineno; + if (!$entry) { + return $declaration_lineno; + } + // $lineno_search <= $declaration_lineno + $lineno_search = $declaration_lineno - ($this->comment_lines_count - $i - 1); + // Search up to 10 lines before $lineno_search + $lineno_stop = \max(1, $lineno_search - 9); + $lines_array = $entry->getLines(); + + $line = $this->lines[$i]; + $trimmed_line = \trim($line); + for ($check_lineno = $lineno_search; $check_lineno >= $lineno_stop; $check_lineno--) { + $cur_line = $lines_array[$check_lineno]; + if (\strpos($cur_line, $line) !== false) { + // Better heuristic: Lines in the middle of phpdoc are guaranteed to be complete, including a few newlines at the end. + $j = $i - ($lineno_search - $check_lineno); + if ($j > 0 && $j < $this->comment_lines_count - 1) { + if ($trimmed_line !== \trim($cur_line)) { + continue; + } + } + return $check_lineno; + } + } + // We couldn't identify the line; + return $declaration_lineno; + } + + /** + * Find the line number of line $i of the doc comment with lines $lines + * + * @param list $lines + * @suppress PhanUnreferencedPublicMethod + */ + public static function findLineNumberOfCommentForElement(AddressableElementInterface $element, array $lines, int $i): int + { + $context = $element->getContext(); + + $entry = FileCache::getOrReadEntry(Config::projectPath($context->getFile())); + $declaration_lineno = $context->getLineNumberStart(); + $lines_array = $entry->getLines(); + $count = \count($lines); + $lineno_search = $declaration_lineno - ($count - $i - 1); + $lineno_stop = \max(1, $lineno_search - 9); + $line = $lines[$i]; + $trimmed_line = \trim($lines[$i]); + for ($check_lineno = $lineno_search; $check_lineno >= $lineno_stop; $check_lineno--) { + $cur_line = $lines_array[$check_lineno]; + if (\strpos($cur_line, $line) !== false) { + // Better heuristic: Lines in the middle of phpdoc are guaranteed to be complete, including a few newlines at the end. + $j = $i - ($lineno_search - $check_lineno); + if ($j > 0 && $j < $count - 1) { + if ($trimmed_line !== \trim($cur_line)) { + continue; + } + } + return $check_lineno; + } + } + return $declaration_lineno; + } + + /** + * @param list $valid_types + * @suppress PhanAccessClassConstantInternal + */ + private function checkCompatible(string $param_name, array $valid_types, int $i): bool + { + if (\in_array($this->comment_type, $valid_types, true) || $this->comment_type === Comment::ON_ANY) { + return true; + } + $this->emitInvalidCommentForDeclarationType( + $param_name, + $this->guessActualLineLocation($i) + ); + return false; + } + + private function emitInvalidCommentForDeclarationType( + string $annotation_type, + int $issue_lineno + ): void { + $this->emitIssue( + Issue::InvalidCommentForDeclarationType, + $issue_lineno, + $annotation_type, + Comment::NAME_FOR_TYPE[$this->comment_type] + ); + } + + /** + * @param string $line + * An individual line of a comment + * + * @return TemplateType|null + * A generic type identifier or null if a valid type identifier + * wasn't found. + */ + private static function templateTypeFromCommentLine( + string $line + ): ?TemplateType { + // Backslashes or nested templates wouldn't make sense, so use WORD_REGEX. + if (\preg_match('/@(?:phan-)?template\s+(' . self::WORD_REGEX . ')/', $line, $match)) { + $template_type_identifier = $match[1]; + return TemplateType::instanceForId($template_type_identifier, false); + } + + return null; + } + + /** + * @param string $line + * An individual line of a comment + * + * @return Option + * An optional type overriding the extended type of the class + */ + private function inheritsFromCommentLine( + string $line + ): Option { + $match = []; + if (\preg_match('/@(?:phan-)?(?:inherits|extends)\s+(' . Type::type_regex . ')/', $line, $match)) { + $type_string = $match[1]; + + $type = new Some(Type::fromStringInContext( + $type_string, + $this->context, + Type::FROM_PHPDOC, + $this->code_base + )); + + return $type; + } + + return None::instance(); + } + + /** + * This regex contains a single pattern, which matches a valid PHP identifier. + * (e.g. for variable names, magic property names, etc.) + * This does not allow backslashes. + */ + public const WORD_REGEX = '([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)'; + + /** + * This regex contains a single pattern, which matches a reasonable Phan issue name + * (e.g. for variable names, magic property names, etc.) + * + * E.g. "PhanPluginSomeIssueName" (preferred), "PhanPlugin_some_issue_name", and "PhanPlugin-some-issue-name". + * + * Note that Phan doesn't forbid using names not matching this regex in the Issue constructor at the time of writing. + */ + public const ISSUE_REGEX = '([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(-[a-zA-Z0-9_\x7f-\xff]+)*)'; + + /** + * @internal + */ + public const SUPPRESS_ISSUE_LIST = '(' . self::ISSUE_REGEX . '(,\s*' . self::ISSUE_REGEX . ')*)'; + + /** + * @internal + */ + public const PHAN_SUPPRESS_REGEX = '/@(?:phan-)?suppress\s+' . self::SUPPRESS_ISSUE_LIST . '/'; + + /** + * @param string $line + * An individual line of a comment + * + * @return list + * 0 or more issue names to suppress + */ + private static function suppressIssuesFromCommentLine( + string $line + ): array { + if (\preg_match(self::PHAN_SUPPRESS_REGEX, $line, $match)) { + return \array_map('trim', \explode(',', $match[1])); + } + + return []; + } + + /** @internal */ + public const MAGIC_PARAM_REGEX = '/^(' . UnionType::union_type_regex . ')?\s*(?:(\.\.\.)\s*)?(?:\$' . self::WORD_REGEX . ')?((?:\s*=.*)?)$/'; + + /** + * Parses a magic method based on https://phpdoc.org/docs/latest/references/phpdoc/tags/method.html + * @return ?Parameter - if null, the phpdoc magic method was invalid. + */ + private function magicParamFromMagicMethodParamString( + string $param_string, + int $param_index, + int $comment_line_offset + ): ?Parameter { + $param_string = \trim($param_string); + // Don't support trailing commas, or omitted params. Provide at least one of [type] or [parameter] + if ($param_string === '') { + return null; + } + // Parse an entry for [type] [parameter] - Assume both of those are optional. + // https://github.com/phpDocumentor/phpDocumentor2/pull/1271/files - phpdoc allows passing an default value. + // Phan allows `=.*`, to indicate that a parameter is optional + // TODO: in another PR, check that optional parameters aren't before required parameters. + if (\preg_match(self::MAGIC_PARAM_REGEX, $param_string, $param_match)) { + // Note: a magic method parameter can be variadic, but it can't be pass-by-reference? (No support in __call) + $union_type_string = $param_match[1]; + $union_type = UnionType::fromStringInContext( + $union_type_string, + $this->context, + Type::FROM_PHPDOC, + $this->code_base + ); + $is_variadic = $param_match[15] === '...'; + $default_str = $param_match[17]; + $has_default_value = $default_str !== ''; + if ($has_default_value) { + $default_value_representation = \trim(\explode('=', $default_str, 2)[1]); + if (\strcasecmp($default_value_representation, 'null') === 0) { + $union_type = $union_type->nullableClone(); + } + } else { + $default_value_representation = null; + } + $var_name = $param_match[16]; + if ($var_name === '') { + // placeholder names are p1, p2, ... + $var_name = 'p' . ($param_index + 1); + } + return new Parameter($var_name, $union_type, $this->guessActualLineLocation($comment_line_offset), $is_variadic, $has_default_value, false, false, false, $default_value_representation); + } + return null; + } + + /** + * @param string $line + * An individual line of a comment + * + * @return ?Method + * magic method with the parameter types, return types, and name. + */ + private function magicMethodFromCommentLine( + string $line, + int $comment_line_offset + ): ?Method { + // https://phpdoc.org/docs/latest/references/phpdoc/tags/method.html + // > Going to assume "static" is a magic keyword, based on https://github.com/phpDocumentor/phpDocumentor2/issues/822 + // > TODO: forbid in trait? + // TODO: finish writing the regex. + // Syntax: + // @method [return type] [name]([[type] [parameter]<, ...>]) [] + // Assumes the parameters end at the first ")" after "(" + // As an exception, allows one level of matching brackets + // to support old style arrays such as $x = array(), $x = array(2) (Default values are ignored) + if (\preg_match('/@(?:phan-)?method(?:\s+(static))?(?:(?:\s+(' . UnionType::union_type_regex_or_this . '))?)\s+' . self::WORD_REGEX . '\s*\(((?:[^()]|\([()]*\))*)\)\s*(.*)/', $line, $match)) { + $is_static = $match[1] === 'static'; + $return_union_type_string = $match[2]; + if ($return_union_type_string !== '') { + $return_union_type = + UnionType::fromStringInContext( + $return_union_type_string, + $this->context, + Type::FROM_PHPDOC, + $this->code_base + ); + } else { + // From https://phpdoc.org/docs/latest/references/phpdoc/tags/method.html + // > When the intended method does not have a return value then the return type MAY be omitted; in which case 'void' is implied. + if ($is_static) { + // > `static` on its own would mean that the method returns an instance of the child class which the method is called on. + $return_union_type = StaticType::instance(false)->asPHPDocUnionType(); + $is_static = false; + } else { + $return_union_type = VoidType::instance(false)->asPHPDocUnionType(); + } + } + $method_name = $match[22]; + + $arg_list = \trim($match[23]); + $comment_params = []; + // Special check if param list has 0 params. + if ($arg_list !== '') { + // TODO: Would need to use a different approach if templates were ever supported + // e.g. The magic method parsing doesn't support commas? + $params_strings = self::extractMethodParts($arg_list); + $failed = false; + foreach ($params_strings as $i => $param_string) { + $param = $this->magicParamFromMagicMethodParamString($param_string, $i, $comment_line_offset); + if ($param === null) { + $this->emitIssue( + Issue::UnextractableAnnotationPart, + $this->guessActualLineLocation($comment_line_offset), + \trim($line), + $param_string + ); + $failed = true; + } + $comment_params[] = $param; + } + if ($failed) { + // Emit everything that was wrong with the parameters of the @method annotation at once, then reject it. + return null; + } + } + + return new Method( + $method_name, + $return_union_type, + $comment_params, + $is_static, + $this->guessActualLineLocation($comment_line_offset) + ); + } else { + $this->emitIssue( + Issue::UnextractableAnnotation, + $this->guessActualLineLocation($comment_line_offset), + \trim($line) + ); + } + + return null; + } + + /** + * @return list + */ + private static function extractMethodParts(string $type_string): array + { + $parts = []; + foreach (\explode(',', $type_string) as $part) { + $parts[] = \trim($part); + } + + if (\count($parts) <= 1) { + return $parts; + } + if (!\preg_match('/[<({]/', $type_string)) { + return $parts; + } + return self::mergeMethodParts($parts); + } + + /** + * @param string[] $parts (already trimmed) + * @return string[] + * @see Type::extractTemplateParameterTypeNameList() (Similar method) + */ + private static function mergeMethodParts(array $parts): array + { + $prev_parts = []; + $delta = 0; + $results = []; + foreach ($parts as $part) { + if (\count($prev_parts) > 0) { + $prev_parts[] = $part; + $delta += \substr_count($part, '<') + \substr_count($part, '(') + \substr_count($part, '{') - \substr_count($part, '>') - \substr_count($part, ')') - \substr_count($part, '}'); + if ($delta <= 0) { + if ($delta === 0) { + $results[] = \implode(',', $prev_parts); + } // ignore unparsable data such as ">" + $prev_parts = []; + $delta = 0; + continue; + } + continue; + } + $bracket_count = \substr_count($part, '<') + \substr_count($part, '(') + \substr_count($part, '{'); + if ($bracket_count === 0) { + $results[] = $part; + continue; + } + $delta = $bracket_count - \substr_count($part, '>') - \substr_count($part, ')') - \substr_count($part, '}'); + if ($delta === 0) { + $results[] = $part; + } elseif ($delta > 0) { + $prev_parts[] = $part; + } // otherwise ignore unparsable data such as ">" (should be impossible) + } + return $results; + } + + /** + * @param string $line + * An individual line of a comment + * Analysis will handle (at)property-read and (at)property-write differently from + * (at)property. + * + * @return Property|null + * magic property with the union type. + */ + private function magicPropertyFromCommentLine( + string $line, + int $i + ): ?Property { + // Note that the type of a property can be left out (@property $myVar) - This is equivalent to @property mixed $myVar + // TODO: properly handle duplicates... + if (\preg_match('/@(?:phan-)?(property|property-read|property-write)(?:\s+(' . UnionType::union_type_regex . '))?(?:\s+(?:\\$' . self::WORD_REGEX . '))/', $line, $match)) { + $category = $match[1]; + if ($category === 'property-read') { + $flags = Flags::IS_READ_ONLY; + } elseif ($category === 'property-write') { + $flags = Flags::IS_WRITE_ONLY; + } else { + $flags = 0; + } + $type = $match[2] ?? ''; + + $property_name = $match[16] ?? ''; + if ($property_name === '') { + return null; + } + + // If the type looks like a property name, make it an + // empty type so that other stuff can match it. + $union_type = + UnionType::fromStringInContext( + $type, + $this->context, + Type::FROM_PHPDOC, + $this->code_base + ); + + return new Property( + $property_name, + $union_type, + $this->guessActualLineLocation($i), + $flags + ); + } else { + $this->emitIssue( + Issue::UnextractableAnnotation, + $this->guessActualLineLocation($i), + \trim($line) + ); + } + + return null; + } + + /** + * The context in which the comment line appears + * + * @param string $line + * An individual line of a comment + * + * @return Option + * A class/interface to use as a context for a closure. + * (Phan expects a ClassScope to have exactly one type) + */ + private function getPhanClosureScopeFromCommentLine( + string $line, + int $comment_line_offset + ): Option { + $closure_scope_union_type_string = ''; + + // https://secure.php.net/manual/en/closure.bindto.php + // There wasn't anything in the phpdoc standard to indicate the class to which + // a Closure would be bound with bind() or bindTo(), so using a custom tag. + // + // TODO: Also add a version which forbids using $this in the closure? + if (\preg_match('/@(PhanClosureScope|phan-closure-scope)\s+(' . Type::type_regex . ')/', $line, $match)) { + $closure_scope_union_type_string = $match[2]; + } + + if ($closure_scope_union_type_string !== '') { + return new Some(Type::fromStringInContext( + $closure_scope_union_type_string, + $this->context, + Type::FROM_PHPDOC, + $this->code_base + )); + } + $this->emitIssue( + Issue::UnextractableAnnotation, + $this->guessActualLineLocation($comment_line_offset), + \trim($line) + ); + return None::instance(); + } + + /** + * @param string $issue_type + * The type of issue to emit such as Issue::ParentlessClass + * + * @param int $issue_lineno + * The line number where the issue was found + * + * @param int|string|FQSEN|UnionType|Type ...$parameters + * Template parameters for the issue's error message + */ + protected function emitIssue( + string $issue_type, + int $issue_lineno, + ...$parameters + ): void { + $this->issues[] = [ + $issue_type, + $issue_lineno, + $parameters, + null, + ]; + } + + /** + * @param string $issue_type + * The type of issue to emit such as Issue::ParentlessClass + * + * @param int $issue_lineno + * The line number where the issue was found + * + * @param list $parameters + * Template parameters for the issue's error message + * + * @param ?Suggestion $suggestion + */ + protected function emitIssueWithSuggestion( + string $issue_type, + int $issue_lineno, + array $parameters, + Suggestion $suggestion = null + ): void { + $this->issues[] = [ + $issue_type, + $issue_lineno, + $parameters, + $suggestion + ]; + } + + protected function emitDeferredIssues(): void + { + foreach ($this->issues as [$issue_type, $issue_lineno, $parameters, $suggestion]) { + if (\array_key_exists($issue_type, $this->suppress_issue_set)) { + // Record that this suppression has been used. + $this->suppress_issue_set[$issue_type] = 1; + continue; + } + Issue::maybeEmitWithParameters( + $this->code_base, + $this->context, + $issue_type, + $issue_lineno, + $parameters, + $suggestion + ); + } + $this->issues = []; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Method.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Method.php new file mode 100644 index 000000000..14243c3fc --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Method.php @@ -0,0 +1,168 @@ + + * A list of phpdoc parameters + */ + private $parameters; + + /** + * @var bool + * Whether or not this is a static magic method + */ + private $is_static; + + /** + * @var int + * The line of this method + */ + private $line; + + /** + * @param string $name + * The name of the method + * + * @param UnionType $type + * The return type of the method + * + * @param list $parameters + * 0 or more comment parameters for this magic method + * + * @param bool $is_static + * Whether this method is static + * + * @param int $line + * The line of this method + */ + public function __construct( + string $name, + UnionType $type, + array $parameters, + bool $is_static, + int $line + ) { + $this->name = $name; + $this->type = $type; + $this->parameters = $parameters; + $this->is_static = $is_static; + $this->line = $line; + } + + /** + * @return string + * The name of the magic method + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return UnionType + * The return type of the magic method + */ + public function getUnionType(): UnionType + { + return $this->type; + } + + /** + * @return list - comment parameters of magic method, from phpdoc. + */ + public function getParameterList(): array + { + return $this->parameters; + } + + /** + * @return bool + * Whether or not the magic method is static + */ + public function isStatic(): bool + { + return $this->is_static; + } + + /** + * @return int + * The line of this method + */ + public function getLine(): int + { + return $this->line; + } + + /** + * @return int + * Number of required parameters of this method + */ + public function getNumberOfRequiredParameters(): int + { + return \array_reduce( + $this->parameters, + static function (int $carry, Parameter $parameter): int { + return ($carry + ($parameter->isRequired() ? 1 : 0)); + }, + 0 + ); + } + + /** + * @return int + * Number of optional parameters of this method + */ + public function getNumberOfOptionalParameters(): int + { + return \array_reduce( + $this->parameters, + static function (int $carry, Parameter $parameter): int { + return ($carry + ($parameter->isOptional() ? 1 : 0)); + }, + 0 + ); + } + + public function __toString(): string + { + if ($this->is_static) { + $string = 'static function '; + } else { + $string = 'function '; + } + // Magic methods can't be by ref? + $string .= $this->name; + + $string .= '(' . \implode(', ', $this->parameters) . ')'; + + if (!$this->type->isEmpty()) { + $string .= ' : ' . (string)$this->type; + } + + return $string; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/NullComment.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/NullComment.php new file mode 100644 index 000000000..bc80f1b08 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/NullComment.php @@ -0,0 +1,44 @@ +throw_union_type = UnionType::empty(); + $this->closure_scope = $none; + $this->inherited_type = $none; + } + + /** @var NullComment the only instance of NullComment. Will be immutable. */ + private static $instance; + + /** + * Returns the immutable NullComment. + */ + public static function instance(): NullComment + { + return self::$instance; + } + + /** + * Ensures the static property is set, for users of this class + * @internal + */ + public static function init(): void + { + self::$instance = new NullComment(); + } +} +NullComment::init(); diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Parameter.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Parameter.php new file mode 100644 index 000000000..03af563bb --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Parameter.php @@ -0,0 +1,256 @@ +name = $name; + $this->type = $type; + $this->lineno = $lineno; + $this->is_variadic = $is_variadic; + $this->has_default_value = $has_default_value; + $this->is_mandatory_in_phpdoc = $is_mandatory_in_phpdoc; + $this->default_value_representation = $default_value_representation; + if ($is_ignored_reference) { + $this->reference_type = self::REFERENCE_IGNORED; + } elseif ($is_output_reference) { + $this->reference_type = self::REFERENCE_OUTPUT; + } else { + $this->reference_type = self::REFERENCE_DEFAULT; + } + } + + /** + * Returns this comment parameter as a real variable. + */ + public function asVariable( + Context $context + ): Variable { + return new Variable( + $context, + $this->name, + $this->type, + 0 + ); + } + + /** + * Converts this parameter to a real parameter, + * using only the information from the comment. + * + * Useful for comments extracted from (at)method, etc. + */ + public function asRealParameter( + Context $context + ): \Phan\Language\Element\Parameter { + $flags = 0; + if ($this->is_variadic) { + $flags |= \ast\flags\PARAM_VARIADIC; + } + $union_type = $this->type; + $param = \Phan\Language\Element\Parameter::create( + $context, + $this->name, + $union_type, + $flags + ); + if ($this->has_default_value) { + $param->setDefaultValueType($union_type); + // TODO: could setDefaultValue in a future PR. Would have to run \ast\parse_code on the default value, catch ParseError/CompileError if necessary. + // If given '= "Default"', then extract the default from 'setDefaultValueRepresentation($this->default_value_representation); + return $param; + } + + /** + * @return string + * The name of the parameter + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return UnionType + * The type of the parameter + */ + public function getUnionType(): UnionType + { + return $this->type; + } + + /** + * Add types (from another comment) to this comment parameter. + * + * @param UnionType $other + * The type to add to the parameter (for conflicting param tags) + * @internal + */ + public function addUnionType(UnionType $other): void + { + $this->type = $this->type->withUnionType($other); + } + + /** + * @return int + * The line number of the parameter + */ + public function getLineno(): int + { + return $this->lineno; + } + /** + * @return bool + * Whether or not the parameter is variadic + */ + public function isVariadic(): bool + { + return $this->is_variadic; + } + + /** + * @return bool + * Whether or not the parameter is an output reference + */ + public function isOutputReference(): bool + { + return $this->reference_type === self::REFERENCE_OUTPUT; + } + + /** + * @return bool + * Whether or not the parameter is an ignored reference + */ + public function isIgnoredReference(): bool + { + return $this->reference_type === self::REFERENCE_IGNORED; + } + + /** + * @return bool + * Whether or not the parameter is required + */ + public function isRequired(): bool + { + return !$this->isOptional(); + } + + /** + * @return bool + * Whether or not the parameter is optional + */ + public function isOptional(): bool + { + return $this->has_default_value || $this->is_variadic; + } + + /** + * @return bool + * Whether or not the parameter is marked as mandatory in phpdoc + */ + public function isMandatoryInPHPDoc(): bool + { + return $this->is_mandatory_in_phpdoc; + } + + public function __toString(): string + { + $string = ''; + + if (!$this->type->isEmpty()) { + $string .= "{$this->type} "; + } + if ($this->is_variadic) { + $string .= '...'; + } + + $string .= '$' . $this->name; + + if ($this->has_default_value) { + $string .= ' = default'; + } + + return $string; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Property.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Property.php new file mode 100644 index 000000000..a52f11153 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/Property.php @@ -0,0 +1,109 @@ +name = $name; + $this->type = $type; + $this->line = $line; + $this->flags = $flags; + } + + /** + * @return string + * The name of the property + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return UnionType + * The type of the property + */ + public function getUnionType(): UnionType + { + return $this->type; + } + + /** + * @return int + * The line of the property + */ + public function getLine(): int + { + return $this->line; + } + + /** + * @return int + * The flags of the property + */ + public function getFlags(): int + { + return $this->flags; + } + + public function __toString(): string + { + $string = ''; + + if (!$this->type->isEmpty()) { + $string .= "{$this->type} "; + } + $string .= '$' . $this->name; + + return $string; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/ReturnComment.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/ReturnComment.php new file mode 100644 index 000000000..293281b3a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Comment/ReturnComment.php @@ -0,0 +1,57 @@ +type = $type; + $this->lineno = $lineno; + } + + /** + * Gets the type of this (at)return comment + */ + public function getType(): UnionType + { + return $this->type; + } + + /** + * Sets the type of this (at)return comment + */ + public function setType(UnionType $type): void + { + $this->type = $type; + } + + /** + * Gets the line number of this (at)return comment's declaration in PHPDoc + */ + public function getLineno(): int + { + return $this->lineno; + } + + /** + * Helper for debugging + */ + public function __toString(): string + { + return "ReturnComment(type=$this->type)"; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/ConstantInterface.php b/bundled-libs/phan/phan/src/Phan/Language/Element/ConstantInterface.php new file mode 100644 index 000000000..52da79692 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/ConstantInterface.php @@ -0,0 +1,44 @@ +getName(); + } + + /** + * Sets the node with the AST representing the value of this constant. + * + * @param Node|string|float|int|resource $node Either a node or a constant to be used as the value of the constant. + * Can be resource for STDERR, etc. + */ + public function setNodeForValue($node): void + { + $this->defining_node = $node; + } + + /** + * Gets the node with the AST representing the value of this constant. + * + * @return Node|string|float|int|resource + */ + public function getNodeForValue() + { + return $this->defining_node; + } + + /** + * Used by daemon mode to restore an element to the state it had before parsing. + * @internal + */ + public function createRestoreCallback(): ?Closure + { + $future_union_type = $this->future_union_type; + if ($future_union_type === null) { + // We already inferred the type for this class constant/global constant. + // Nothing to do. + return null; + } + // If this refers to a class constant in another file, + // the resolved union type might change if that file changes. + return function () use ($future_union_type): void { + $this->future_union_type = $future_union_type; + // Probably don't need to call setUnionType(mixed) again... + }; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/ElementFutureUnionType.php b/bundled-libs/phan/phan/src/Phan/Language/Element/ElementFutureUnionType.php new file mode 100644 index 000000000..0c87cb214 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/ElementFutureUnionType.php @@ -0,0 +1,83 @@ +future_union_type = $future_union_type; + } + + /** + * @return bool + * Returns true if this element has an unresolved union type. + * + * @internal because this is mostly useful for Phan internals + * (e.g. a property with an unresolved future union type can't have a template type) + */ + public function hasUnresolvedFutureUnionType(): bool + { + return $this->future_union_type !== null; + } + + /** + * @return UnionType|null + * Get the UnionType from a future union type defined + * on this object or null if there is no future + * union type. + */ + public function getFutureUnionType(): ?UnionType + { + $future_union_type = $this->future_union_type; + if ($future_union_type === null) { + return null; + } + + // null out the future_union_type before + // we compute it to avoid unbounded + // recursion + $this->future_union_type = null; + + try { + return $future_union_type->get(); + } catch (IssueException $_) { + return null; + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Flags.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Flags.php new file mode 100644 index 000000000..2520a2052 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Flags.php @@ -0,0 +1,120 @@ +getPhanFlags(), ElementPhanFlags()) + */ +class Flags +{ + public const IS_DEPRECATED = (1 << 1); + public const IS_PHP_INTERNAL = (1 << 2); + + public const IS_PARENT_CONSTRUCTOR_CALLED = (1 << 3); + + public const IS_RETURN_TYPE_UNDEFINED = (1 << 4); + public const HAS_RETURN = (1 << 5); + public const IS_OVERRIDE = (1 << 6); + public const HAS_YIELD = (1 << 7); + + public const CLASS_HAS_DYNAMIC_PROPERTIES = (1 << 8); + public const IS_CLONE_OF_VARIADIC = (1 << 9); + public const CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES = (1 << 10); + public const CLASS_FORBID_UNDECLARED_MAGIC_METHODS = (1 << 11); + + public const IS_NS_INTERNAL = (1 << 12); + public const IS_FROM_PHPDOC = (1 << 13); + + // These can be combined in 3 ways, see Parameter->getReferenceType() + public const IS_READ_REFERENCE = (1 << 14); + public const IS_WRITE_REFERENCE = (1 << 15); + public const IS_IGNORED_REFERENCE = (1 << 16); // only applies to parameters, does not conflict with other types + // End of reference types + + // This will be compared against IS_OVERRIDE + public const IS_OVERRIDE_INTENDED = (1 << 16); + + public const IS_PARAM_USING_NULLABLE_SYNTAX = (1 << 17); + + // For dead code detection + public const WAS_PROPERTY_READ = (1 << 18); + public const WAS_PROPERTY_WRITTEN = (1 << 19); + + public const IS_DYNAMIC_PROPERTY = (1 << 20); + // Is this a dynamic global constant? + public const IS_DYNAMIC_CONSTANT = (1 << 20); + public const IS_CONSTRUCTOR_USED_FOR_SIDE_EFFECTS = (1 << 20); + // A property can be read-only, write-only, or neither, but not both. + // This is independent of being a magic property. + // IS_READ_ONLY can also be set on classes as @phan-immutable + public const IS_READ_ONLY = (1 << 21); + public const IS_WRITE_ONLY = (1 << 22); + public const HAS_STATIC_UNION_TYPE = (1 << 23); + public const HAS_TEMPLATE_TYPE = (1 << 24); + + public const IS_OVERRIDDEN_BY_ANOTHER = (1 << 25); + // Currently applies only to some variables (e.g. static variables) + public const IS_CONSTANT_DEFINITION = (1 << 26); + // Also used for `@phan-hardcode-return-type` + public const HARDCODED_RETURN_TYPE = (1 << 26); + + // Flag to be set on fake __construct methods (e.g. for classes/interfaces without having them defined explicitly) + // Currently for strict visibility checking, because fake constructors have public visibility by default, and Phan + // fails thinking that child classes are violating the visibility if they have a private or protected __construct + public const IS_FAKE_CONSTRUCTOR = (1 << 27); + public const IS_EXTERNAL_MUTATION_FREE = (1 << 28); + public const IS_SIDE_EFFECT_FREE = self::IS_READ_ONLY | self::IS_EXTERNAL_MUTATION_FREE; + // @abstract tag on class constants or other elements + public const IS_PHPDOC_ABSTRACT = (1 << 29); + + /** + * Either enable or disable the given flag on + * the given bit vector. + * + * @param int $bit_vector + * The bit vector we're operating on + * + * @param int $flag + * The flag we're setting on the bit vector such + * as Flags::IS_DEPRECATED. + * + * @param bool $value + * True to or the flag in, false to & the bit vector + * with the flags negation + * + * @return int + * A new bit vector with the given flag set or unset + */ + public static function bitVectorWithState( + int $bit_vector, + int $flag, + bool $value + ): int { + return $value + ? ($bit_vector | $flag) + : ($bit_vector & (~$flag)); + } + + /** + * @param int $bit_vector + * The bit vector we'd like to get the state for + * + * @param int $flag + * The flag we'd like to get the state for + * + * @return bool + * True if all bits in the flag are enabled in the bit + * vector, else false. + */ + public static function bitVectorHasState( + int $bit_vector, + int $flag + ): bool { + return (($bit_vector & $flag) === $flag); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Func.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Func.php new file mode 100644 index 000000000..71e8dc552 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Func.php @@ -0,0 +1,518 @@ + $parameter_list + * A list of parameters to set on this method + */ + public function __construct( + Context $context, + string $name, + UnionType $type, + int $flags, + FullyQualifiedFunctionName $fqsen, + $parameter_list + ) { + if ($fqsen->isClosure()) { + $internal_scope = new ClosureScope( + $context->getScope(), + $fqsen + ); + } else { + $internal_scope = new FunctionLikeScope( + $context->getScope(), + $fqsen + ); + } + $context = $context->withScope($internal_scope); + parent::__construct( + $context, + $name, + $type, + $flags, + $fqsen + ); + + // TODO: Is internal scope even necessary to track separately?? + $this->setInternalScope($internal_scope); + + if ($parameter_list !== null) { + $this->setParameterList($parameter_list); + } + } + + /** + * If a Closure overrides the scope(class) it will be executed in (via doc comment) + * then return a context with the new scope instead. + * + * @param CodeBase $code_base + * @param Context $context - The outer context in which the closure was declared. + * Either this (or a new context for the other class) will be returned. + * @return ?FullyQualifiedClassName + * + * Postcondition: if return value !== null, then $Type is the type of a class which exists in the codebase. + */ + private static function getClosureOverrideFQSEN( + CodeBase $code_base, + Context $context, + Type $closure_scope_type, + Node $node + ): ?FullyQualifiedClassName { + if ($node->kind !== ast\AST_CLOSURE) { + return null; + } + if ($closure_scope_type->isNativeType()) { + // TODO: Handle final internal classes (Can't call bindTo on those) + // TODO: What about 'null' (for code planning to bindTo(null)) + // Emit an error + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeInvalidClosureScope, + $node->lineno ?? 0, + (string)$closure_scope_type + ); + return null; + } else { + // TODO: handle 'parent'? + // TODO: Check if isInClassScope + if ($closure_scope_type->isSelfType() || $closure_scope_type->isStaticType()) { + // nothing to do. + return null; + } + } + + return FullyQualifiedClassName::fromType($closure_scope_type); + } + + + /** + * @param Context $context + * The context in which the node appears + * + * @param CodeBase $code_base + * + * @param Node $node + * An AST node representing a function + * + * @param FullyQualifiedFunctionName $fqsen + * A fully qualified name for the function + * + * @return Func + * A Func representing the AST node in the + * given context + */ + public static function fromNode( + Context $context, + CodeBase $code_base, + Node $node, + FullyQualifiedFunctionName $fqsen + ): Func { + // Create the skeleton function object from what + // we know so far + $func = new Func( + $context, + (string)$node->children['name'], + UnionType::empty(), + $node->flags, + $fqsen, + null + ); + $doc_comment = $node->children['docComment'] ?? ''; + $func->setDocComment($doc_comment); + + // Parse the comment above the function to get + // extra meta information about the function. + $comment = Comment::fromStringInContext( + $doc_comment, + $code_base, + $context, + $node->lineno ?? 0, + Comment::ON_FUNCTION + ); + + // Defer adding params to the local scope for user functions. (FunctionTrait::addParamsToScopeOfFunctionOrMethod) + // See PreOrderAnalysisVisitor->visitFuncDecl and visitClosure + $func->setComment($comment); + + $element_context = new ElementContext($func); + + // @var list + // The list of parameters specified on the + // method + $parameter_list = Parameter::listFromNode( + $element_context, + $code_base, + $node->children['params'] + ); + $func->setParameterList($parameter_list); + + // Redefine the function's internal scope to point to the new class before adding any variables to the scope. + + $closure_scope_option = $comment->getClosureScopeOption(); + if ($closure_scope_option->isDefined()) { + $override_class_fqsen = self::getClosureOverrideFQSEN($code_base, $context, $closure_scope_option->get(), $node); + if ($override_class_fqsen !== null) { + // TODO: Allow Null? + $scope = $func->getInternalScope(); + if (!($scope instanceof ClosureScope)) { + throw new AssertionError('Expected scope of a closure to be a ClosureScope'); + } + $scope->overrideClassFQSEN($override_class_fqsen); + $func->getContext()->setScope($scope); + } + } + + // Add each parameter to the scope of the function + // NOTE: it's important to clone this, + // because we don't want anything to modify the original Parameter + foreach ($parameter_list as $parameter) { + $func->getInternalScope()->addVariable( + $parameter->cloneAsNonVariadic() + ); + } + + if (!$func->isPHPInternal()) { + // If the function is Analyzable, set the node so that + // we can come back to it whenever we like and + // rescan it + $func->setNode($node); + } + foreach ($comment->getTemplateTypeList() as $template_type) { + $func->getInternalScope()->addTemplateType($template_type); + } + + // Keep an copy of the original parameter list, to check for fatal errors later on. + $func->setRealParameterList($parameter_list); + + $required_parameter_count = self::computeNumberOfRequiredParametersForList($parameter_list); + $func->setNumberOfRequiredParameters($required_parameter_count); + + $func->setNumberOfOptionalParameters(\count($parameter_list) - $required_parameter_count); + + // Check to see if the comment specifies that the + // function is deprecated + $func->setIsDeprecated($comment->isDeprecated()); + + // Set whether or not the function is internal to + // the namespace. + $func->setIsNSInternal($comment->isNSInternal()); + + // Set whether this function is pure. + if ($comment->isPure()) { + $func->setIsPure(); + } + + $func->setSuppressIssueSet( + $comment->getSuppressIssueSet() + ); + + // Take a look at function return types + if ($node->children['returnType'] !== null) { + // Get the type of the parameter + $union_type = UnionTypeVisitor::unionTypeFromNode( + $code_base, + $context, + $node->children['returnType'] + ); + $func->setRealReturnType($union_type); + + $func->setUnionType($func->getUnionType()->withUnionType($union_type)->withRealTypeSet($union_type->getTypeSet())); + } + + if ($comment->hasReturnUnionType()) { + // See if we have a return type specified in the comment + $union_type = $comment->getReturnType(); + + // FIXME properly handle self/static in closures declared within methods. + if ($union_type->hasSelfType()) { + $union_type = $union_type->makeFromFilter(static function (Type $type): bool { + return !$type->isSelfType(); + }); + if ($context->isInClassScope()) { + $union_type = $union_type->withType( + $context->getClassFQSEN()->asType() + ); + } else { + Issue::maybeEmit( + $code_base, + $context, + Issue::ContextNotObjectUsingSelf, + $comment->getReturnLineno(), + 'self', + $fqsen + ); + } + } + + $new_type = $func->getUnionType()->withUnionType($union_type)->withRealTypeSet($func->getRealReturnType()->getTypeSet()); + if ($union_type->hasRealTypeSet() && !$new_type->hasRealTypeSet()) { + $new_type = $new_type->withRealTypeSet($union_type->getRealTypeSet()); + } + $func->setUnionType($new_type); + $func->setPHPDocReturnType($union_type); + } + $element_context->freeElementReference(); + + $func->setOriginalReturnType(); + + return $func; + } + + public function getFQSEN(): FullyQualifiedFunctionName + { + return $this->fqsen; + } + + /** + * @return \Generator + * @phan-return \Generator + * The set of all alternates to this function + * @suppress PhanParamSignatureMismatch + */ + public function alternateGenerator(CodeBase $code_base): \Generator + { + $alternate_id = 0; + $fqsen = $this->getFQSEN(); + + while ($code_base->hasFunctionWithFQSEN($fqsen)) { + yield $code_base->getFunctionByFQSEN($fqsen); + $fqsen = $fqsen->withAlternateId(++$alternate_id); + } + } + + /** + * @return string + * A string representation of this function signature + */ + public function __toString(): string + { + $string = ''; + + $string .= 'function ' . $this->name; + + $string .= '(' . \implode(', ', $this->getParameterList()) . ')'; + + if (!$this->getUnionType()->isEmpty()) { + $string .= ' : ' . (string)$this->getUnionType(); + } + + $string .= ';'; + + return $string; + } + + /** + * @return bool + * True if this function returns a reference + */ + public function returnsRef(): bool + { + return $this->getFlagsHasState(flags\FUNC_RETURNS_REF); + } + + /** + * @return bool + * True if this is a static closure or arrow func, such as `static fn() => $x` + */ + public function isStatic(): bool + { + return $this->getFlagsHasState(flags\MODIFIER_STATIC); + } + + /** + * @return bool Always false for global functions. + */ + public function isFromPHPDoc(): bool + { + return false; + } + + /** + * True if this is a closure + */ + public function isClosure(): bool + { + return $this->getFQSEN()->isClosure(); + } + + /** + * Returns a string that can be used as a standalone PHP stub for this global function. + * @suppress PhanUnreferencedPublicMethod (toStubInfo is used by callers for more flexibility) + */ + public function toStub(): string + { + [$namespace, $string] = $this->toStubInfo(); + $namespace_text = $namespace === '' ? '' : "$namespace "; + $string = \sprintf("namespace %s{\n%s}\n", $namespace_text, $string); + return $string; + } + + public function getMarkupDescription(): string + { + $fqsen = $this->getFQSEN(); + $namespace = \ltrim($fqsen->getNamespace(), '\\'); + $stub = ''; + if (StringUtil::isNonZeroLengthString($namespace)) { + $stub = "namespace $namespace;\n"; + } + $stub .= 'function '; + if ($this->returnsRef()) { + $stub .= '&'; + } + $stub .= $fqsen->getName(); + + $stub .= '(' . $this->getParameterStubText() . ')'; + + $return_type = $this->getUnionType(); + if (!$return_type->isEmpty()) { + $stub .= ': ' . (string)$return_type; + } + return $stub; + } + + /** + * Returns stub info for `tool/make_stubs` + * @return array{0:string,1:string} [string $namespace, string $text] + */ + public function toStubInfo(): array + { + $fqsen = $this->getFQSEN(); + $stub = 'function '; + if ($this->returnsRef()) { + $stub .= '&'; + } + $stub .= $fqsen->getName(); + + $stub .= '(' . $this->getRealParameterStubText() . ')'; + + $return_type = $this->real_return_type; + if ($return_type && !$return_type->isEmpty()) { + $stub .= ' : ' . (string)$return_type; + } + $stub .= " {}\n"; + $namespace = \ltrim($fqsen->getNamespace(), '\\'); + return [$namespace, $stub]; + } + + public function getUnionTypeWithUnmodifiedStatic(): UnionType + { + return $this->getUnionType(); + } + + /** + * @return string + * The fully-qualified structural element name of this + * structural element (or something else for closures and callables) + * @override + */ + public function getRepresentationForIssue(bool $show_args = false): string + { + if ($this->isClosure()) { + return $this->getStubForClosure(); + } + return $this->getRepresentationForIssueInternal($show_args); + } + + private function getStubForClosure(): string + { + $stub = 'Closure'; + if ($this->returnsRef()) { + $stub .= '&'; + } + $stub .= '(' . \implode(', ', \array_map(static function (Parameter $parameter): string { + return $parameter->toStubString(); + }, $this->getRealParameterList())) . ')'; + if ($this->real_return_type && !$this->getRealReturnType()->isEmpty()) { + $stub .= ' : ' . (string)$this->getRealReturnType(); + } + return $stub; + } + + /** + * @return string + * The name of this structural element (without namespace/class), + * or a string for FunctionLikeDeclarationType (or a closure) which lacks a real FQSEN + */ + public function getNameForIssue(): string + { + if ($this->isClosure()) { + return $this->getStubForClosure(); + } + return $this->name . '()'; + } + + /** + * @override + */ + public function addReference(FileRef $file_ref): void + { + if (Config::get_track_references()) { + // Currently, we don't need to track references to PHP-internal methods/functions/constants + // such as PHP_VERSION, strlen(), Closure::bind(), etc. + // This may change in the future. + if ($this->isPHPInternal()) { + return; + } + if ($file_ref instanceof Context && $file_ref->isInFunctionLikeScope() && $file_ref->getFunctionLikeFQSEN() === $this->fqsen) { + // Don't track functions calling themselves + return; + } + $this->reference_list[$file_ref->__toString()] = $file_ref; + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/FunctionFactory.php b/bundled-libs/phan/phan/src/Phan/Language/Element/FunctionFactory.php new file mode 100644 index 000000000..cfeeb5f90 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/FunctionFactory.php @@ -0,0 +1,318 @@ + + * One or more (alternate) functions begotten from + * reflection info and internal functions data + */ + public static function functionListFromReflectionFunction( + FullyQualifiedFunctionName $fqsen, + \ReflectionFunction $reflection_function + ): array { + + $context = new Context(); + + $namespaced_name = $fqsen->getNamespacedName(); + + $function = new Func( + $context, + $namespaced_name, + UnionType::empty(), + 0, + $fqsen, + null + ); + + $function->setNumberOfRequiredParameters( + $reflection_function->getNumberOfRequiredParameters() + ); + + $function->setNumberOfOptionalParameters( + $reflection_function->getNumberOfParameters() + - $reflection_function->getNumberOfRequiredParameters() + ); + $function->setIsDeprecated($reflection_function->isDeprecated()); + $real_return_type = UnionType::fromReflectionType($reflection_function->getReturnType()); + if (Config::getValue('assume_real_types_for_internal_functions')) { + // @phan-suppress-next-line PhanAccessMethodInternal + $real_type_string = UnionType::getLatestRealFunctionSignatureMap(Config::get_closest_target_php_version_id())[$namespaced_name] ?? null; + if (\is_string($real_type_string)) { + // Override it with Phan's information, useful for list overriding array + // TODO: Validate that Phan's signatures are compatible (e.g. nullability) + $real_return_type = UnionType::fromStringInContext($real_type_string, new Context(), Type::FROM_TYPE); + } + } + $function->setRealReturnType($real_return_type); + $function->setRealParameterList(Parameter::listFromReflectionParameterList($reflection_function->getParameters())); + + return self::functionListFromFunction($function); + } + + /** + * @param string[] $signature + * @return list + * One or more (alternate) methods begotten from + * reflection info and internal method data + */ + public static function functionListFromSignature( + FullyQualifiedFunctionName $fqsen, + array $signature + ): array { + + // TODO: Look into adding helper method in UnionType caching this to speed up loading. + $context = new Context(); + + $return_type = UnionType::fromStringInContext( + $signature[0], + $context, + Type::FROM_TYPE + ); + unset($signature[0]); + + $func = new Func( + $context, + $fqsen->getNamespacedName(), + $return_type, + 0, + $fqsen, + [] // will be filled in by functionListFromFunction + ); + + return self::functionListFromFunction($func); + } + + /** + * @return list a list of 1 or more method signatures from a ReflectionMethod + * and Phan's alternate signatures for that method's FQSEN in FunctionSignatureMap. + */ + public static function methodListFromReflectionClassAndMethod( + Context $context, + \ReflectionClass $class, + \ReflectionMethod $reflection_method + ): array { + $class_name = $class->getName(); + $method_fqsen = FullyQualifiedMethodName::make( + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + FullyQualifiedClassName::fromFullyQualifiedString($class_name), + $reflection_method->getName() + ); + + // Deliberately don't use getModifiers - flags we don't know about might cause unexpected effects, + // and there's no guarantee MODIFIER_PUBLIC would continue to equal ReflectionMethod::IS_PUBLIC + if ($reflection_method->isPublic()) { + $modifiers = ast\flags\MODIFIER_PUBLIC; + } elseif ($reflection_method->isProtected()) { + $modifiers = ast\flags\MODIFIER_PROTECTED; + } else { + $modifiers = ast\flags\MODIFIER_PRIVATE; + } + if ($reflection_method->isStatic()) { + $modifiers |= ast\flags\MODIFIER_STATIC; + } + if ($reflection_method->isFinal()) { + $modifiers |= ast\flags\MODIFIER_FINAL; + } + if ($reflection_method->isAbstract()) { + $modifiers |= ast\flags\MODIFIER_ABSTRACT; + } + + $method = new Method( + $context, + $reflection_method->name, + UnionType::empty(), + $modifiers, + $method_fqsen, + null + ); + // Knowing the defining class of the method is useful for warning about unused calls to inherited methods such as Exception->getCode() + $defining_class_name = $reflection_method->class; + if ($defining_class_name !== $class_name) { + $method->setDefiningFQSEN( + FullyQualifiedMethodName::make( + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + FullyQualifiedClassName::fromFullyQualifiedString($defining_class_name), + $reflection_method->getName() + ) + ); + } + + $method->setNumberOfRequiredParameters( + $reflection_method->getNumberOfRequiredParameters() + ); + + $method->setNumberOfOptionalParameters( + $reflection_method->getNumberOfParameters() + - $reflection_method->getNumberOfRequiredParameters() + ); + + if ($method->isMagicCall() || $method->isMagicCallStatic()) { + $method->setNumberOfOptionalParameters(FunctionInterface::INFINITE_PARAMETERS); + $method->setNumberOfRequiredParameters(0); + } + $method->setIsDeprecated($reflection_method->isDeprecated()); + // https://github.com/phan/phan/issues/888 - Reflection for that class's parameters causes php to throw/hang + if ($class_name !== 'ServerResponse') { + $method->setRealReturnType(UnionType::fromReflectionType($reflection_method->getReturnType())); + $method->setRealParameterList(Parameter::listFromReflectionParameterList($reflection_method->getParameters())); + } + + return self::functionListFromFunction($method); + } + + /** + * @param FunctionInterface $function + * Get a list of methods hydrated with type information + * for the given partial method + * + * @return list + * A list of typed functions/methods based on the given method + */ + public static function functionListFromFunction( + FunctionInterface $function + ): array { + // See if we have any type information for this + // internal function + $map_list = UnionType::internalFunctionSignatureMapForFQSEN( + $function->getFQSEN() + ); + + if (!$map_list) { + if (!$function->getParameterList()) { + $function->setParameterList($function->getRealParameterList()); + } + $function->inheritRealParameterDefaults(); + return [$function]; + } + + $alternate_id = 0; + /** + * @param array $map + * @suppress PhanPossiblyFalseTypeArgumentInternal, PhanPossiblyFalseTypeArgument + */ + return \array_map(static function (array $map) use ( + $function, + &$alternate_id + ): FunctionInterface { + $alternate_function = clone($function); + + $alternate_function->setFQSEN( + $alternate_function->getFQSEN()->withAlternateId( + $alternate_id++ + ) + ); + + // Set the return type if one is defined + $return_type = $map['return_type'] ?? null; + if ($return_type instanceof UnionType) { + $real_return_type = $function->getRealReturnType(); + if (!$real_return_type->isEmpty()) { + $return_type = UnionType::of($return_type->getTypeSet(), $real_return_type->getTypeSet()); + } + $alternate_function->setUnionType($return_type); + } + $alternate_function->clearParameterList(); + + // Load parameter types if defined + foreach ($map['parameter_name_type_map'] ?? [] as $parameter_name => $parameter_type) { + $flags = 0; + $phan_flags = 0; + $is_optional = false; + + // Check to see if it's a pass-by-reference parameter + if (($parameter_name[0] ?? '') === '&') { + $flags |= \ast\flags\PARAM_REF; + $parameter_name = \substr($parameter_name, 1); + if (\strncmp($parameter_name, 'rw_', 3) === 0) { + $phan_flags |= Flags::IS_READ_REFERENCE | Flags::IS_WRITE_REFERENCE; + $parameter_name = \substr($parameter_name, 3); + } elseif (\strncmp($parameter_name, 'w_', 2) === 0) { + $phan_flags |= Flags::IS_WRITE_REFERENCE; + $parameter_name = \substr($parameter_name, 2); + } elseif (\strncmp($parameter_name, 'r_', 2) === 0) { + $phan_flags |= Flags::IS_READ_REFERENCE; + $parameter_name = \substr($parameter_name, 2); + } + } + + // Check to see if it's variadic + if (\strpos($parameter_name, '...') !== false) { + $flags |= \ast\flags\PARAM_VARIADIC; + $parameter_name = \str_replace('...', '', $parameter_name); + } + + // Check to see if it's an optional parameter + if (\strpos($parameter_name, '=') !== false) { + $is_optional = true; + $parameter_name = \str_replace('=', '', $parameter_name); + } + + $parameter = Parameter::create( + $function->getContext(), + $parameter_name, + $parameter_type, + $flags + ); + $parameter->enablePhanFlagBits($phan_flags); + if ($is_optional) { + if (!$parameter->hasDefaultValue()) { + // Placeholder value. PHP 8.0+ is better at actually providing real parameter defaults. + $parameter->setDefaultValueType(NullType::instance(false)->asPHPDocUnionType()); + } + } + + // Add the parameter + $alternate_function->appendParameter($parameter); + } + + // TODO: Store the "real" number of required parameters, + // if this is out of sync with the extension's ReflectionMethod->getParameterList()? + // (e.g. third party extensions may add more required parameters?) + $alternate_function->setNumberOfRequiredParameters( + \array_reduce( + $alternate_function->getParameterList(), + static function (int $carry, Parameter $parameter): int { + return ($carry + ( + $parameter->isOptional() ? 0 : 1 + )); + }, + 0 + ) + ); + + $alternate_function->setNumberOfOptionalParameters( + \count($alternate_function->getParameterList()) - + $alternate_function->getNumberOfRequiredParameters() + ); + + if ($alternate_function instanceof Method) { + if ($alternate_function->isMagicCall() || $alternate_function->isMagicCallStatic()) { + $alternate_function->setNumberOfOptionalParameters(999); + $alternate_function->setNumberOfRequiredParameters(0); + } + } + $alternate_function->inheritRealParameterDefaults(); + + return $alternate_function; + }, $map_list); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/FunctionInterface.php b/bundled-libs/phan/phan/src/Phan/Language/Element/FunctionInterface.php new file mode 100644 index 000000000..cf331951a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/FunctionInterface.php @@ -0,0 +1,482 @@ + + * A list of parameters on the method + */ + public function getParameterList(); + + /** + * @param list $parameter_list + * A list of parameters to set on this method + * (When quick_mode is false, this is also called to temporarily + * override parameter types, etc.) + * @internal + */ + public function setParameterList(array $parameter_list): void; + + /** + * @internal - moves real parameter defaults to the inferred phpdoc parameters + */ + public function inheritRealParameterDefaults(): void; + + /** + * Gets the $ith parameter for the **caller**. + * In the case of variadic arguments, an infinite number of parameters exist. + * (The callee would see variadic arguments(T ...$args) as a single variable of type T[], + * while the caller sees a place expecting an expression of type T. + * + * @param int $i - offset of the parameter. + * @return Parameter|null The parameter type that the **caller** observes. + */ + public function getParameterForCaller(int $i): ?Parameter; + + /** + * Gets the $ith real parameter for the **caller**. + * In the case of variadic arguments, an infinite number of parameters exist. + * (The callee would see variadic arguments(T ...$args) as a single variable of type T[], + * while the caller sees a place expecting an expression of type T. + * + * @param int $i - offset of the parameter. + * @return Parameter|null The parameter type that the **caller** observes. + */ + public function getRealParameterForCaller(int $i): ?Parameter; + + /** + * @param Parameter $parameter + * A parameter to append to the parameter list + */ + public function appendParameter(Parameter $parameter): void; + + /** + * @return void + * + * Call this before calling appendParameter, if parameters were already added. + */ + public function clearParameterList(): void; + + /** + * Records the fact that $parameter_name is an output-only reference. + * @param string $parameter_name + */ + public function recordOutputReferenceParamName(string $parameter_name): void; + + /** + * @return list list of output references (annotated with (at)phan-output-reference. Usually empty. + */ + public function getOutputReferenceParamNames(): array; + + /** + * @return \Generator + * The set of all alternates to this function + */ + public function alternateGenerator(CodeBase $code_base): \Generator; + + /** + * @param CodeBase $code_base + * The code base in which this element exists. + * + * @return bool + * True if this is marked as an element internal to + * its namespace + */ + public function isNSInternal(CodeBase $code_base): bool; + + /** + * @param CodeBase $code_base + * The code base in which this element exists. + * + * @return bool + * True if this element is internal to the namespace + */ + public function isNSInternalAccessFromContext( + CodeBase $code_base, + Context $context + ): bool; + + /** + * @return Context + * Analyze the node associated with this object + * in the given context + */ + public function analyze(Context $context, CodeBase $code_base): Context; + + /** + * @return Context + * Analyze the node associated with this object + * in the given context. + * This function's parameter list may or may not have been modified. + * @param list $parameter_list + */ + public function analyzeWithNewParams(Context $context, CodeBase $code_base, array $parameter_list): Context; + + /** + * @return string the namespace in which this function interface was declared. + * + * Used for checking (at)internal annotations, etc. + */ + public function getElementNamespace(): string; + + /** + * @return UnionType + * The type of this method in its given context. + */ + public function getRealReturnType(): UnionType; + + /** + * @return list + * A list of parameters on the method, with types from the method signature. + */ + public function getRealParameterList(); + + /** + * @param array $parameter_map maps a subset of param names to the unmodified phpdoc parameter types. + * Will differ from real parameter types (ideally narrower) + */ + public function setPHPDocParameterTypeMap(array $parameter_map): void; + + /** + * @return array maps a subset of param names to the unmodified phpdoc parameter types. + */ + public function getPHPDocParameterTypeMap(): array; + + /** + * @param ?UnionType $union_type the raw phpdoc union type + */ + public function setPHPDocReturnType(?UnionType $union_type): void; + + /** + * @return ?UnionType the raw phpdoc union type + */ + public function getPHPDocReturnType(): ?UnionType; + + /** + * @return bool + * True if this function or method returns a reference + */ + public function returnsRef(): bool; + + /** + * Returns true if the return type depends on the argument, and a plugin makes Phan aware of that. + */ + public function hasDependentReturnType(): bool; + + /** + * Returns a union type based on $args_node and $context + * @param CodeBase $code_base + * @param Context $context + * @param list $args + */ + public function getDependentReturnType(CodeBase $code_base, Context $context, array $args): UnionType; + + /** + * Make calculation of the return type of this function/method use $closure + */ + public function setDependentReturnTypeClosure(Closure $closure): void; + + /** + * Returns true if this function or method has additional analysis logic for invocations (From internal and user defined plugins) + * @see getDependentReturnType + */ + public function hasFunctionCallAnalyzer(): bool; + + /** + * Perform additional analysis logic for invocations (From internal and user defined plugins) + * + * @param CodeBase $code_base + * @param Context $context + * @param list $args + * @param ?Node $node - the node causing the call. This may be dynamic, e.g. call_user_func_array. This will be required in Phan 3. + */ + public function analyzeFunctionCall(CodeBase $code_base, Context $context, array $args, Node $node = null): void; + + /** + * Make additional analysis logic of this function/method use $closure + * If callers need to invoke multiple closures, they should pass in a closure to invoke multiple closures or use addFunctionCallAnalyzer. + */ + public function setFunctionCallAnalyzer(Closure $closure): void; + + /** + * If callers need to invoke multiple closures, they should pass in a closure to invoke multiple closures. + */ + public function addFunctionCallAnalyzer(Closure $closure): void; + + /** + * Initialize the inner scope of this method with variables created from the parameters. + * + * Deferred until the parse phase because getting the UnionType of parameter defaults requires having all class constants be known. + */ + public function ensureScopeInitialized(CodeBase $code_base): void; + + /** Get the node with the declaration of this function/method, if it exists and if Phan is running in non-quick mode */ + public function getNode(): ?Node; + + /** + * @return ?Comment - Not set for internal functions/methods + */ + public function getComment(): ?Comment; + + /** + * @return string - a suffix for an issue message indicating the cause for deprecation. This string is empty if the cause is unknown. + */ + public function getDeprecationReason(): string; + + /** + * Set the comment data for this function/method + */ + public function setComment(Comment $comment): void; + + /** + * Mark this function or method as pure (having no visible side effects) + */ + public function setIsPure(): void; + + /** + * Check if this function or method is marked as pure (having no visible side effects) + */ + public function isPure(): bool; + + /** + * @return UnionType of 0 or more types from (at)throws annotations on this function-like + */ + public function getThrowsUnionType(): UnionType; + + /** + * @return bool + * True if this is a magic phpdoc method (declared via (at)method on class declaration phpdoc) + * Always false for global functions(Func). + */ + public function isFromPHPDoc(): bool; + + /** + * Clone the parameter list, so that modifying the parameters on the first call won't modify the others. + * TODO: If parameters were changed to be immutable, they can be shared without cloning with less worry. + * @internal + */ + public function cloneParameterList(): void; + + /** + * @return bool - Does any parameter type possibly require recursive analysis if more specific types are provided? + * + * If this returns true, there is at least one parameter and at least one of those can be overridden with a more specific type. + */ + public function needsRecursiveAnalysis(): bool; + + /** + * Returns a FunctionLikeDeclarationType based on phpdoc+real types. + * The return value is used for type casting rule checking. + */ + public function asFunctionLikeDeclarationType(): FunctionLikeDeclarationType; + + /** + * @return array in the same format as FunctionSignatureMap.php + */ + public function toFunctionSignatureArray(): array; + + /** + * Precondition: This function is a generator type + * Converts Generator|T[] to Generator + * Converts Generator|array to Generator, etc. + */ + public function getReturnTypeAsGeneratorTemplateType(): Type; + + /** + * Returns this function's union type without resolving `static` in the function declaration's context. + */ + public function getUnionTypeWithUnmodifiedStatic(): UnionType; + + /** + * Check this method's return types (phpdoc and real) to make sure they're valid, + * and infer a return type from the combination of the signature and phpdoc return types. + */ + public function analyzeReturnTypes(CodeBase $code_base): void; + + /** + * Does this function/method declare an (at)template type for this type? + */ + public function declaresTemplateTypeInComment(TemplateType $template_type): bool; + + /** + * Create any plugins that exist due to doc comment annotations. + * Must be called after adding this FunctionInterface to the $code_base, so that issues can be emitted if needed. + * @return ?Closure(CodeBase, Context, FunctionInterface, list):UnionType + * @internal + */ + public function getCommentParamAssertionClosure(CodeBase $code_base): ?Closure; + + /** + * @return array very conservatively maps variable names to union types they can have. + * Entries are omitted if there are possible assignments that aren't known. + * + * This is useful as a fallback for determining missing types when analyzing the first iterations of loops. + * + * Other approaches, such as analyzing loops multiple times, are possible, but not implemented. + */ + public function getVariableTypeFallbackMap(CodeBase $code_base): array; + + /** + * Gets the original union type of this function/method. + * + * This is populated the first time it is called. + */ + public function getOriginalReturnType(): UnionType; + + /** + * Record the existence of a parameter with an `(at)phan-mandatory-param` comment at $offset + */ + public function recordHasMandatoryPHPDocParamAtOffset(int $parameter_offset): void; +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/FunctionTrait.php b/bundled-libs/phan/phan/src/Phan/Language/Element/FunctionTrait.php new file mode 100644 index 000000000..7bb37ec06 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/FunctionTrait.php @@ -0,0 +1,1965 @@ +getPhanFlags() */ + abstract public function getPhanFlagsHasState(int $bits): bool; + + abstract public function setPhanFlags(int $phan_flags): void; + + /** + * @return string + * The (not fully-qualified) name of this element. + */ + abstract public function getName(): string; + + /** + * @return FQSEN + * The fully-qualified structural element name of this + * structural element + */ + abstract public function getFQSEN(): FQSEN; + + /** + * @return string + * A representation of this function, closure, or method, + * for issue messages. + */ + public function getRepresentationForIssue(bool $show_args = false): string + { + $args_repr = ''; + if ($show_args) { + $parameter_list = $this->parameter_list; + if ($parameter_list) { + $is_internal = $this->isPHPInternal(); + $args_repr = \implode(', ', \array_map(static function (Parameter $parameter) use ($is_internal): string { + return $parameter->getShortRepresentationForIssue($is_internal); + }, $parameter_list)); + } + } + return $this->getFQSEN()->__toString() . '(' . $args_repr . ')'; + } + + /** + * @return string + * The name of this structural element (without namespace/class), + * or a string for FunctionLikeDeclarationType which lacks a real FQSEN + */ + public function getNameForIssue(): string + { + return $this->getName() . '()'; + } + + /** + * @var int + * The number of required parameters for the method + */ + private $number_of_required_parameters = 0; + + /** + * @var int + * The number of optional parameters for the method. + * Note that this is set to a large number in methods using varargs or func_get_arg*() + */ + private $number_of_optional_parameters = 0; + + /** + * @var int + * The number of required (real) parameters for the method declaration. + * For internal methods, ignores Phan's annotations. + */ + private $number_of_required_real_parameters = 0; + + /** + * @var int + * The number of optional (real) parameters for the method declaration. + * For internal methods, ignores Phan's annotations. + * For user-defined methods, ignores presence of func_get_arg*() + */ + private $number_of_optional_real_parameters = 0; + + /** + * @var bool|null + * Does any parameter type possibly require recursive analysis if more specific types are provided? + * Caches the return value for $this->needsRecursiveAnalysis() + */ + private $needs_recursive_analysis = null; + + /** + * @var list + * The list of parameters for this method + * This will change while the method is being analyzed when the config quick_mode is false. + */ + private $parameter_list = []; + + /** + * @var ?int + * The hash of the types for the list of parameters for this function/method. + */ + private $parameter_list_hash = null; + + /** + * @var ?bool + * Whether or not this function/method has any pass by reference parameters. + */ + private $has_pass_by_reference_parameters = null; + + /** + * @var array + * @phan-var associative-array + * If the types for a parameter list were checked, + * this contains the recursion depth for a given integer hash (smaller is earlier in recursion) + */ + private $checked_parameter_list_hashes = []; + + /** + * @var list + * The list of *real* (not from phpdoc) parameters for this method. + * This does not change after initialization. + */ + private $real_parameter_list = []; + + /** + * @var array + * The list of unmodified *phpdoc* parameter types for this method. + * This does not change after initialization. + */ + private $phpdoc_parameter_type_map = []; + + /** + * @var list + * A list of parameter names that are output-only references + */ + private $phpdoc_output_references = []; + + /** + * @var ?UnionType + * The unmodified *phpdoc* union type for this method. + * Will be null without any (at)return statements. + */ + private $phpdoc_return_type; + + /** + * @var UnionType + * The *real* (not from phpdoc) return type from this method. + * This does not change after initialization. + */ + private $real_return_type; + + /** + * @var ?Closure(CodeBase, Context, FunctionInterface, list):UnionType + */ + private $return_type_callback = null; + + /** + * @var ?Closure(CodeBase, Context, FunctionInterface, list, ?Node):void + */ + private $function_call_analyzer_callback = null; + + /** + * @var array + */ + private $function_call_analyzer_callback_set = []; + + /** + * @var FunctionLikeDeclarationType|null (Lazily generated representation of this as a closure type) + */ + private $as_closure_declaration_type; + + /** + * @var Type|null (Lazily generated representation of this as a generator type) + */ + private $as_generator_template_type; + + /** + * @var ?UnionType + */ + private $original_return_type; + + /** + * @return int + * The number of optional real parameters on this function/method. + * This may differ from getNumberOfOptionalParameters() + * for internal modules lacking proper reflection info, + * or if the installed module version's API changed from what Phan's stubs used, + * or if a function/method uses variadics/func_get_arg*() + * + * @suppress PhanUnreferencedPublicMethod this is made available for plugins + */ + public function getNumberOfOptionalRealParameters(): int + { + return $this->number_of_optional_real_parameters; + } + + /** + * @return int + * The number of optional parameters on this method + */ + public function getNumberOfOptionalParameters(): int + { + return $this->number_of_optional_parameters; + } + + /** + * The number of optional parameters + */ + public function setNumberOfOptionalParameters(int $number): void + { + $this->number_of_optional_parameters = $number; + } + + /** + * @return int + * The number of parameters in this function/method declaration. + * Variadic parameters are counted only once. + * TODO: Specially handle variadic parameters, either here or in ParameterTypesAnalyzer::analyzeOverrideRealSignature + */ + public function getNumberOfRealParameters(): int + { + return $this->number_of_required_real_parameters + + $this->number_of_optional_real_parameters; + } + + /** + * @return int + * The maximum number of parameters to this function/method + */ + public function getNumberOfParameters(): int + { + return $this->number_of_required_parameters + + $this->number_of_optional_parameters; + } + + /** + * @return int + * The number of required real parameters on this function/method. + * This may differ for internal modules lacking proper reflection info, + * or if the installed module version's API changed from what Phan's stubs used. + */ + public function getNumberOfRequiredRealParameters(): int + { + return $this->number_of_required_real_parameters; + } + + /** + * @return int + * The number of required parameters on this function/method + */ + public function getNumberOfRequiredParameters(): int + { + return $this->number_of_required_parameters; + } + + /** + * + * The number of required parameters + */ + public function setNumberOfRequiredParameters(int $number): void + { + $this->number_of_required_parameters = $number; + } + + /** + * @return bool + * True if this method had no return type defined when it + * was defined (either in the signature itself or in the + * docblock). + */ + public function isReturnTypeUndefined(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_RETURN_TYPE_UNDEFINED); + } + + /** + * @return bool + * True if this method had no return type defined when it was defined, + * or if the method had a vague enough return type that Phan would add types to it + * (return type is inferred from the method signature itself and the docblock). + */ + public function isReturnTypeModifiable(): bool + { + if ($this->isReturnTypeUndefined()) { + return true; + } + if (!Config::getValue('allow_overriding_vague_return_types')) { + return false; + } + // Don't allow overriding method types if they have known overrides + if ($this instanceof Method && $this->isOverriddenByAnother()) { + return false; + } + if ($this->getPhanFlags() & Flags::HARDCODED_RETURN_TYPE) { + return false; + } + $return_type = $this->getUnionType(); + // expect that $return_type has at least one type if isReturnTypeUndefined is false. + foreach ($return_type->getTypeSet() as $type) { + // Allow adding more specific types to ObjectType or MixedType. + // TODO: Allow adding more specific types to Array + if ($type instanceof ObjectType || $type instanceof MixedType) { + return true; + } + } + return false; + } + + + /** + * @param bool $is_return_type_undefined + * True if this method had no return type defined when it + * was defined (either in the signature itself or in the + * docblock). + */ + public function setIsReturnTypeUndefined( + bool $is_return_type_undefined + ): void { + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_RETURN_TYPE_UNDEFINED, + $is_return_type_undefined + )); + } + + /** + * @return bool + * True if this method returns a value + * (i.e. it has a return with an expression) + */ + public function hasReturn(): bool + { + return $this->getPhanFlagsHasState(Flags::HAS_RETURN); + } + + /** + * @return bool + * True if this method yields any value(i.e. it is a \Generator) + */ + public function hasYield(): bool + { + return $this->getPhanFlagsHasState(Flags::HAS_YIELD); + } + + /** + * @param bool $has_return + * Set to true to mark this method as having a + * return value + */ + public function setHasReturn(bool $has_return): void + { + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::HAS_RETURN, + $has_return + )); + } + + /** + * @param bool $has_yield + * Set to true to mark this method as having a + * yield value + */ + public function setHasYield(bool $has_yield): void + { + // TODO: In a future release of php-ast, this information will be part of the function node's flags. + // (PHP 7.1+ only, not supported in PHP 7.0) + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::HAS_YIELD, + $has_yield + )); + } + + /** + * @return list + * A list of parameters on the method + * + * @suppress PhanPluginCanUseReturnType + * FIXME: Figure out why adding `: array` causes failures elsewhere (combination with interface?) + */ + public function getParameterList() + { + return $this->parameter_list; + } + + /** + * @return bool - Does any parameter type possibly require recursive analysis if more specific types are provided? + * + * If this returns true, there is at least one parameter and at least one of those can be overridden with a more specific type. + */ + public function needsRecursiveAnalysis(): bool + { + return $this->needs_recursive_analysis ?? ($this->needs_recursive_analysis = $this->computeNeedsRecursiveAnalysis()); + } + + private function computeNeedsRecursiveAnalysis(): bool + { + if (!$this->getNode()) { + // E.g. this can be the case for magic methods, internal methods, stubs, etc. + return false; + } + + foreach ($this->parameter_list as $parameter) { + if ($parameter->getNonVariadicUnionType()->shouldBeReplacedBySpecificTypes()) { + return true; + } + if ($parameter->isPassByReference() && $parameter->getReferenceType() !== Flags::IS_WRITE_REFERENCE) { + return true; + } + } + return false; + } + + /** + * Gets the $ith parameter for the **caller**. + * In the case of variadic arguments, an infinite number of parameters exist. + * (The callee would see variadic arguments(T ...$args) as a single variable of type T[], + * while the caller sees a place expecting an expression of type T. + * + * @param int $i - offset of the parameter. + * @return Parameter|null The parameter type that the **caller** observes. + */ + public function getParameterForCaller(int $i): ?Parameter + { + $list = $this->parameter_list; + if (count($list) === 0) { + return null; + } + $parameter = $list[$i] ?? null; + if ($parameter) { + return $parameter->asNonVariadic(); + } + $last_parameter = $list[count($list) - 1]; + if ($last_parameter->isVariadic()) { + return $last_parameter->asNonVariadic(); + } + return null; + } + + /** + * Gets the $ith parameter for the **caller** (with real types). + * In the case of variadic arguments, an infinite number of parameters exist. + * (The callee would see variadic arguments(T ...$args) as a single variable of type T[], + * while the caller sees a place expecting an expression of type T. + * + * @param int $i - offset of the parameter. + * @return Parameter|null The real parameter type (from php signature) that the **caller** observes. + */ + public function getRealParameterForCaller(int $i): ?Parameter + { + $list = $this->real_parameter_list; + if (count($list) === 0) { + return null; + } + $parameter = $list[$i] ?? null; + if ($parameter) { + return $parameter->asNonVariadic(); + } + $last_parameter = $list[count($list) - 1]; + if ($last_parameter->isVariadic()) { + return $last_parameter->asNonVariadic(); + } + return null; + } + + /** + * @param list $parameter_list + * A list of parameters to set on this method + * (When quick_mode is false, this is also called to temporarily + * override parameter types, etc.) + * @internal + */ + public function setParameterList(array $parameter_list): void + { + $this->parameter_list = $parameter_list; + if (\is_null($this->parameter_list_hash)) { + $this->initParameterListInfo(); + } + } + + /** + * Called to lazily initialize properties of $this derived from $this->parameter_list + */ + private function initParameterListInfo(): void + { + $parameter_list = $this->parameter_list; + $this->parameter_list_hash = self::computeParameterListHash($parameter_list); + $has_pass_by_reference_parameters = false; + foreach ($parameter_list as $param) { + if ($param->isPassByReference()) { + $has_pass_by_reference_parameters = true; + break; + } + } + $this->has_pass_by_reference_parameters = $has_pass_by_reference_parameters; + } + + /** + * Called to generate a hash of a given parameter list, to avoid calling this on the same parameter list twice. + * + * @param list $parameter_list + * + * @return int 32-bit or 64-bit hash. Not likely to collide unless there are around 2^16 possible union types on 32-bit, or around 2^32 on 64-bit. + * (Collisions aren't a concern; The memory/runtime would probably be a bigger issue than collisions in non-quick mode.) + */ + private static function computeParameterListHash(array $parameter_list): int + { + // Choosing a small value to fit inside of a packed array. + if (\count($parameter_list) === 0) { + return 0; + } + if (Config::get_quick_mode()) { + return 0; + } + $param_repr = ''; + foreach ($parameter_list as $param) { + $param_repr .= $param->getUnionType()->__toString() . ','; + } + $raw_bytes = \md5($param_repr, true); + return \unpack(\PHP_INT_SIZE === 8 ? 'q' : 'l', $raw_bytes)[1]; + } + + /** + * @return list $parameter_list + * A list of parameters (not from phpdoc) that were set on this method. The parameters will be cloned. + * + * @suppress PhanPluginCanUseReturnType + * FIXME: Figure out why adding `: array` causes failures elsewhere (combination with interface?) + */ + public function getRealParameterList() + { + // Excessive cloning, to ensure that this stays immutable. + return \array_map(static function (Parameter $param): Parameter { + return clone($param); + }, $this->real_parameter_list); + } + + /** + * @param list $parameter_list + * A list of parameters (not from phpdoc) to set on this method. The parameters will be cloned. + */ + public function setRealParameterList(array $parameter_list): void + { + $this->real_parameter_list = \array_map(static function (Parameter $param): Parameter { + return clone($param); + }, $parameter_list); + + $required_count = self::computeNumberOfRequiredParametersForList($parameter_list); + $optional_count = \count($parameter_list) - $required_count; + $this->number_of_required_real_parameters = $required_count; + $this->number_of_optional_real_parameters = $optional_count; + } + + /** + * @internal - moves real parameter defaults to the inferred phpdoc parameters + */ + public function inheritRealParameterDefaults(): void + { + foreach ($this->real_parameter_list as $i => $real_parameter) { + $parameter = $this->parameter_list[$i] ?? null; + if (!$parameter || $parameter->isVariadic() || $real_parameter->isVariadic()) { + // No more parameters + // TODO: Properly inherit variadic real types + return; + } + $real_type = $real_parameter->getUnionType(); + if (!$real_type->isEmpty()) { + $parameter->setUnionType($parameter->getUnionType()->withRealTypeSet($real_type->getTypeSet())); + } + if (!$real_parameter->hasDefaultValue()) { + continue; + } + + if (!$parameter->isOptional() || $parameter->isVariadic()) { + continue; + } + $parameter->copyDefaultValueFrom($real_parameter); + } + } + + /** + * @param list $parameter_list + */ + protected static function computeNumberOfRequiredParametersForList(array $parameter_list): int + { + for ($i = \count($parameter_list) - 1; $i >= 0; $i--) { + $parameter = $parameter_list[$i]; + if (!$parameter->isOptional()) { + return $i + 1; + } + } + return 0; + } + + /** + * @param UnionType $union_type + * The real (non-phpdoc) return type of this method in its given context. + */ + public function setRealReturnType(UnionType $union_type): void + { + // TODO: was `self` properly resolved already? What about in subclasses? + $this->real_return_type = $union_type; + } + + /** + * @return UnionType + * The type of this method in its given context. + */ + public function getRealReturnType(): UnionType + { + if (!$this->real_return_type) { + // Incomplete patch for https://github.com/phan/phan/issues/670 + return UnionType::empty(); + // throw new \Error(sprintf("Failed to get real return type in %s method %s", (string)$this->getClassFQSEN(), (string)$this)); + } + // Clone the union type, to be certain it will remain immutable. + return $this->real_return_type; + } + + /** + * @param Parameter $parameter + * A parameter to append to the parameter list + * @internal + */ + public function appendParameter(Parameter $parameter): void + { + $this->parameter_list[] = $parameter; + } + + /** + * @return void + * + * Call this before calling appendParameter, if parameters were already added. + * @internal + */ + public function clearParameterList(): void + { + $this->parameter_list = []; + $this->parameter_list_hash = null; + } + + /** + * Adds types from comments to the params of a user-defined function or method. + * Also adds the types from defaults, and emits warnings for certain violations. + * + * Conceptually, Func and Method should have defaults/comments analyzed in the same way. + * + * This does nothing if $function is for an internal method. + * + * @param Context $context + * The context in which the node appears + * + * @param CodeBase $code_base + * + * @param FunctionInterface $function - A Func or Method to add params to the local scope of. + * + * @param Comment $comment - processed doc comment of $node, with params + */ + public static function addParamsToScopeOfFunctionOrMethod( + Context $context, + CodeBase $code_base, + FunctionInterface $function, + Comment $comment + ): void { + if ($function->isPHPInternal()) { + return; + } + $parameter_offset = 0; + $function_parameter_list = $function->getParameterList(); + $real_parameter_name_map = []; + foreach ($function_parameter_list as $parameter) { + $real_parameter_name_map[$parameter->getName()] = $parameter; + self::addParamToScopeOfFunctionOrMethod( + $context, + $code_base, + $function, + $comment, + $parameter_offset, + $parameter + ); + ++$parameter_offset; + } + + $valid_comment_parameter_type_map = []; + foreach ($comment->getParameterMap() as $comment_parameter_name => $comment_parameter) { + if (!\array_key_exists($comment_parameter_name, $real_parameter_name_map)) { + Issue::maybeEmit( + $code_base, + $context, + count($real_parameter_name_map) > 0 ? Issue::CommentParamWithoutRealParam : Issue::CommentParamOnEmptyParamList, + $comment_parameter->getLineno(), + $comment_parameter_name, + (string)$function + ); + continue; + } + // Record phpdoc types to check if they are narrower than real types, later. + // Only keep non-empty types. + $comment_parameter_type = $comment_parameter->getUnionType(); + if (!$comment_parameter_type->isEmpty()) { + $valid_comment_parameter_type_map[$comment_parameter_name] = $comment_parameter_type; + } + if ($comment_parameter->isIgnoredReference()) { + $real_parameter_name_map[$comment_parameter_name]->setIsIgnoredReference(); + } elseif ($comment_parameter->isOutputReference()) { + $real_parameter_name_map[$comment_parameter_name]->setIsOutputReference(); + } + } + foreach ($comment->getVariableList() as $comment_variable) { + if (\array_key_exists($comment_variable->getName(), $real_parameter_name_map)) { + Issue::maybeEmit( + $code_base, + $context, + Issue::CommentVarInsteadOfParam, + $comment_variable->getLineno(), + $comment_variable->getName(), + (string)$function + ); + continue; + } + } + $function->setPHPDocParameterTypeMap($valid_comment_parameter_type_map); + if ($function instanceof Method) { + $function->checkForTemplateTypes(); + } + // Special, for libraries which use this for to document variadic param lists. + } + + /** + * Internally used. + */ + public static function addParamToScopeOfFunctionOrMethod( + Context $context, + CodeBase $code_base, + FunctionInterface $function, + Comment $comment, + int $parameter_offset, + Parameter $parameter + ): void { + if ($function->isPHPInternal()) { + return; + } + $real_type_set = $parameter->getNonVariadicUnionType()->getRealTypeSet(); + $parameter_name = $parameter->getName(); + if ($comment->hasParameterWithNameOrOffset( + $parameter_name, + $parameter_offset + )) { + $comment_param = $comment->getParameterWithNameOrOffset( + $parameter_name, + $parameter_offset + ); + if ($comment_param->isMandatoryInPHPDoc()) { + $function->recordHasMandatoryPHPDocParamAtOffset($parameter_offset); + } + } else { + $comment_param = null; + } + if ($parameter->getNonVariadicUnionType()->isEmpty()) { + // If there is no type specified in PHP, check + // for a docComment with @param declarations. We + // assume order in the docComment matches the + // parameter order in the code + if ($comment_param) { + $comment_param_type = $comment_param->getUnionType(); + if ($parameter->isVariadic() !== $comment_param->isVariadic()) { + Issue::maybeEmit( + $code_base, + $context, + $parameter->isVariadic() ? Issue::TypeMismatchVariadicParam : Issue::TypeMismatchVariadicComment, + $comment_param->getLineno(), + $comment_param->__toString(), + $parameter->__toString() + ); + } + + // if ($parameter->isCloneOfVariadic()) { throw new \Error("Impossible\n"); } + $parameter->addUnionType($comment_param_type); + } + } + + // If there's a default value on the parameter, check to + // see if the type of the default is cool with the + // specified type. + if ($parameter->hasDefaultValue()) { + $default_type = $parameter->getDefaultValueType(); + $default_is_null = $default_type->isType(NullType::instance(false)); + // If the default type isn't null and can't cast + // to the parameter's declared type, emit an + // issue. + if (!$default_is_null) { + if (!$default_type->canCastToUnionType( + $parameter->getUnionType() + )) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeMismatchDefault, + $function->getFileRef()->getLineNumberStart(), + (string)$parameter->getUnionType(), + $parameter_name, + (string)$default_type + ); + } + } + + // If there are no types on the parameter, the + // default shouldn't be treated as the one + // and only allowable type. + $was_empty = $parameter->getUnionType()->isEmpty(); + + // If we have no other type info about a parameter, + // just because it has a default value of null + // doesn't mean that is its type. Any type can default + // to null + if ($default_is_null) { + if ($was_empty) { + $parameter->addUnionType(MixedType::instance(true)->asPHPDocUnionType()); + } + // The parameter constructor or above check for wasEmpty already took care of null default case + } else { + $default_type = $default_type->withFlattenedArrayShapeOrLiteralTypeInstances()->withRealTypeSet($parameter->getNonVariadicUnionType()->getRealTypeSet()); + if ($was_empty) { + $parameter->addUnionType(self::inferNormalizedTypesOfDefault($default_type)); + if (!Config::getValue('guess_unknown_parameter_type_using_default')) { + $parameter->addUnionType(MixedType::instance(false)->asPHPDocUnionType()); + } + } else { + // Don't add both `int` and `?int` to the same set. + foreach ($default_type->getTypeSet() as $default_type_part) { + if (!$parameter->getNonvariadicUnionType()->hasType($default_type_part->withIsNullable(true))) { + // if ($parameter->isCloneOfVariadic()) { throw new \Error("Impossible\n"); } + $parameter->addType($default_type_part); + } + } + } + } + } + // Keep the real type set of the parameter to make redundant condition detection more accurate. + $new_parameter_type = $parameter->getNonVariadicUnionType()->withRealTypeSet($real_type_set); + if ($real_type_set) { + $new_parameter_type = $new_parameter_type->asNormalizedTypes(); + } + $parameter->setUnionType($new_parameter_type); + } + + private static function inferNormalizedTypesOfDefault(UnionType $default_type): UnionType + { + $type_set = $default_type->getTypeSet(); + if (\count($type_set) === 0) { + return $default_type; + } + $normalized_default_type = UnionType::empty(); + foreach ($type_set as $type) { + if ($type instanceof FalseType || $type instanceof NullType) { + return MixedType::instance(false)->asPHPDocUnionType(); + } elseif ($type instanceof GenericArrayType) { + // Ideally should be the **only** type. + $normalized_default_type = $normalized_default_type->withType(ArrayType::instance(false)); + } elseif ($type instanceof TrueType) { + // e.g. for `function myFn($x = true) { }, $x is probably of type bool, but we're less sure about the type of $x from `$x = false` + $normalized_default_type = $normalized_default_type->withType(BoolType::instance(false)); + } else { + $normalized_default_type = $normalized_default_type->withType($type); + } + } + return $normalized_default_type; + } + + /** + * @param array $parameter_map maps a subset of param names to the unmodified phpdoc parameter types. This may differ from real parameter types. + */ + public function setPHPDocParameterTypeMap(array $parameter_map): void + { + $this->phpdoc_parameter_type_map = $parameter_map; + } + + /** + * Records the fact that $parameter_name is an output-only reference. + * @param string $parameter_name + * @suppress PhanUnreferencedPublicMethod + * @unused - TODO: Clean up this and phpdoc_output_references + */ + public function recordOutputReferenceParamName(string $parameter_name): void + { + $this->phpdoc_output_references[] = $parameter_name; + } + + /** + * @return list list of output references. Usually empty. + */ + public function getOutputReferenceParamNames(): array + { + return $this->phpdoc_output_references; + } + + /** + * @return array maps a subset of param names to the unmodified phpdoc parameter types. + */ + public function getPHPDocParameterTypeMap(): array + { + return $this->phpdoc_parameter_type_map; + } + + /** + * @param ?UnionType $type the raw phpdoc union type + */ + public function setPHPDocReturnType(?UnionType $type): void + { + $this->phpdoc_return_type = $type; + } + + /** + * @return ?UnionType the raw phpdoc union type + * @suppress PhanUnreferencedPublicMethod this is made available for plugins or future features. + */ + public function getPHPDocReturnType(): ?UnionType + { + return $this->phpdoc_return_type; + } + + /** + * Returns true if the param list has an instance of PassByReferenceVariable + * If it does, the method has to be analyzed even if the same parameter types were analyzed already + * @param list $parameter_list + */ + private function hasPassByReferenceVariable(array $parameter_list): bool + { + // Common case: function doesn't have any references in parameter list + if ($this->has_pass_by_reference_parameters === false) { + return false; + } + foreach ($parameter_list as $param) { + if ($param instanceof PassByReferenceVariable) { + return true; + } + } + return false; + } + + /** + * analyzeWithNewParams is called only when the quick_mode config is false. + * The new types are inferred based on the caller's types. + * As an optimization, this refrains from re-analyzing the method/function it has already been analyzed for those param types + * (With an equal or larger remaining recursion depth) + * + * @param list $parameter_list + */ + public function analyzeWithNewParams(Context $context, CodeBase $code_base, array $parameter_list): Context + { + $hash = $this->computeParameterListHash($parameter_list); + $has_pass_by_reference_variable = null; + // Nothing to do, except if PassByReferenceVariable was used + if ($hash === $this->parameter_list_hash) { + if (!$this->hasPassByReferenceVariable($parameter_list)) { + // Have to analyze pass by reference variables anyway + return $context; + } + $has_pass_by_reference_variable = true; + } + // Check if we've already analyzed this method with those given types, + // with as much or even more depth left in the recursion. + // (getRecursionDepth() increases as the program recurses downward) + $old_recursion_depth_for_hash = $this->checked_parameter_list_hashes[$hash] ?? null; + $new_recursion_depth_for_hash = $this->getRecursionDepth(); + if ($old_recursion_depth_for_hash !== null) { + if ($new_recursion_depth_for_hash >= $old_recursion_depth_for_hash) { + if (!($has_pass_by_reference_variable ?? $this->hasPassByReferenceVariable($parameter_list))) { + return $context; + } + // Have to analyze pass by reference variables anyway + $new_recursion_depth_for_hash = $old_recursion_depth_for_hash; + } + } + // Record the fact that it has already been analyzed, + // along with the depth of recursion so far. + $this->checked_parameter_list_hashes[$hash] = $new_recursion_depth_for_hash; + return $this->analyze($context, $code_base); + } + + /** + * Analyze this with original parameter types or types from arguments. + */ + abstract public function analyze(Context $context, CodeBase $code_base): Context; + + /** @return int the current depth of recursive non-quick analysis. */ + abstract public function getRecursionDepth(): int; + + /** @return Node|null the node of this function-like's declaration, if any exist and were kept for recursive non-quick analysis. */ + abstract public function getNode(): ?Node; + + /** @return Context location and scope where this was declared. */ + abstract public function getContext(): Context; + + /** @return FileRef location where this was declared. */ + abstract public function getFileRef(): FileRef; + + /** + * Returns true if the return type depends on the argument, and a plugin makes Phan aware of that. + */ + public function hasDependentReturnType(): bool + { + return $this->return_type_callback !== null; + } + + /** + * Returns a union type based on $args_node and $context + * + * @param CodeBase $code_base + * @param Context $context + * @param list $args + */ + public function getDependentReturnType(CodeBase $code_base, Context $context, array $args): UnionType + { + // @phan-suppress-next-line PhanTypeMismatchArgument, PhanTypePossiblyInvalidCallable - Callers should check hasDependentReturnType + $result = ($this->return_type_callback)($code_base, $context, $this, $args); + if (!$result->hasRealTypeSet()) { + $real_return_type = $this->getRealReturnType(); + if (!$real_return_type->isEmpty()) { + return $result->withRealTypeSet($real_return_type->getTypeSet()); + } + } + return $result; + } + + public function setDependentReturnTypeClosure(Closure $closure): void + { + $this->return_type_callback = $closure; + } + + /** + * Returns true if this function or method has additional analysis logic for invocations (From internal and user defined plugins) + */ + public function hasFunctionCallAnalyzer(): bool + { + return $this->function_call_analyzer_callback !== null; + } + + /** + * Perform additional analysis logic for invocations (From internal and user defined plugins) + * + * @param CodeBase $code_base + * @param Context $context + * @param list $args + * @param ?Node $node - the node causing the call. This may be dynamic, e.g. call_user_func_array. This will be required in Phan 3. + */ + public function analyzeFunctionCall(CodeBase $code_base, Context $context, array $args, Node $node = null): void + { + // @phan-suppress-next-line PhanTypePossiblyInvalidCallable, PhanTypeMismatchArgument - Callers should check hasFunctionCallAnalyzer + ($this->function_call_analyzer_callback)($code_base, $context, $this, $args, $node); + } + + /** + * Make additional analysis logic of this function/method use $closure + * If callers need to invoke multiple closures, they should pass in a closure to invoke multiple closures or use addFunctionCallAnalyzer. + */ + public function setFunctionCallAnalyzer(Closure $closure): void + { + $closure_id = spl_object_id($closure); + $this->function_call_analyzer_callback_set = [ + $closure_id => true + ]; + $this->function_call_analyzer_callback = $closure; + } + + /** + * Make additional analysis logic of this function/method use $closure in addition to any other closures. + */ + public function addFunctionCallAnalyzer(Closure $closure): void + { + $closure_id = spl_object_id($closure); + if (isset($this->function_call_analyzer_callback_set[$closure_id])) { + return; + } + $this->function_call_analyzer_callback_set[$closure_id] = true; + $old_closure = $this->function_call_analyzer_callback; + if ($old_closure) { + $closure = ConfigPluginSet::mergeAnalyzeFunctionCallClosures($old_closure, $closure); + } + $this->function_call_analyzer_callback = $closure; + } + + /** + * @return ?Comment - Not set for internal functions/methods + */ + public function getComment(): ?Comment + { + return $this->comment; + } + + public function setComment(Comment $comment): void + { + $this->comment = $comment; + } + + public function getThrowsUnionType(): UnionType + { + $comment = $this->comment; + return $comment ? $comment->getThrowsUnionType() : UnionType::empty(); + } + + /** + * Initialize the inner scope of this method with variables created from the parameters. + * + * Deferred until the parse phase because getting the UnionType of parameter defaults requires having all class constants be known. + */ + public function ensureScopeInitialized(CodeBase $code_base): void + { + if ($this->is_inner_scope_initialized) { + return; + } + $this->is_inner_scope_initialized = true; + $comment = $this->comment; + // $comment can be null for magic methods from `@method` + if ($comment !== null) { + if (!($this instanceof FunctionInterface)) { + throw new AssertionError('Expected any class using FunctionTrait to implement FunctionInterface'); + } + FunctionTrait::addParamsToScopeOfFunctionOrMethod($this->getContext(), $code_base, $this, $comment); + } + } + + /** + * Memoize the result of $fn(), saving the result + * with key $key. + * + * @template T + * + * @param string $key + * The key to use for storing the result of the + * computation. + * + * @param Closure():T $fn + * A function to compute only once for the given + * $key. + * + * @return T + * The result of the given computation is returned + */ + abstract protected function memoize(string $key, Closure $fn); + + abstract protected function memoizeFlushAll(): void; + + /** @return UnionType union type this function-like's declared return type (from PHPDoc, signatures, etc.) */ + abstract public function getUnionType(): UnionType; + + abstract public function setUnionType(UnionType $type): void; + + /** + * Creates a callback that can restore this element to the state it had before parsing. + * @internal - Used by daemon mode + * @return Closure + * @suppress PhanTypeMismatchDeclaredReturnNullable overriding phpdoc type deliberately so that this works in php 7.1 + */ + public function createRestoreCallback(): ?Closure + { + $clone_this = clone($this); + foreach ($clone_this->parameter_list as $i => $parameter) { + $clone_this->parameter_list[$i] = clone($parameter); + } + foreach ($clone_this->real_parameter_list as $i => $parameter) { + $clone_this->real_parameter_list[$i] = clone($parameter); + } + $union_type = $this->getUnionType(); + + return function () use ($clone_this, $union_type): void { + $this->memoizeFlushAll(); + // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach this is intentionally iterating over private properties of the clone. + foreach ($clone_this as $key => $value) { + $this->{$key} = $value; + } + $this->setUnionType($union_type); + }; + } + + /** + * Clone the parameter list, so that modifying the parameters on the first call won't modify the others. + * TODO: If they're immutable, they can be shared without cloning with less worry. + * @internal + */ + public function cloneParameterList(): void + { + $this->setParameterList( + \array_map( + static function (Parameter $parameter): Parameter { + return clone($parameter); + }, + $this->parameter_list + ) + ); + } + + /** + * Returns a FunctionLikeDeclarationType based on phpdoc+real types. + * The return value is used for type casting rule checking. + */ + public function asFunctionLikeDeclarationType(): FunctionLikeDeclarationType + { + return $this->as_closure_declaration_type ?? ($this->as_closure_declaration_type = $this->createFunctionLikeDeclarationType()); + } + + /** + * Does this function-like return a reference? + */ + abstract public function returnsRef(): bool; + + private function createFunctionLikeDeclarationType(): FunctionLikeDeclarationType + { + $params = \array_map(static function (Parameter $parameter): ClosureDeclarationParameter { + return $parameter->asClosureDeclarationParameter(); + }, $this->parameter_list); + + $return_type = $this->getUnionType(); + if ($return_type->isEmpty()) { + $return_type = MixedType::instance(false)->asPHPDocUnionType(); + } + return new ClosureDeclarationType( + $this->getFileRef(), + $params, + $return_type, + $this->returnsRef(), + false + ); + } + + /** + * @return array in the same format as FunctionSignatureMap.php + * @throws \InvalidArgumentException if this function has invalid parameters for generating a stub (e.g. param names, types, etc.) + */ + public function toFunctionSignatureArray(): array + { + $return_type = $this->getUnionType(); + $stub = [$return_type->__toString()]; + '@phan-var array $stub'; // TODO: Should not warn about PhanTypeMismatchDimFetch in isset below + foreach ($this->parameter_list as $parameter) { + $name = $parameter->getName(); + if ($name === '' || isset($stub[$name])) { + throw new \InvalidArgumentException("Invalid name '$name' for {$this->getFQSEN()}"); + } + if ($parameter->isOptional()) { + $name .= '='; + } + $type_string = $parameter->getUnionType()->__toString(); + if ($parameter->isVariadic()) { + $name = '...' . $name; + } + if ($parameter->isPassByReference()) { + $name = '&' . $name; + } + $stub[$name] = $type_string; + } + return $stub; + } + + /** + * Precondition: This function is a generator type + * Converts Generator|T[] to Generator + * Converts Generator|array to Generator, etc. + */ + public function getReturnTypeAsGeneratorTemplateType(): Type + { + return $this->as_generator_template_type ?? ($this->as_generator_template_type = $this->getUnionType()->asGeneratorTemplateType()); + } + + /** + * @var bool have the return types (both real and PHPDoc) of this method been analyzed and combined yet? + */ + protected $did_analyze_return_types = false; + + /** + * Check this method's return types (phpdoc and real) to make sure they're valid, + * and infer a return type from the combination of the signature and phpdoc return types. + */ + public function analyzeReturnTypes(CodeBase $code_base): void + { + if ($this->did_analyze_return_types) { + return; + } + $this->did_analyze_return_types = true; + try { + $this->analyzeReturnTypesInner($code_base); + } catch (RecursionDepthException $_) { + } + } + + /** + * Is this internal? + */ + abstract public function isPHPInternal(): bool; + + /** + * Returns this function's union type without resolving `static` in the function declaration's context. + */ + abstract public function getUnionTypeWithUnmodifiedStatic(): UnionType; + + private function analyzeReturnTypesInner(CodeBase $code_base): void + { + if ($this->isPHPInternal()) { + // nothing to do, no known Node + return; + } + $return_type = $this->getUnionTypeWithUnmodifiedStatic(); + $real_return_type = $this->getRealReturnType(); + $phpdoc_return_type = $this->phpdoc_return_type; + $context = $this->getContext(); + // TODO: use method->getPHPDocReturnType() and getRealReturnType() to check compatibility, like analyzeParameterTypesDocblockSignaturesMatch + + // Look at each parameter to make sure their types + // are valid + + // Look at each type in the function's return union type + foreach ($return_type->withFlattenedArrayShapeOrLiteralTypeInstances()->getTypeSet() as $outer_type) { + foreach ($outer_type->getReferencedClasses() as $type) { + // If its a reference to self, its OK + if ($this instanceof Method && $type instanceof StaticOrSelfType) { + continue; + } + + if ($type instanceof TemplateType) { + if ($this instanceof Method) { + if ($this->isStatic() && !$this->declaresTemplateTypeInComment($type)) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TemplateTypeStaticMethod, + $this->getFileRef()->getLineNumberStart(), + (string)$this->getFQSEN() + ); + } + } + continue; + } + // Make sure the class exists + $type_fqsen = FullyQualifiedClassName::fromType($type); + if (!$code_base->hasClassWithFQSEN($type_fqsen)) { + Issue::maybeEmitWithParameters( + $code_base, + $this->getContext(), + Issue::UndeclaredTypeReturnType, + $this->getFileRef()->getLineNumberStart(), + [$this->getNameForIssue(), (string)$outer_type], + IssueFixSuggester::suggestSimilarClass($code_base, $this->getContext(), $type_fqsen, null, 'Did you mean', IssueFixSuggester::CLASS_SUGGEST_CLASSES_AND_TYPES_AND_VOID) + ); + } elseif ($code_base->hasClassWithFQSEN($type_fqsen->withAlternateId(1))) { + UnionType::emitRedefinedClassReferenceWarning( + $code_base, + $this->getContext(), + $type_fqsen + ); + } + } + } + if (Config::getValue('check_docblock_signature_return_type_match') && !$real_return_type->isEmpty() && ($phpdoc_return_type instanceof UnionType) && !$phpdoc_return_type->isEmpty()) { + $resolved_real_return_type = $real_return_type->withStaticResolvedInContext($context); + foreach ($phpdoc_return_type->getTypeSet() as $phpdoc_type) { + $is_exclusively_narrowed = $phpdoc_type->isExclusivelyNarrowedFormOrEquivalentTo( + $resolved_real_return_type, + $context, + $code_base + ); + // Make sure that the commented type is a narrowed + // or equivalent form of the syntax-level declared + // return type. + if (!$is_exclusively_narrowed) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeMismatchDeclaredReturn, + // @phan-suppress-next-line PhanAccessMethodInternal, PhanPartialTypeMismatchArgument TODO: Support inferring this is FunctionInterface + ParameterTypesAnalyzer::guessCommentReturnLineNumber($this) ?? $context->getLineNumberStart(), + $this->getName(), + $phpdoc_type->__toString(), + $real_return_type->__toString() + ); + } + if ($is_exclusively_narrowed && Config::getValue('prefer_narrowed_phpdoc_return_type')) { + $normalized_phpdoc_return_type = ParameterTypesAnalyzer::normalizeNarrowedParamType($phpdoc_return_type, $real_return_type); + if ($normalized_phpdoc_return_type) { + // TODO: How does this currently work when there are multiple types in the union type that are compatible? + $real_type_set = $real_return_type->getTypeSet(); + $new_return_type = $normalized_phpdoc_return_type->withRealTypeSet($real_type_set); + if ($real_type_set) { + $new_return_type = $new_return_type->asNormalizedTypes(); + } + $this->setUnionType($new_return_type); + } else { + // This check isn't urgent to fix, and is specific to nullable casting rules, + // so use a different issue type. + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeMismatchDeclaredReturnNullable, + // @phan-suppress-next-line PhanAccessMethodInternal, PhanPartialTypeMismatchArgument TODO: Support inferring this is FunctionInterface + ParameterTypesAnalyzer::guessCommentReturnLineNumber($this) ?? $context->getLineNumberStart(), + $this->getName(), + $phpdoc_type->__toString(), + $real_return_type->__toString() + ); + } + } + } + } + if ($return_type->isEmpty()) { + if ($this->hasReturn()) { + if ($this instanceof Method) { + $union_type = $this->getUnionTypeOfMagicIfKnown(); + if ($union_type) { + $this->setUnionType($union_type); + } + } + } else { + if ($this instanceof Func || ($this instanceof Method && ($this->isPrivate() || $this->isEffectivelyFinal() || $this->isMagicAndVoid() || $this->getClass($code_base)->isFinal()))) { + $this->setUnionType(VoidType::instance(false)->asPHPDocUnionType()); + } + } + } + foreach ($real_return_type->getTypeSet() as $type) { + if (!$type->isObjectWithKnownFQSEN()) { + continue; + } + $type_fqsen = FullyQualifiedClassName::fromType($type); + if (!$code_base->hasClassWithFQSEN($type_fqsen)) { + // We should have already warned + continue; + } + $class = $code_base->getClassByFQSEN($type_fqsen); + if ($class->isTrait()) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TypeInvalidTraitReturn, + $this->getFileRef()->getLineNumberStart(), + $this->getNameForIssue(), + $type_fqsen->__toString() + ); + } + } + if ($this->comment) { + // Add plugins **after** the phpdoc and real comment types were merged. + // Plugins affecting return types (for template in (at)return) + $template_type_list = $this->comment->getTemplateTypeList(); + if ($template_type_list) { + $this->addClosureForDependentTemplateType($code_base, $context, $template_type_list); + } + } + } + + /** + * Does this function/method declare an (at)template type for this type? + */ + public function declaresTemplateTypeInComment(TemplateType $template_type): bool + { + if ($this->comment) { + // Template types are identical if they have the same name. See TemplateType::instanceForId. + return \in_array($template_type, $this->comment->getTemplateTypeList(), true); + } + return false; + } + + private function isTemplateTypeUsed(TemplateType $type): bool + { + if ($this->getUnionType()->usesTemplateType($type)) { + // used in `@return` + return true; + } + + if ($this->comment) { + foreach ($this->comment->getParamAssertionMap() as $assertion) { + // @phan-suppress-next-line PhanAccessPropertyInternal + if ($assertion->union_type->usesTemplateType($type)) { + // used in `@phan-assert` + return true; + } + } + } + return false; + } + + /** + * @param TemplateType[] $template_type_list + */ + private function addClosureForDependentTemplateType(CodeBase $code_base, Context $context, array $template_type_list): void + { + if ($this->hasDependentReturnType()) { + // We already added this or this conflicts with a plugin. + return; + } + if (!$template_type_list) { + // Shouldn't happen + return; + } + $parameter_extractor_map = []; + $has_all_templates = true; + foreach ($template_type_list as $template_type) { + if (!$this->isTemplateTypeUsed($template_type)) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TemplateTypeNotUsedInFunctionReturn, + $context->getLineNumberStart(), + $template_type, + $this->getNameForIssue() + ); + $has_all_templates = false; + continue; + } + $parameter_extractor = $this->getTemplateTypeExtractorClosure($code_base, $template_type); + if (!$parameter_extractor) { + Issue::maybeEmit( + $code_base, + $context, + Issue::TemplateTypeNotDeclaredInFunctionParams, + $context->getLineNumberStart(), + $template_type, + $this->getNameForIssue() + ); + $has_all_templates = false; + continue; + } + $parameter_extractor_map[$template_type->getName()] = $parameter_extractor; + } + if (!$has_all_templates) { + return; + } + /** + * Resolve the template types based on the parameters passed to the function + * @param list $args + */ + $analyzer = static function (CodeBase $code_base, Context $context, FunctionInterface $function, array $args) use ($parameter_extractor_map): UnionType { + $args_types = \array_map( + /** + * @param mixed $node + */ + static function ($node) use ($code_base, $context): UnionType { + return UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node); + }, + $args + ); + $template_type_map = []; + foreach ($parameter_extractor_map as $name => $closure) { + $template_type_map[$name] = $closure($args_types, $context); + } + return $function->getUnionType()->withTemplateParameterTypeMap($template_type_map); + }; + $this->setDependentReturnTypeClosure($analyzer); + } + + /** + * @param TemplateType $template_type the template type that this function is looking for references to in parameters + * + * @return ?Closure(list, Context):UnionType + */ + public function getTemplateTypeExtractorClosure(CodeBase $code_base, TemplateType $template_type, int $skip_index = null): ?Closure + { + $closure = null; + foreach ($this->parameter_list as $i => $parameter) { + if ($i === $skip_index) { + continue; + } + $closure_for_type = $parameter->getUnionType()->getTemplateTypeExtractorClosure($code_base, $template_type); + if (!$closure_for_type) { + continue; + } + $closure = TemplateType::combineParameterClosures( + $closure, + /** + * @param list $parameters + */ + static function (array $parameters, Context $context) use ($code_base, $i, $closure_for_type): UnionType { + $param_value = $parameters[$i] ?? null; + if ($param_value !== null) { + if ($param_value instanceof UnionType) { + // This helper method has two callers - one passes in an array of union types, another passes in the raw nodes. + $param_type = $param_value; + } else { + $param_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $param_value); + } + return $closure_for_type($param_type, $context); + } + return UnionType::empty(); + } + ); + } + return $closure; + } + + /** + * Returns the index of the parameter with name $name. + */ + public function getParamIndexForName(string $name): ?int + { + foreach ($this->parameter_list as $i => $param) { + if ($param->getName() === $name) { + return $i; + } + } + return null; + } + + /** + * Adds a plugin that will ensure postconditions in the comments take effect. + * This adds closures the same way getAnalyzeFunctionCallClosures in a plugin would. + * + * @param array $param_assertion_map + * @param ?Closure(CodeBase, Context, FunctionInterface, list):void $closure the previous closures to combine param assertions with + * @return ?Closure(CodeBase, Context, FunctionInterface, list):void + * @internal + */ + private function getPluginForParamAssertionMap(CodeBase $code_base, array $param_assertion_map, ?Closure $closure): ?Closure + { + foreach ($param_assertion_map as $param_name => $assertion) { + $i = $this->getParamIndexForName($param_name); + if ($i === null) { + Issue::maybeEmit( + $code_base, + $this->getContext(), + Issue::CommentParamAssertionWithoutRealParam, + $this->getContext()->getLineNumberStart(), + $param_name, + $this->getNameForIssue() + ); + continue; + } + $new_closure = $this->createClosureForAssertion($code_base, $assertion, $i); + if ($new_closure) { + // @phan-suppress-next-line PhanTypeMismatchArgument + $closure = ConfigPluginSet::mergeAnalyzeFunctionCallClosures($new_closure, $closure); + } + } + return $closure; + } + + /** + * @param int $i the index of the parameter which $assertion acts upon. + * @return ?Closure(CodeBase, Context, FunctionInterface, array, ?Node):void a closure to update the scope with the side effect of the condition + * @suppress PhanAccessPropertyInternal + * @internal + */ + public function createClosureForAssertion(CodeBase $code_base, Assertion $assertion, int $i): ?Closure + { + $union_type = $assertion->union_type; + if ($union_type->hasTemplateTypeRecursive()) { + $union_type_extractor = $this->makeAssertionUnionTypeExtractor($code_base, $union_type, $i); + if (!$union_type_extractor) { + return null; + } + } else { + /** + * @param list $unused_args + */ + $union_type_extractor = static function (CodeBase $unused_code_base, Context $unused_context, array $unused_args) use ($union_type): UnionType { + return $union_type; + }; + } + return self::createClosureForUnionTypeExtractorAndAssertionType($union_type_extractor, $assertion->assertion_type, $i); + } + + /** + * @internal + * @suppress PhanAccessClassConstantInternal + * @param Closure(CodeBase, Context, array):UnionType $union_type_extractor + * @return ?Closure(CodeBase, Context, FunctionInterface, array, ?Node):void a closure to update the scope with the side effect of the condition given the new type from $union_type_extractor + */ + public static function createClosureForUnionTypeExtractorAndAssertionType(Closure $union_type_extractor, int $assertion_type, int $i): ?Closure + { + switch ($assertion_type) { + case Assertion::IS_OF_TYPE: + /** + * @param list $args + */ + return static function (CodeBase $code_base, Context $context, FunctionInterface $unused_function, array $args, ?Node $unused_node) use ($i, $union_type_extractor): void { + $arg = $args[$i] ?? null; + if (!($arg instanceof Node)) { + return; + } + $union_type = $union_type_extractor($code_base, $context, $args); + $new_context = ConditionVisitor::updateToHaveType($code_base, $context, $arg, $union_type); + // NOTE: This is hackish. This modifies the passed in context's scope. + $context->setScope($new_context->getScope()); + }; + case Assertion::IS_NOT_OF_TYPE: + /** + * @param list $args + */ + return static function (CodeBase $code_base, Context $context, FunctionInterface $unused_function, array $args, ?Node $unused_node) use ($i, $union_type_extractor): void { + $arg = $args[$i] ?? null; + if (!($arg instanceof Node)) { + return; + } + $union_type = $union_type_extractor($code_base, $context, $args); + $new_context = ConditionVisitor::updateToNotHaveType($code_base, $context, $arg, $union_type); + // NOTE: This is hackish. This modifies the passed in context's scope. + $context->setScope($new_context->getScope()); + }; + case Assertion::IS_TRUE: + /** + * @param list $args + */ + return static function (CodeBase $code_base, Context $context, FunctionInterface $unused_function, array $args, ?Node $unused_node) use ($i): void { + $arg = $args[$i] ?? null; + if (!($arg instanceof Node)) { + return; + } + $new_context = (new ConditionVisitor($code_base, $context))->__invoke($arg); + // NOTE: This is hackish. This modifies the passed in context's scope. + $context->setScope($new_context->getScope()); + }; + case Assertion::IS_FALSE: + /** + * @param list $args + */ + return static function (CodeBase $code_base, Context $context, FunctionInterface $unused_function, array $args, ?Node $unused_node) use ($i): void { + $arg = $args[$i] ?? null; + if (!($arg instanceof Node)) { + return; + } + $new_context = (new NegatedConditionVisitor($code_base, $context))->__invoke($arg); + // NOTE: This is hackish. This modifies the passed in context's scope. + $context->setScope($new_context->getScope()); + }; + } + // TODO: Support and test combining these closures + return null; + } + + /** + * Creates a closure that can extract real types from template types used in (at)phan-assert. + * + * @return ?Closure(CodeBase, Context, array):UnionType + */ + private function makeAssertionUnionTypeExtractor(CodeBase $code_base, UnionType $type, int $asserted_param_index): ?Closure + { + if (!$this->comment) { + return null; + } + $parameter_extractor_map = []; + foreach ($this->comment->getTemplateTypeList() as $template_type) { + if (!$type->usesTemplateType($template_type)) { + continue; + } + $param_closure = $this->getTemplateTypeExtractorClosure($code_base, $template_type, $asserted_param_index); + if (!$param_closure) { + // TODO: Warn + return null; + } + $parameter_extractor_map[$template_type->getName()] = $param_closure; + } + if (!$parameter_extractor_map) { + return null; + } + /** + * @param list $args + */ + return static function (CodeBase $unused_code_base, Context $context, array $args) use ($type, $parameter_extractor_map): UnionType { + $template_type_map = []; + foreach ($parameter_extractor_map as $template_type_name => $closure) { + $template_type_map[$template_type_name] = $closure($args, $context); + } + return $type->withTemplateParameterTypeMap($template_type_map); + }; + } + + /** + * Create any plugins that exist due to doc comment annotations. + * Must be called after adding this FunctionInterface to the $code_base, so that issues can be emitted if needed. + * @return ?Closure(CodeBase, Context, FunctionInterface, list):UnionType + * @internal + */ + public function getCommentParamAssertionClosure(CodeBase $code_base): ?Closure + { + if (!\is_object($this->comment)) { + return null; + } + $last_mandatory_phpdoc_param_offset = $this->last_mandatory_phpdoc_param_offset; + $closure = null; + if (is_int($last_mandatory_phpdoc_param_offset)) { + $param = $this->parameter_list[$last_mandatory_phpdoc_param_offset] ?? null; + + if ($param) { + /** + * @param list $array + */ + $closure = static function (CodeBase $code_base, Context $context, FunctionInterface $function, array $array) use ($param, $last_mandatory_phpdoc_param_offset): void { + if (count($array) > $last_mandatory_phpdoc_param_offset) { + return; + } + $last_arg = end($array); + if ($last_arg instanceof Node && $last_arg->kind === ast\AST_UNPACK) { + return; + } + Issue::maybeEmit( + $code_base, + $context, + Issue::ParamTooFewInPHPDoc, + $context->getLineNumberStart(), + count($array), + $function->getRepresentationForIssue(true), + $last_mandatory_phpdoc_param_offset + 1, + $param->getName(), + $function->getContext()->getFile(), + $function->getContext()->getLineNumberStart() + ); + }; + } + } + $param_assertion_map = $this->comment->getParamAssertionMap(); + if ($param_assertion_map) { + return $this->getPluginForParamAssertionMap($code_base, $param_assertion_map, $closure); + } + return $closure; + } + + /** + * Returns stub text for the phpdoc parameters that can be used in markdown + */ + public function getParameterStubText(): string + { + return \implode(', ', \array_map(function (Parameter $parameter): string { + return $parameter->toStubString($this->isPHPInternal()); + }, $this->parameter_list)); + } + + /** + * Returns stub text for the real parameters that can be used in `tool/make_stubs` + */ + public function getRealParameterStubText(): string + { + return \implode(', ', \array_map(function (Parameter $parameter): string { + return $parameter->toStubString($this->isPHPInternal()); + }, $this->getRealParameterList())); + } + + /** + * Mark this function or method as read-only + */ + public function setIsPure(): void + { + $this->setPhanFlags($this->getPhanFlags() | Flags::IS_SIDE_EFFECT_FREE); + } + + /** + * Check if this function or method is marked as pure (having no visible side effects) + */ + public function isPure(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_SIDE_EFFECT_FREE); + } + + /** + * @return array very conservatively maps variable names to union types they can have. + * Entries are omitted if there are possible assignments that aren't known. + * + * This is useful as a fallback for determining missing types when analyzing the first iterations of loops. + * + * Other approaches, such as analyzing loops multiple times, are possible, but not implemented. + */ + public function getVariableTypeFallbackMap(CodeBase $code_base): array + { + return $this->memoize(__METHOD__, /** @return array */ function () use ($code_base): array { + // @phan-suppress-next-line PhanTypeMismatchArgument no way to indicate $this always implements FunctionInterface + return FallbackMethodTypesVisitor::inferTypes($code_base, $this); + }); + } + + /** + * Gets the original union type of this function/method. + * + * This is populated the first time it is called. + * + * NOTE: Phan also infers union types from the overridden methods. + * This doesn't attempt to account for that. + */ + public function getOriginalReturnType(): UnionType + { + return $this->original_return_type ?? ($this->original_return_type = $this->getUnionType()); + } + + /** + * Sets the original union type of this function/method to the current union type. + * + * This is populated the first time it is called. + */ + public function setOriginalReturnType(): void + { + $this->original_return_type = $this->getUnionType(); + } + + public function recordHasMandatoryPHPDocParamAtOffset(int $parameter_offset): void + { + if (isset($this->last_mandatory_phpdoc_param_offset) && $parameter_offset <= $this->last_mandatory_phpdoc_param_offset) { + return; + } + $this->last_mandatory_phpdoc_param_offset = $parameter_offset; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/GlobalConstant.php b/bundled-libs/phan/phan/src/Phan/Language/Element/GlobalConstant.php new file mode 100644 index 000000000..7a77d61c2 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/GlobalConstant.php @@ -0,0 +1,163 @@ +setPhanFlags( + Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_DYNAMIC_CONSTANT, + $dynamic_constant + ) + ); + } + + /** + * @return bool + * True if this is a global constant that should be treated as if the real type is unknown. + */ + public function isDynamicConstant(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_DYNAMIC_CONSTANT); + } + + /** + * Override the default getter to fill in a future + * union type if available. + */ + public function getUnionType(): UnionType + { + if (null !== ($union_type = $this->getFutureUnionType())) { + $this->setUnionType($union_type); + } + + return parent::getUnionType(); + } + + // TODO: Make callers check for object types. Those are impossible. + public function setUnionType(UnionType $type): void + { + if ($this->isDynamicConstant() || !$type->hasRealTypeSet()) { + $type = $type->withRealTypeSet(UnionType::typeSetFromString('array|bool|float|int|string|resource|null')); + } + parent::setUnionType($type); + } + + /** + * @return FullyQualifiedGlobalConstantName + * The fully-qualified structural element name of this + * structural element + */ + public function getFQSEN(): FullyQualifiedGlobalConstantName + { + return $this->fqsen; + } + + /** + * @param string $name + * The name of a builtin constant to build a new GlobalConstant structural + * element from. + * + * @return GlobalConstant + * A GlobalConstant structural element representing the given named + * builtin constant. + * + * @throws InvalidArgumentException + * If reflection could not locate the builtin constant. + * + * @throws FQSENException + * If a module declares an invalid constant FQSEN + */ + public static function fromGlobalConstantName( + string $name + ): GlobalConstant { + if (!\defined($name)) { + throw new InvalidArgumentException(\sprintf("This should not happen, defined(%s) is false, but the constant was returned by get_defined_constants()", \var_export($name, true))); + } + $value = \constant($name); + $constant_fqsen = FullyQualifiedGlobalConstantName::fromFullyQualifiedString( + '\\' . $name + ); + $type = Type::fromObject($value); + $result = new self( + new Context(), + $name, + UnionType::of([$type], [$type->asNonLiteralType()]), + 0, + $constant_fqsen + ); + $result->setNodeForValue($value); + return $result; + } + + /** + * Returns a standalone stub of PHP code for this global constant. + * @suppress PhanUnreferencedPublicMethod toStubInfo is used by callers instead + */ + public function toStub(): string + { + [$namespace, $string] = $this->toStubInfo(); + $namespace_text = $namespace === '' ? '' : "$namespace "; + $string = \sprintf("namespace %s{\n%s}\n", $namespace_text, $string); + return $string; + } + + public function getMarkupDescription(): string + { + $string = 'const ' . $this->name . ' = '; + $value_node = $this->getNodeForValue(); + $string .= ASTReverter::toShortString($value_node); + return $string; + } + + /** @return array{0:string,1:string} [string $namespace, string $text] */ + public function toStubInfo(): array + { + $fqsen = (string)$this->getFQSEN(); + $pos = \strrpos($fqsen, '\\'); + if ($pos !== false) { + $name = (string)\substr($fqsen, $pos + 1); + } else { + $name = $fqsen; + } + + $is_defined = \defined($fqsen); + if ($is_defined) { + $repr = StringUtil::varExportPretty(\constant($fqsen)); + $comment = ''; + } else { + $repr = 'null'; + $comment = ' // could not find'; + } + $namespace = \ltrim($this->getFQSEN()->getNamespace(), '\\'); + if (\preg_match('@^[a-zA-Z_\x7f-\xff\\\][a-zA-Z0-9_\x7f-\xff\\\]*$@D', $name)) { + $string = "const $name = $repr;$comment\n"; + } else { + // Internal extension defined a constant with an invalid identifier. + $string = \sprintf("define(%s, %s);%s\n", \var_export($name, true), $repr, $comment); + } + return [$namespace, $string]; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/MarkupDescription.php b/bundled-libs/phan/phan/src/Phan/Language/Element/MarkupDescription.php new file mode 100644 index 000000000..416125b37 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/MarkupDescription.php @@ -0,0 +1,495 @@ +getMarkupDescription(); + $result = "```php\n$markup\n```"; + $extracted_doc_comment = self::extractDescriptionFromDocCommentOrAncestor($element, $code_base); + if (StringUtil::isNonZeroLengthString($extracted_doc_comment)) { + $result .= "\n\n" . $extracted_doc_comment; + } + + return $result; + } + + /** + * @template T + * @param array $signatures + * @return array + */ + private static function signaturesToLower(array $signatures): array + { + $result = []; + foreach ($signatures as $fqsen => $summary) { + $result[\strtolower($fqsen)] = $summary; + } + return $result; + } + + /** + * Eagerly load all of the hover signatures into memory before potentially forking. + */ + public static function eagerlyLoadAllDescriptionMaps(): void + { + if (!\extension_loaded('pcntl')) { + // There's no forking, so descriptions will always be available after the first time they're loaded. + // No need to force phan to load these prior to forking. + return; + } + self::loadClassDescriptionMap(); + self::loadConstantDescriptionMap(); + self::loadFunctionDescriptionMap(); + self::loadPropertyDescriptionMap(); + } + + /** + * @return array mapping lowercase function/method FQSENs to short summaries. + * @internal - The data format may change + */ + public static function loadFunctionDescriptionMap(): array + { + static $descriptions = null; + if (\is_array($descriptions)) { + return $descriptions; + } + return $descriptions = self::signaturesToLower(require(\dirname(__DIR__) . '/Internal/FunctionDocumentationMap.php')); + } + + /** + * @return array mapping lowercase constant/class constant FQSENs to short summaries. + * @internal - The data format may change + */ + public static function loadConstantDescriptionMap(): array + { + static $descriptions = null; + if (\is_array($descriptions)) { + return $descriptions; + } + return $descriptions = self::signaturesToLower(require(\dirname(__DIR__) . '/Internal/ConstantDocumentationMap.php')); + } + + /** + * @return array mapping class FQSENs to short summaries. + */ + public static function loadClassDescriptionMap(): array + { + static $descriptions = null; + if (\is_array($descriptions)) { + return $descriptions; + } + return $descriptions = self::signaturesToLower(require(\dirname(__DIR__) . '/Internal/ClassDocumentationMap.php')); + } + + /** + * @return array mapping property FQSENs to short summaries. + * @internal - The data format may change + */ + public static function loadPropertyDescriptionMap(): array + { + static $descriptions = null; + if (\is_array($descriptions)) { + return $descriptions; + } + return $descriptions = self::signaturesToLower(require(\dirname(__DIR__) . '/Internal/PropertyDocumentationMap.php')); + } + + /** + * Extracts a plaintext description of the element from the doc comment of an element or its ancestor. + * (or from FunctionDocumentationMap.php) + * + * @param array $checked_class_fqsens + */ + public static function extractDescriptionFromDocCommentOrAncestor( + AddressableElementInterface $element, + CodeBase $code_base, + array &$checked_class_fqsens = [] + ): ?string { + $extracted_doc_comment = self::extractDescriptionFromDocComment($element, $code_base); + if (StringUtil::isNonZeroLengthString($extracted_doc_comment)) { + return $extracted_doc_comment; + } + if ($element instanceof ClassElement) { + try { + $fqsen_string = $element->getClassFQSEN()->__toString(); + if (isset($checked_class_fqsens[$fqsen_string])) { + // We already checked this and either succeeded or returned null + return null; + } + $checked_class_fqsens[$fqsen_string] = true; + return self::extractDescriptionFromDocCommentOfAncestorOfClassElement($element, $code_base); + } catch (Exception $_) { + // ignore + } + } + return null; + } + + /** + * @param array $checked_class_fqsens used to guard against recursion + */ + private static function extractDescriptionFromDocCommentOfAncestorOfClassElement( + ClassElement $element, + CodeBase $code_base, + array &$checked_class_fqsens = [] + ): ?string { + if (!$element->isOverride() && $element->getRealDefiningFQSEN() === $element->getFQSEN()) { + return null; + } + $class_fqsen = $element->getDefiningClassFQSEN(); + $class = $code_base->getClassByFQSEN($class_fqsen); + foreach ($class->getAncestorFQSENList() as $ancestor_fqsen) { + $ancestor_element = Clazz::getAncestorElement($code_base, $ancestor_fqsen, $element); + if (!$ancestor_element) { + continue; + } + + $extracted_doc_comment = self::extractDescriptionFromDocCommentOrAncestor($ancestor_element, $code_base, $checked_class_fqsens); + if (StringUtil::isNonZeroLengthString($extracted_doc_comment)) { + return $extracted_doc_comment; + } + } + return null; + } + + /** + * Extracts a plaintext description of the element from the doc comment of an element. + * (or from FunctionDocumentationMap.php) + */ + public static function extractDescriptionFromDocComment( + AddressableElementInterface $element, + CodeBase $code_base = null + ): ?string { + $extracted_doc_comment = self::extractDescriptionFromDocCommentRaw($element); + if (StringUtil::isNonZeroLengthString($extracted_doc_comment)) { + return $extracted_doc_comment; + } + + // This is an element internal to PHP. + if ($element->isPHPInternal()) { + if ($element instanceof FunctionInterface) { + // This is a function/method - Use Phan's FunctionDocumentationMap.php to try to load a markup description. + if ($element instanceof Method && \strtolower($element->getName()) !== '__construct') { + $fqsen = $element->getDefiningFQSEN(); + } else { + $fqsen = $element->getFQSEN(); + } + $key = \strtolower(\ltrim((string)$fqsen, '\\')); + $result = self::loadFunctionDescriptionMap()[$key] ?? null; + if (StringUtil::isNonZeroLengthString($result)) { + return $result; + } + if ($code_base && $element instanceof Method) { + try { + if (\strtolower($element->getName()) === '__construct') { + $class = $element->getClass($code_base); + $class_description = self::extractDescriptionFromDocComment($class, $code_base); + if (StringUtil::isNonZeroLengthString($class_description)) { + return "Construct an instance of `{$class->getFQSEN()}`.\n\n$class_description"; + } + } + } catch (Exception $_) { + } + } + } elseif ($element instanceof ConstantInterface) { + // This is a class or global constant - Use Phan's ConstantDocumentationMap.php to try to load a markup description. + if ($element instanceof ClassConstant) { + $fqsen = $element->getDefiningFQSEN(); + } else { + $fqsen = $element->getFQSEN(); + } + $key = \strtolower(\ltrim((string)$fqsen, '\\')); + return self::loadConstantDescriptionMap()[$key] ?? null; + } elseif ($element instanceof Clazz) { + $key = \strtolower(\ltrim((string)$element->getFQSEN(), '\\')); + return self::loadClassDescriptionMap()[$key] ?? null; + } elseif ($element instanceof Property) { + $key = \strtolower(\ltrim((string)$element->getDefiningFQSEN(), '\\')); + return self::loadPropertyDescriptionMap()[$key] ?? null; + } + } + return null; + } + + private static function extractDescriptionFromDocCommentRaw(AddressableElementInterface $element): ?string + { + $doc_comment = $element->getDocComment(); + if (!StringUtil::isNonZeroLengthString($doc_comment)) { + return null; + } + $comment_category = null; + if ($element instanceof Property) { + $comment_category = Comment::ON_PROPERTY; + } elseif ($element instanceof ConstantInterface) { + $comment_category = Comment::ON_CONST; + } elseif ($element instanceof FunctionInterface) { + $comment_category = Comment::ON_FUNCTION; + } + $extracted_doc_comment = self::extractDocComment($doc_comment, $comment_category, $element->getUnionType()); + return StringUtil::isNonZeroLengthString($extracted_doc_comment) ? $extracted_doc_comment : null; + } + + /** + * @return array information about the param tags + */ + public static function extractParamTagsFromDocComment(AddressableElementInterface $element, bool $with_param_details = true): array + { + $doc_comment = $element->getDocComment(); + if (!\is_string($doc_comment)) { + return []; + } + if (\strpos($doc_comment, '@param') === false) { + return []; + } + // Trim the start and the end of the doc comment. + // + // We leave in the second `*` of `/**` so that every single non-empty line + // of a typical doc comment will begin with a `*` + $doc_comment = \preg_replace('@(^/\*)|(\*/$)@D', '', $doc_comment); + + $results = []; + $lines = \explode("\n", $doc_comment); + foreach ($lines as $i => $line) { + $line = self::trimLine($line); + if (\preg_match('/^\s*@param(\s|$)/D', $line) > 0) { + // Extract all of the (at)param annotations. + $param_tag_summary = self::extractTagSummary($lines, $i); + if (\end($param_tag_summary) === '') { + \array_pop($param_tag_summary); + } + $full_comment = \implode("\n", self::trimLeadingWhitespace($param_tag_summary)); + // @phan-suppress-next-line PhanAccessClassConstantInternal + $matched = \preg_match(Builder::PARAM_COMMENT_REGEX, $full_comment, $match); + if (!$matched) { + continue; + } + if (!isset($match[17])) { + continue; + } + + $name = $match[17]; + if ($with_param_details) { + // Keep the param details and put them in a markdown quote + // @phan-suppress-next-line PhanAccessClassConstantInternal + $full_comment = \preg_replace(Builder::PARAM_COMMENT_REGEX, '`\0`', $full_comment); + } else { + // Drop the param details + // @phan-suppress-next-line PhanAccessClassConstantInternal + $full_comment = \trim(\preg_replace(Builder::PARAM_COMMENT_REGEX, '', $full_comment)); + } + $results[$name] = $full_comment; + } + } + return $results; + } + + /** + * Returns a doc comment with: + * + * - leading `/**` and trailing `*\/` removed + * - leading/trailing space on lines removed, + * - blank lines removed from the beginning and end. + * + * @return string simplified version of the doc comment, with leading `*` on lines preserved. + */ + public static function getDocCommentWithoutWhitespace(string $doc_comment): string + { + // Trim the start and the end of the doc comment. + // + // We leave in the second `*` of `/**` so that every single non-empty line + // of a typical doc comment will begin with a `*` + $doc_comment = \preg_replace('@(^/\*)|(\*/$)@D', '', $doc_comment); + + $lines = \explode("\n", $doc_comment); + $lines = \array_map('trim', $lines); + $lines = MarkupDescription::trimLeadingWhitespace($lines); + while (\in_array(\end($lines), ['*', ''], true)) { + \array_pop($lines); + } + while (\in_array(\reset($lines), ['*', ''], true)) { + \array_shift($lines); + } + return \implode("\n", $lines); + } + + /** + * Returns a markup string with the extracted description of this element (known to be a comment of an element with type $comment_category). + * On success, this is a non-empty string. + * + * @return string markup string + * @internal + */ + public static function extractDocComment(string $doc_comment, int $comment_category = null, UnionType $element_type = null, bool $remove_type = false): string + { + // Trim the start and the end of the doc comment. + // + // We leave in the second `*` of `/**` so that every single non-empty line + // of a typical doc comment will begin with a `*` + $doc_comment = \preg_replace('@(^/\*)|(\*/$)@D', '', $doc_comment); + + $results = []; + $lines = \explode("\n", $doc_comment); + $saw_phpdoc_tag = false; + $did_build_from_phpdoc_tag = false; + + foreach ($lines as $i => $line) { + $line = self::trimLine($line); + if (\preg_match('/^\s*@/', $line) > 0) { + $saw_phpdoc_tag = true; + if (count($results) === 0) { + // Special cases: + if (\in_array($comment_category, [Comment::ON_PROPERTY, Comment::ON_CONST], true)) { + // Treat `@var T description of T` as a valid single-line comment of constants and properties. + // Variables don't currently have associated comments + if (\preg_match('/^\s*@var\s/', $line) > 0) { + $new_lines = self::extractTagSummary($lines, $i); + if (isset($new_lines[0])) { + $did_build_from_phpdoc_tag = true; + // @phan-suppress-next-line PhanAccessClassConstantInternal + $new_lines[0] = \preg_replace(Builder::PARAM_COMMENT_REGEX, $remove_type ? '' : '`\0`', $new_lines[0]); + } + $results = \array_merge($results, $new_lines); + } + } elseif (\in_array($comment_category, Comment::FUNCTION_LIKE, true)) { + // Treat `@return T description of return value` as a valid single-line comment of closures, functions, and methods. + // Variables don't currently have associated comments + if (\preg_match('/^\s*@return(\s|$)/D', $line) > 0) { + $new_lines = self::extractTagSummary($lines, $i); + if (isset($new_lines[0])) { + // @phan-suppress-next-line PhanAccessClassConstantInternal + $new_lines[0] = \preg_replace(Builder::RETURN_COMMENT_REGEX, $remove_type ? '' : '`\0`', $new_lines[0]); + } + $results = \array_merge($results, $new_lines); + } + } + } + } + if ($saw_phpdoc_tag) { + continue; + } + if (\trim($line) === '') { + $line = ''; + if (\in_array(\end($results), ['', false], true)) { + continue; + } + } + $results[] = $line; + } + if (\end($results) === '') { + \array_pop($results); + } + $results = self::trimLeadingWhitespace($results); + $str = \implode("\n", $results); + if ($comment_category === Comment::ON_PROPERTY && !$did_build_from_phpdoc_tag && !$remove_type) { + if ($element_type && !$element_type->isEmpty()) { + $str = \trim("`@var $element_type` $str"); + } + } + return $str; + } + + /** + * Remove leading * and spaces (and trailing spaces) from the provided line of text. + * This is useful for trimming raw doc comment lines + */ + public static function trimLine(string $line): string + { + $line = \rtrim($line); + $pos = \strpos($line, '*'); + if ($pos !== false) { + return (string)\substr($line, $pos + 1); + } else { + return \ltrim($line, "\n\t "); + } + } + + /** + * @param list $lines + * @param int $i the offset of the tag in $lines + * @return list the trimmed lines + * @internal + */ + public static function extractTagSummary(array $lines, int $i): array + { + $summary = []; + $summary[] = self::trimLine($lines[$i]); + for ($j = $i + 1; $j < count($lines); $j++) { + $line = self::trimLine($lines[$j]); + if (\preg_match('/^\s*\{?@/', $line)) { + // Break on other annotations such as (at)internal, {(at)inheritDoc}, etc. + break; + } + if ($line === '' && \end($summary) === '') { + continue; + } + $summary[] = $line; + } + if (\end($summary) === '') { + \array_pop($summary); + } + if (count($summary) === 1 && count(\preg_split('/\s+/', \trim($summary[0]))) <= 2) { + // For something uninformative such as "* (at)return int" (and nothing else), + // don't treat it as a summary. + // + // The caller would already show the return type + return []; + } + return $summary; + } + + /** + * @param list $lines + * @return list + */ + private static function trimLeadingWhitespace(array $lines): array + { + if (count($lines) === 0) { + return []; + } + $min_whitespace = \PHP_INT_MAX; + foreach ($lines as $line) { + if ($line === '') { + continue; + } + $min_whitespace = \min($min_whitespace, \strlen($line)); + $min_whitespace = \min($min_whitespace, \strspn($line, ' ', 0, $min_whitespace)); + if ($min_whitespace === 0) { + return $lines; + } + } + if ($min_whitespace > 0) { + foreach ($lines as $i => $line) { + if ($line === '') { + continue; + } + $lines[$i] = (string)\substr($line, $min_whitespace); + } + } + return $lines; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Method.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Method.php new file mode 100644 index 000000000..71a5d47e9 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Method.php @@ -0,0 +1,1046 @@ + $parameter_list + * A list of parameters to set on this method + */ + public function __construct( + Context $context, + string $name, + UnionType $type, + int $flags, + FullyQualifiedMethodName $fqsen, + $parameter_list + ) { + $internal_scope = new FunctionLikeScope( + $context->getScope(), + $fqsen + ); + $context = $context->withScope($internal_scope); + if ($type->hasTemplateType()) { + $this->recordHasTemplateType(); + } + parent::__construct( + $context, + FullyQualifiedMethodName::canonicalName($name), + $type, + $flags, + $fqsen + ); + + // Presume that this is the original definition + // of this method, and let it be overwritten + // if it isn't. + $this->setDefiningFQSEN($fqsen); + $this->real_defining_fqsen = $fqsen; + + // Record the FQSEN of this method (With the current Clazz), + // to prevent recursing from a method into itself in non-quick mode. + $this->setInternalScope($internal_scope); + + if ($parameter_list !== null) { + $this->setParameterList($parameter_list); + } + $this->checkForTemplateTypes(); + } + + public function __clone() + { + $this->setInternalScope(clone($this->getInternalScope())); + } + + /** + * Sets hasTemplateType to true if it finds any template types in the parameters or methods + */ + public function checkForTemplateTypes(): void + { + if ($this->getUnionType()->hasTemplateTypeRecursive()) { + $this->recordHasTemplateType(); + return; + } + foreach ($this->parameter_list as $parameter) { + if ($parameter->getUnionType()->hasTemplateTypeRecursive()) { + $this->recordHasTemplateType(); + return; + } + } + } + + /** + * @return bool + * True if this is a magic phpdoc method (declared via (at)method on class declaration phpdoc) + */ + public function isFromPHPDoc(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_FROM_PHPDOC); + } + + /** + * Sets whether this is a magic phpdoc method (declared via (at)method on class declaration phpdoc) + * @param bool $from_phpdoc - True if this is a magic phpdoc method + */ + public function setIsFromPHPDoc(bool $from_phpdoc): void + { + $this->setPhanFlags( + Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_FROM_PHPDOC, + $from_phpdoc + ) + ); + } + + /** + * Returns true if this element is overridden by at least one other element + */ + public function isOverriddenByAnother(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_OVERRIDDEN_BY_ANOTHER); + } + + /** + * Sets whether this method is overridden by another method + * + * @param bool $is_overridden_by_another + * True if this method is overridden by another method + */ + public function setIsOverriddenByAnother(bool $is_overridden_by_another): void + { + $this->setPhanFlags(Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_OVERRIDDEN_BY_ANOTHER, + $is_overridden_by_another + )); + } + + /** + * @return bool + * True if this is an abstract method + */ + public function isAbstract(): bool + { + return $this->getFlagsHasState(ast\flags\MODIFIER_ABSTRACT); + } + + /** + * @return bool + * True if this is a final method + */ + public function isFinal(): bool + { + return $this->getFlagsHasState(ast\flags\MODIFIER_FINAL); + } + + /** + * @return bool + * True if this should be analyzed as if it is a final method + */ + public function isEffectivelyFinal(): bool + { + if ($this->isFinal()) { + return true; + } + return Config::getValue('assume_no_external_class_overrides') + && !$this->isOverriddenByAnother() + && !$this->isAbstract(); + } + + /** + * @return bool + * True if this method returns a reference + */ + public function returnsRef(): bool + { + return $this->getFlagsHasState(ast\flags\FUNC_RETURNS_REF); + } + + /** + * Returns true if this is a magic method + * (Names are all normalized in FullyQualifiedMethodName::make()) + */ + public function isMagic(): bool + { + return \array_key_exists($this->name, FullyQualifiedMethodName::MAGIC_METHOD_NAME_SET); + } + + /** + * Returns the return union type of this magic method, if known. + */ + public function getUnionTypeOfMagicIfKnown(): ?UnionType + { + $type_string = FullyQualifiedMethodName::MAGIC_METHOD_TYPE_MAP[$this->name] ?? null; + return $type_string ? UnionType::fromFullyQualifiedPHPDocString($type_string) : null; + } + + /** + * Returns true if this is a magic method which should have return type of void + * (Names are all normalized in FullyQualifiedMethodName::make()) + */ + public function isMagicAndVoid(): bool + { + return \array_key_exists($this->name, FullyQualifiedMethodName::MAGIC_VOID_METHOD_NAME_SET); + } + + /** + * Returns true if this is the `__construct` method + * (Does not return true for php4 constructors) + */ + public function isNewConstructor(): bool + { + // NOTE: This is normalized to lowercase by canonicalName + return $this->name === '__construct'; + } + + /** + * Returns true if this is a placeholder for the `__construct` method that was never declared + */ + public function isFakeConstructor(): bool + { + return ($this->getPhanFlags() & Flags::IS_FAKE_CONSTRUCTOR) !== 0; + } + + /** + * Returns true if this is the magic `__call` method + */ + public function isMagicCall(): bool + { + // NOTE: This is normalized to lowercase by canonicalName + return $this->name === '__call'; + } + + /** + * Returns true if this is the magic `__callStatic` method + */ + public function isMagicCallStatic(): bool + { + // NOTE: This is normalized to lowercase by canonicalName + return $this->name === '__callStatic'; + } + + /** + * @return Method + * A default constructor for the given class + */ + public static function defaultConstructorForClass( + Clazz $clazz, + CodeBase $code_base + ): Method { + if ($clazz->getFQSEN()->getNamespace() === '\\' && $clazz->hasMethodWithName($code_base, $clazz->getName(), true)) { + $old_style_constructor = $clazz->getMethodByName($code_base, $clazz->getName()); + } else { + $old_style_constructor = null; + } + + $method_fqsen = FullyQualifiedMethodName::make( + $clazz->getFQSEN(), + '__construct' + ); + + $method = new Method( + $old_style_constructor ? $old_style_constructor->getContext() : $clazz->getContext(), + '__construct', + $clazz->getUnionType(), + 0, + $method_fqsen, + $old_style_constructor ? $old_style_constructor->getParameterList() : null + ); + + if ($old_style_constructor) { + $method->setRealParameterList($old_style_constructor->getRealParameterList()); + $method->setNumberOfRequiredParameters($old_style_constructor->getNumberOfRequiredParameters()); + $method->setNumberOfOptionalParameters($old_style_constructor->getNumberOfOptionalParameters()); + $method->setRealReturnType($old_style_constructor->getRealReturnType()); + $method->setUnionType($old_style_constructor->getUnionType()); + } + + $method->setPhanFlags($method->getPhanFlags() | Flags::IS_FAKE_CONSTRUCTOR); + + return $method; + } + + /** + * Convert this method to a method from phpdoc. + * Used when importing methods with mixins. + * + * Precondition: This is not a magic method + */ + public function asPHPDocMethod(Clazz $class): Method + { + $method = clone($this); + $method->setFlags($method->getFlags() & ( + ast\flags\MODIFIER_PUBLIC | + ast\flags\MODIFIER_PROTECTED | + ast\flags\MODIFIER_PRIVATE | + ast\flags\MODIFIER_STATIC + )); // clear MODIFIER_ABSTRACT and other flags + $method->setPhanFlags( + ($method->getPhanFlags() | Flags::IS_FROM_PHPDOC) & ~(Flags::IS_OVERRIDDEN_BY_ANOTHER | Flags::IS_OVERRIDE) + ); + + // TODO: Handle template. Possibly support @mixin Foo and resolve methods. + // $method->setPhanFlags(Flags::IS_FROM_PHPDOC); + $method->clearNode(); + // Set the new FQSEN but keep the defining FQSEN + $method->setFQSEN(FullyQualifiedMethodName::make($class->getFQSEN(), $method->getName())); + return $method; + } + + /** + * @param Clazz $clazz - The class to treat as the defining class of the alias. (i.e. the inheriting class) + * @param string $alias_method_name - The alias method name. + * @param int $new_visibility_flags (0 if unchanged) + * @return Method + * + * An alias from a trait use, which is treated as though it was defined in $clazz + * E.g. if you import a trait's method as private/protected, it becomes private/protected **to the class which used the trait** + * + * The resulting alias doesn't inherit the Node of the method body, so aliases won't have a redundant analysis step. + */ + public function createUseAlias( + Clazz $clazz, + string $alias_method_name, + int $new_visibility_flags + ): Method { + + $method_fqsen = FullyQualifiedMethodName::make( + $clazz->getFQSEN(), + $alias_method_name + ); + + $method = new Method( + $this->getContext(), + $alias_method_name, + $this->getUnionTypeWithUnmodifiedStatic(), + $this->getFlags(), + $method_fqsen, + $this->getParameterList() + ); + $method->setPhanFlags($this->getPhanFlags() & ~(Flags::IS_OVERRIDE | Flags::IS_OVERRIDDEN_BY_ANOTHER)); + switch ($new_visibility_flags) { + case ast\flags\MODIFIER_PUBLIC: + case ast\flags\MODIFIER_PROTECTED: + case ast\flags\MODIFIER_PRIVATE: + // Replace the visibility with the new visibility. + $method->setFlags(Flags::bitVectorWithState( + Flags::bitVectorWithState( + $method->getFlags(), + ast\flags\MODIFIER_PUBLIC | ast\flags\MODIFIER_PROTECTED | ast\flags\MODIFIER_PRIVATE, + false + ), + $new_visibility_flags, + true + )); + break; + default: + break; + } + + $defining_fqsen = $this->getDefiningFQSEN(); + if ($method->isPublic()) { + $method->setDefiningFQSEN($defining_fqsen); + } + $method->real_defining_fqsen = $defining_fqsen; + + $method->setRealParameterList($this->getRealParameterList()); + $method->setRealReturnType($this->getRealReturnType()); + $method->setNumberOfRequiredParameters($this->getNumberOfRequiredParameters()); + $method->setNumberOfOptionalParameters($this->getNumberOfOptionalParameters()); + // Copy the comment so that features such as templates will work + $method->comment = $this->comment; + + return $method; + } + + /** + * These magic **instance** methods don't inherit pureness from the class in question + */ + private const NON_PURE_METHOD_NAME_SET = [ + '__clone' => true, + '__construct' => true, + '__destruct' => true, + '__set' => true, // This could exist in a pure class to throw exceptions or do nothing + '__unserialize' => true, + '__unset' => true, // This could exist in a pure class to throw exceptions or do nothing + '__wakeup' => true, + ]; + + /** + * @param Context $context + * The context in which the node appears + * + * @param CodeBase $code_base + * + * @param Node $node + * An AST node representing a method + * + * @param ?Clazz $class + * This will be mandatory in a future Phan release + * + * @return Method + * A Method representing the AST node in the + * given context + */ + public static function fromNode( + Context $context, + CodeBase $code_base, + Node $node, + FullyQualifiedMethodName $fqsen, + ?Clazz $class = null + ): Method { + + // Create the skeleton method object from what + // we know so far + $method = new Method( + $context, + (string)$node->children['name'], + UnionType::empty(), + $node->flags, + $fqsen, + null + ); + $doc_comment = $node->children['docComment'] ?? ''; + $method->setDocComment($doc_comment); + + // Parse the comment above the method to get + // extra meta information about the method. + $comment = Comment::fromStringInContext( + $doc_comment, + $code_base, + $context, + $node->lineno, + Comment::ON_METHOD + ); + + // Defer adding params to the local scope for user functions. (FunctionTrait::addParamsToScopeOfFunctionOrMethod) + // See PostOrderAnalysisVisitor->analyzeCallToMethod + $method->setComment($comment); + + // Record @internal, @deprecated, and @phan-pure + $method->setPhanFlags($method->getPhanFlags() | $comment->getPhanFlagsForMethod()); + + $element_context = new ElementContext($method); + // @var list + // The list of parameters specified on the + // method + $parameter_list = Parameter::listFromNode( + $element_context, + $code_base, + $node->children['params'] + ); + $method->setParameterList($parameter_list); + foreach ($parameter_list as $parameter) { + if ($parameter->getUnionType()->hasTemplateTypeRecursive()) { + $method->recordHasTemplateType(); + break; + } + } + + // Add each parameter to the scope of the function + // NOTE: it's important to clone this, + // because we don't want any assignments to modify the original Parameter + foreach ($parameter_list as $parameter) { + $method->getInternalScope()->addVariable( + $parameter->cloneAsNonVariadic() + ); + } + foreach ($comment->getTemplateTypeList() as $template_type) { + $method->getInternalScope()->addTemplateType($template_type); + } + + if (!$method->isPHPInternal()) { + // If the method is Analyzable, set the node so that + // we can come back to it whenever we like and + // rescan it + $method->setNode($node); + } + + // Keep an copy of the original parameter list, to check for fatal errors later on. + $method->setRealParameterList($parameter_list); + + $required_parameter_count = self::computeNumberOfRequiredParametersForList($parameter_list); + $method->setNumberOfRequiredParameters($required_parameter_count); + + $method->setNumberOfOptionalParameters(\count($parameter_list) - $required_parameter_count); + + // Check to see if the comment specifies that the + // method is deprecated + $method->setIsDeprecated($comment->isDeprecated()); + + // Set whether or not the method is internal to + // the namespace. + $method->setIsNSInternal($comment->isNSInternal()); + + // Set whether or not the comment indicates that the method is intended + // to override another method. + $method->setIsOverrideIntended($comment->isOverrideIntended()); + $method->setSuppressIssueSet($comment->getSuppressIssueSet()); + + $class = $class ?? $context->getClassInScope($code_base); + + if ($method->isMagicCall() || $method->isMagicCallStatic()) { + $method->setNumberOfOptionalParameters(FunctionInterface::INFINITE_PARAMETERS); + $method->setNumberOfRequiredParameters(0); + } + + if ($class->isPure() && !$method->isStatic() && + !\array_key_exists(\strtolower($method->getName()), self::NON_PURE_METHOD_NAME_SET)) { + $method->setIsPure(); + } + + $is_trait = $class->isTrait(); + // Add the syntax-level return type to the method's union type + // if it exists + $return_type = $node->children['returnType']; + if ($return_type instanceof Node) { + // TODO: Avoid resolving this, but only in traits + $return_union_type = (new UnionTypeVisitor($code_base, $context))->fromTypeInSignature( + $return_type + ); + $method->setUnionType($method->getUnionType()->withUnionType($return_union_type)->withRealTypeSet($return_union_type->getTypeSet())); + // TODO: Replace 'self' with the real class when not in a trait + } else { + $return_union_type = UnionType::empty(); + } + // TODO: Deprecate the setRealReturnType API due to properly tracking real return type? + $method->setRealReturnType($return_union_type); + + // If available, add in the doc-block annotated return type + // for the method. + if ($comment->hasReturnUnionType()) { + $comment_return_union_type = $comment->getReturnType(); + if (!$is_trait) { + $comment_return_union_type = $comment_return_union_type->withSelfResolvedInContext($context); + } + $signature_union_type = $method->getUnionType(); + + $new_type = self::computeNewTypeForComment($code_base, $context, $signature_union_type, $comment_return_union_type); + $method->setUnionType($new_type); + $method->setPHPDocReturnType($comment_return_union_type); + } + $element_context->freeElementReference(); + // Populate the original return type. + $method->setOriginalReturnType(); + + return $method; + } + + private static function computeNewTypeForComment(CodeBase $code_base, Context $context, UnionType $signature_union_type, UnionType $comment_return_union_type): UnionType + { + $new_type = $comment_return_union_type; + foreach ($comment_return_union_type->getTypeSet() as $type) { + if (!$type->asPHPDocUnionType()->canAnyTypeStrictCastToUnionType($code_base, $signature_union_type)) { + // Allow `@return static` to override a real type of MyClass. + // php8 may add a real type of static. + $resolved_type = $type->withStaticResolvedInContext($context); + if ($resolved_type === $type || !$resolved_type->asPHPDocUnionType()->canAnyTypeStrictCastToUnionType($code_base, $signature_union_type)) { + $new_type = $new_type->withoutType($type); + } + } + } + + if ($new_type !== $comment_return_union_type) { + $new_type = $signature_union_type->withUnionType($new_type)->withRealTypeSet($signature_union_type->getRealTypeSet()); + if ($comment_return_union_type->hasRealTypeSet() && !$new_type->hasRealTypeSet()) { + $new_type = $new_type->withRealTypeSet($comment_return_union_type->getRealTypeSet()); + } + return $new_type; + } + if ($comment_return_union_type->hasRealTypeSet() && !$signature_union_type->hasRealTypeSet()) { + return $comment_return_union_type; + } + return $comment_return_union_type->withRealTypeSet($signature_union_type->getRealTypeSet()); + } + + + /** + * Ensure that this clone will use the return type of the ancestor method + */ + public function ensureClonesReturnType(Method $original_method): void + { + if ($this->defining_method_for_type_fetching) { + return; + } + // Get the real ancestor of C::method() if C extends B and B extends A + $original_method = $original_method->defining_method_for_type_fetching ?? $original_method; + + // Don't bother with methods that can't have types inferred recursively + if ($original_method->isAbstract() || $original_method->isFromPHPDoc() || $original_method->isPHPInternal()) { + return; + } + + if (!$original_method->getUnionType()->isEmpty() || !$original_method->getRealReturnType()->isEmpty()) { + // This heuristic is used as little as possible. + // It will only use this fallback of directly using the (possibly modified) + // parent's type if the parent method declaration had no phpdoc return type and no real return type (and nothing was guessed such as `void`). + return; + } + $this->defining_method_for_type_fetching = $original_method; + } + + public function setUnionType(UnionType $union_type): void + { + $this->defining_method_for_type_fetching = null; + parent::setUnionType($union_type); + } + + protected function getUnionTypeWithStatic(): UnionType + { + return parent::getUnionType(); + } + + /** + * @return UnionType + * The return type of this method in its given context. + */ + public function getUnionType(): UnionType + { + if ($this->defining_method_for_type_fetching) { + $union_type = $this->defining_method_for_type_fetching->getUnionTypeWithStatic(); + } else { + $union_type = parent::getUnionType(); + } + + // If the type is 'static', add this context's class + // to the return type + if ($union_type->hasStaticType()) { + $union_type = $union_type->withType( + $this->getFQSEN()->getFullyQualifiedClassName()->asType() + ); + } + + // If the type is a generic array of 'static', add + // a generic array of this context's class to the return type + if ($union_type->genericArrayElementTypes()->hasStaticType()) { + // TODO: Base this on the static array type... + $key_type_enum = GenericArrayType::keyTypeFromUnionTypeKeys($union_type); + $union_type = $union_type->withType( + $this->getFQSEN()->getFullyQualifiedClassName()->asType()->asGenericArrayType($key_type_enum) + ); + } + + return $union_type; + } + + public function getUnionTypeWithUnmodifiedStatic(): UnionType + { + return parent::getUnionType(); + } + + public function getFQSEN(): FullyQualifiedMethodName + { + return $this->fqsen; + } + + /** + * @return \Generator + * @phan-return \Generator + * The set of all alternates to this method + * @suppress PhanParamSignatureMismatch + */ + public function alternateGenerator(CodeBase $code_base): \Generator + { + // Workaround so that methods of generic classes will have the resolved template types + yield $this; + $fqsen = $this->getFQSEN(); + $alternate_id = $fqsen->getAlternateId() + 1; + + $fqsen = $fqsen->withAlternateId($alternate_id); + + while ($code_base->hasMethodWithFQSEN($fqsen)) { + yield $code_base->getMethodByFQSEN($fqsen); + $fqsen = $fqsen->withAlternateId(++$alternate_id); + } + } + + /** + * @param CodeBase $code_base + * The code base with which to look for classes + * + * @return Method[] + * 0 or more Methods that this Method is overriding + * (Abstract methods are returned before concrete methods) + */ + public function getOverriddenMethods( + CodeBase $code_base + ): array { + // Get the class that defines this method + $class = $this->getClass($code_base); + + // Get the list of ancestors of that class + $ancestor_class_list = $class->getAncestorClassList( + $code_base + ); + + $defining_fqsen = $this->getDefiningFQSEN(); + + $method_list = []; + $abstract_method_list = []; + // Hunt for any ancestor classes that define a method with + // the same name as this one. + foreach ($ancestor_class_list as $ancestor_class) { + // TODO: Handle edge cases in traits. + // A trait may be earlier in $ancestor_class_list than the parent, but the parent may define abstract classes. + // TODO: What about trait aliasing rules? + if ($ancestor_class->hasMethodWithName($code_base, $this->name, true)) { + $method = $ancestor_class->getMethodByName( + $code_base, + $this->name + ); + if ($method->getDefiningFQSEN() === $defining_fqsen) { + // Skip it, this method **is** the one which defined this. + continue; + } + // We initialize the overridden method's scope to ensure that + // analyzers are aware of the full param/return types of the overridden method. + $method->ensureScopeInitialized($code_base); + if ($method->isAbstract()) { + // TODO: check for trait conflicts, etc. + $abstract_method_list[] = $method; + continue; + } + $method_list[] = $method; + } + } + // Return abstract methods before concrete methods, in order to best check method compatibility. + $method_list = \array_merge($abstract_method_list, $method_list); + // Give up on throwing exceptions if this method doesn't override anything. + // Mixins and traits result in too many edge cases: https://github.com/phan/phan/issues/3796 + return $method_list; + } + + /** + * @return FullyQualifiedMethodName the FQSEN with the original definition (Even if this is private/protected and inherited from a trait). Used for dead code detection. + * Inheritance tests use getDefiningFQSEN() so that access checks won't break. + */ + public function getRealDefiningFQSEN(): FullyQualifiedMethodName + { + return $this->real_defining_fqsen ?? $this->getDefiningFQSEN(); + } + + /** + * @return string + * A string representation of this method signature (preferring phpdoc types) + */ + public function __toString(): string + { + $string = ''; + // TODO: should this representation and other representations include visibility? + + $string .= 'function '; + if ($this->returnsRef()) { + $string .= '&'; + } + $string .= $this->name; + + $string .= '(' . \implode(', ', \array_map(function (Parameter $param): string { + return $param->toStubString($this->isPHPInternal()); + }, $this->getParameterList())) . ')'; + + $union_type = $this->getUnionTypeWithUnmodifiedStatic(); + if (!$union_type->isEmpty()) { + $string .= ' : ' . (string)$union_type; + } + + return $string; + } + + /** + * @return string + * A string representation of this method signature + * (Based on real types only, instead of phpdoc+real types) + */ + public function toRealSignatureString(): string + { + $string = ''; + + $string .= 'function '; + if ($this->returnsRef()) { + $string .= '&'; + } + $string .= $this->name; + + $string .= '(' . \implode(', ', \array_map(function (Parameter $param): string { + return $param->toStubString($this->isPHPInternal()); + }, $this->getRealParameterList())) . ')'; + + if (!$this->getRealReturnType()->isEmpty()) { + $string .= ' : ' . (string)$this->getRealReturnType(); + } + + return $string; + } + + public function getMarkupDescription(): string + { + $string = ''; + // It's an error to have visibility or abstract in an interface's stub (e.g. JsonSerializable) + if ($this->isPrivate()) { + $string .= 'private '; + } elseif ($this->isProtected()) { + $string .= 'protected '; + } else { + $string .= 'public '; + } + + if ($this->isAbstract()) { + $string .= 'abstract '; + } + + if ($this->isStatic()) { + $string .= 'static '; + } + + $string .= 'function '; + if ($this->returnsRef()) { + $string .= '&'; + } + $string .= $this->name; + + $string .= '(' . $this->getParameterStubText() . ')'; + + if ($this->isPHPInternal()) { + $return_type = $this->getUnionType(); + } else { + $return_type = $this->real_return_type; + } + if ($return_type && !$return_type->isEmpty()) { + // Use PSR-12 style with no space before `:` + $string .= ': ' . (string)$return_type; + } + + return $string; + } + + /** + * Returns this method's visibility ('private', 'protected', or 'public') + */ + public function getVisibilityName(): string + { + if ($this->isPrivate()) { + return 'private'; + } elseif ($this->isProtected()) { + return 'protected'; + } else { + return 'public'; + } + } + + /** + * Returns a PHP stub that can be used in the output of `tool/make_stubs` + */ + public function toStub(bool $class_is_interface = false): string + { + $string = ' '; + if ($this->isFinal()) { + $string .= 'final '; + } + // It's an error to have visibility or abstract in an interface's stub (e.g. JsonSerializable) + if (!$class_is_interface) { + $string .= $this->getVisibilityName() . ' '; + + if ($this->isAbstract()) { + $string .= 'abstract '; + } + } + + if ($this->isStatic()) { + $string .= 'static '; + } + + $string .= 'function '; + if ($this->returnsRef()) { + $string .= '&'; + } + $string .= $this->name; + + $string .= '(' . $this->getRealParameterStubText() . ')'; + + if (!$this->getRealReturnType()->isEmpty()) { + $string .= ' : ' . (string)$this->getRealReturnType(); + } + if ($this->isAbstract()) { + $string .= ';'; + } else { + $string .= ' {}'; + } + + return $string; + } + + /** + * Does this method have template types anywhere in its parameters or return type? + * (This check is recursive) + */ + public function hasTemplateType(): bool + { + return $this->getPhanFlagsHasState(Flags::HAS_TEMPLATE_TYPE); + } + + private function recordHasTemplateType(): void + { + $this->setPhanFlags($this->getPhanFlags() | Flags::HAS_TEMPLATE_TYPE); + } + + /** + * Attempt to convert this template method into a method with concrete types + * Either returns the original method or a clone of the method with more type information. + */ + public function resolveTemplateType( + CodeBase $code_base, + UnionType $object_union_type + ): Method { + $defining_fqsen = $this->getDefiningClassFQSEN(); + $defining_class = $code_base->getClassByFQSEN($defining_fqsen); + if (!$defining_class->isGeneric()) { + // ??? + return $this; + } + $expected_type = $defining_fqsen->asType(); + + foreach ($object_union_type->getTypeSet() as $type) { + if (!$type->hasTemplateParameterTypes()) { + continue; + } + if (!$type->isObjectWithKnownFQSEN()) { + continue; + } + $expanded_type = $type->withIsNullable(false)->asExpandedTypes($code_base); + foreach ($expanded_type->getTypeSet() as $candidate) { + if (!$candidate->isTemplateSubtypeOf($expected_type)) { + continue; + } + // $candidate is $expected_type + $result = $this->cloneWithTemplateParameterTypeMap($candidate->getTemplateParameterTypeMap($code_base)); + return $result; + } + } + // E.g. we can have `MyClass @implements MyBaseClass` - so we check the expanded types for any template types, as well + foreach ($object_union_type->asExpandedTypes($code_base)->getTypeSet() as $type) { + if (!$type->hasTemplateParameterTypes()) { + continue; + } + if (!$type->isObjectWithKnownFQSEN()) { + continue; + } + $expanded_type = $type->withIsNullable(false)->asExpandedTypes($code_base); + foreach ($expanded_type->getTypeSet() as $candidate) { + if (!$candidate->isTemplateSubtypeOf($expected_type)) { + continue; + } + // $candidate is $expected_type + $result = $this->cloneWithTemplateParameterTypeMap($candidate->getTemplateParameterTypeMap($code_base)); + return $result; + } + } + return $this; + } + + /** + * @param array $template_type_map + * A map from template type identifier to a concrete type + */ + private function cloneWithTemplateParameterTypeMap(array $template_type_map): Method + { + $result = clone($this); + $result->cloneParameterList(); + foreach ($result->parameter_list as $parameter) { + $parameter->setUnionType($parameter->getUnionType()->withTemplateParameterTypeMap($template_type_map)); + } + $result->setUnionType($result->getUnionType()->withTemplateParameterTypeMap($template_type_map)); + $result->setPhanFlags($result->getPhanFlags() & ~Flags::HAS_TEMPLATE_TYPE); + if (Config::get_track_references()) { + // Quick and dirty fix to make dead code detection work on this clone. + // Consider making this an object instead. + // @see AddressableElement::addReference() + $result->reference_list = &$this->reference_list; + } + return $result; + } + + /** + * @override + */ + public function addReference(FileRef $file_ref): void + { + if (Config::get_track_references()) { + // Currently, we don't need to track references to PHP-internal methods/functions/constants + // such as PHP_VERSION, strlen(), Closure::bind(), etc. + // This may change in the future. + if ($this->isPHPInternal()) { + return; + } + if ($file_ref instanceof Context && $file_ref->isInFunctionLikeScope() && $file_ref->getFunctionLikeFQSEN() === $this->fqsen) { + // Don't track methods calling themselves + return; + } + $this->reference_list[$file_ref->__toString()] = $file_ref; + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Parameter.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Parameter.php new file mode 100644 index 000000000..16a148f92 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Parameter.php @@ -0,0 +1,884 @@ +getDefaultConstantName(), if this is available from reflection. + * This gives better issue messages, hover text, and better output in tool/make_stubs + */ + private $default_value_constant_name = null; + + /** + * @var ?string + * The raw comment string from a default in an (at)method tag. + * + * This may be nonsense like '...' or 'default'. + */ + private $default_value_representation = null; + + /** + * @var bool + * True if the default value was inferred from reflection + */ + private $default_value_from_reflection = false; + + /** + * @var bool + * True if the variable name or comment indicates the parameter is unused + */ + private $should_warn_if_provided = false; + + /** + * @return static + */ + public static function create( + Context $context, + string $name, + UnionType $type, + int $flags + ) { + if (Flags::bitVectorHasState($flags, ast\flags\PARAM_VARIADIC)) { + return new VariadicParameter($context, $name, $type, $flags); + } + return new Parameter($context, $name, $type, $flags); + } + + /** + * @return bool + * True if this parameter has a type for its + * default value + */ + public function hasDefaultValue(): bool + { + return $this->default_value_type !== null || $this->default_value_future_type !== null; + } + + /** + * @param UnionType $type + * The type of the default value for this parameter + */ + public function setDefaultValueType(UnionType $type): void + { + $this->default_value_type = $type; + } + + /** + * @param FutureUnionType $type + * The future type of the default value for this parameter + */ + public function setDefaultValueFutureType(FutureUnionType $type): void + { + $this->default_value_future_type = $type; + } + + /** + * @param ?string $representation + * The new representation of the default value. + */ + public function setDefaultValueRepresentation(?string $representation): void + { + $this->default_value_representation = $representation; + } + + /** + * @return UnionType + * The type of the default value for this parameter + * if it exists + */ + public function getDefaultValueType(): UnionType + { + $future_type = $this->default_value_future_type; + if ($future_type !== null) { + // Only attempt to resolve the future type once. + try { + $this->default_value_type = $future_type->get()->asNonLiteralType(); + } catch (IssueException $exception) { + // Ignore exceptions + Issue::maybeEmitInstance( + $future_type->getCodebase(), // @phan-suppress-current-line PhanAccessMethodInternal + $future_type->getContext(), // @phan-suppress-current-line PhanAccessMethodInternal + $exception->getIssueInstance() + ); + } finally { + // Only try to resolve the FutureType once. + $this->default_value_future_type = null; + } + } + // @phan-suppress-next-line PhanPossiblyNullTypeReturn callers should check hasDefaultType + return $this->default_value_type; + } + + /** + * @param mixed $value + * The value of the default for this parameter + */ + public function setDefaultValue($value): void + { + $this->default_value = $value; + } + + /** + * If the value's default is null, or a constant evaluating to null, + * then the parameter type should be converted to nullable + * (E.g. `int $x = null` and `?int $x = null` are equivalent. + */ + public function handleDefaultValueOfNull(): void + { + if ($this->default_value_type && $this->default_value_type->isType(NullType::instance(false))) { + // If it isn't already nullable, convert the parameter type to nullable. + $this->convertToNullable(); + } + } + + /** + * @return mixed + * The value of the default for this parameter if one + * is defined, otherwise null. + */ + public function getDefaultValue() + { + return $this->default_value; + } + + /** + * @return list + * A list of parameters from an AST node. + */ + public static function listFromNode( + Context $context, + CodeBase $code_base, + Node $node + ): array { + $parameter_list = []; + foreach ($node->children as $child_node) { + $parameter = + Parameter::fromNode($context, $code_base, $child_node); + + $parameter_list[] = $parameter; + } + + return $parameter_list; + } + + /** + * @param list<\ReflectionParameter> $reflection_parameters + * @return list + */ + public static function listFromReflectionParameterList( + array $reflection_parameters + ): array { + return \array_map([self::class, 'fromReflectionParameter'], $reflection_parameters); + } + + /** + * Creates a parameter signature for a function-like from the name, type, etc. of the passed in reflection parameter + */ + public static function fromReflectionParameter( + \ReflectionParameter $reflection_parameter + ): Parameter { + $flags = 0; + // Check to see if it's a pass-by-reference parameter + if ($reflection_parameter->isPassedByReference()) { + $flags |= ast\flags\PARAM_REF; + } + + // Check to see if it's variadic + if ($reflection_parameter->isVariadic()) { + $flags |= ast\flags\PARAM_VARIADIC; + } + $parameter_type = UnionType::fromReflectionType($reflection_parameter->getType()); + $parameter = self::create( + new Context(), + $reflection_parameter->getName() ?? "arg", + $parameter_type, + $flags + ); + if ($reflection_parameter->isOptional()) { + if (!$parameter_type->isEmpty() && !$parameter_type->containsNullable()) { + $default_type = $parameter_type; + } else { + $default_type = NullType::instance(false)->asPHPDocUnionType(); + } + if ($reflection_parameter->isDefaultValueAvailable()) { + try { + $default_value = $reflection_parameter->getDefaultValue(); + $parameter->setDefaultValue($default_value); + $default_type = Type::fromObject($default_value)->asPHPDocUnionType(); + if ($reflection_parameter->isDefaultValueConstant()) { + $parameter->default_value_constant_name = $reflection_parameter->getDefaultValueConstantName(); + } + $parameter->default_value_from_reflection = true; + } catch (Throwable $e) { + CLI::printErrorToStderr(\sprintf( + "Warning: encountered invalid ReflectionParameter information for param $%s: %s %s\n", + $reflection_parameter->getName(), + \get_class($e), + $e->getMessage() + )); + // Uncomment to show which function is invalid + // phan_print_backtrace(); + } + } + $parameter->setDefaultValueType($default_type); + } + return $parameter; + } + + /** + * @param Node|string|float|int $node + * @return ?UnionType - Returns if we know the exact type of $node and can easily resolve it + */ + private static function maybeGetKnownDefaultValueForNode($node): ?UnionType + { + if (!($node instanceof Node)) { + return Type::nonLiteralFromObject($node)->asRealUnionType(); + } + // XXX: This could be made more precise and handle things like unary/binary ops. + // However, this doesn't know about constants that haven't been parsed yet. + if ($node->kind === ast\AST_CONST) { + $name = $node->children['name']->children['name'] ?? null; + if (is_string($name)) { + switch (\strtolower($name)) { + case 'false': + return FalseType::instance(false)->asRealUnionType(); + case 'true': + return TrueType::instance(false)->asRealUnionType(); + case 'null': + return NullType::instance(false)->asRealUnionType(); + } + } + } + return null; + } + + /** + * @return Parameter + * A parameter built from a node + */ + public static function fromNode( + Context $context, + CodeBase $code_base, + Node $node + ): Parameter { + // Get the type of the parameter + $type_node = $node->children['type']; + if ($type_node) { + try { + $union_type = (new UnionTypeVisitor($code_base, $context))->fromTypeInSignature($type_node); + } catch (IssueException $e) { + Issue::maybeEmitInstance($code_base, $context, $e->getIssueInstance()); + $union_type = UnionType::empty(); + } + } else { + $union_type = UnionType::empty(); + } + + // Create the skeleton parameter from what we know so far + $parameter_name = (string)$node->children['name']; + $parameter = Parameter::create( + (clone($context))->withLineNumberStart($node->lineno), + $parameter_name, + $union_type, + $node->flags + ); + if (($type_node->kind ?? null) === ast\AST_NULLABLE_TYPE) { + $parameter->setIsUsingNullableSyntax(); + } + + // If there is a default value, store it and its type + $default_node = $node->children['default']; + if (preg_match('/^(_$|unused)/iD', $parameter_name)) { + if ($default_node !== null) { + $parameter->should_warn_if_provided = true; + } + self::warnAboutParamNameIndicatingUnused($code_base, $context, $node, $parameter_name); + } + if ($default_node !== null) { + // Set the actual value of the default + $parameter->setDefaultValue($default_node); + try { + // @phan-suppress-next-line PhanAccessMethodInternal + ParseVisitor::checkIsAllowedInConstExpr($default_node); + + // We can't figure out default values during the + // parsing phase, unfortunately + $has_error = false; + } catch (InvalidArgumentException $_) { + // If the parameter default is an invalid constant expression, + // then don't use that value elsewhere. + Issue::maybeEmit( + $code_base, + $context, + Issue::InvalidConstantExpression, + $default_node->lineno ?? $node->lineno + ); + $has_error = true; + } + $default_value_union_type = $has_error ? null : self::maybeGetKnownDefaultValueForNode($default_node); + + if ($default_value_union_type !== null) { + // Set the default value + $parameter->setDefaultValueType($default_value_union_type); + } else { + if (!($default_node instanceof Node)) { + throw new AssertionError("Somehow failed to infer type for the default_node - not a scalar or a Node"); + } + + if ($default_node->kind === ast\AST_ARRAY) { + // We know the parameter default is some sort of array, but we don't know any more (e.g. key types, value types). + // When the future type is resolved, we'll know something more specific. + $default_value_union_type = ArrayType::instance(false)->asRealUnionType(); + } else { + static $possible_parameter_default_union_type = null; + if ($possible_parameter_default_union_type === null) { + // These can be constants or literals (or null/true/false) + // (STDERR, etc. are constants) + $possible_parameter_default_union_type = UnionType::fromFullyQualifiedRealString('array|bool|float|int|string|resource|null'); + } + $default_value_union_type = $possible_parameter_default_union_type; + } + $parameter->setDefaultValueType($default_value_union_type); + if (!$has_error) { + $parameter->setDefaultValueFutureType(new FutureUnionType( + $code_base, + (clone($context))->withLineNumberStart($default_node->lineno ?? 0), + $default_node + )); + } + } + $parameter->handleDefaultValueOfNull(); + } + + return $parameter; + } + + private static function warnAboutParamNameIndicatingUnused( + CodeBase $code_base, + Context $context, + Node $node, + string $parameter_name + ): void { + if ($context->isPHPInternal()) { + // Don't warn about internal stubs - the actual extension may have $_ or $unused in the name. + return; + } + $is_closure = false; + if ($context->isInFunctionLikeScope()) { + $func = $context->getFunctionLikeFQSEN(); + $is_closure = $func instanceof FullyQualifiedFunctionName && $func->isClosure(); + } + Issue::maybeEmit( + $code_base, + $context, + $is_closure ? Issue::ParamNameIndicatingUnusedInClosure : Issue::ParamNameIndicatingUnused, + $node->lineno, + $parameter_name + ); + } + + /** + * @return bool + * True if this is an optional parameter + */ + public function isOptional(): bool + { + return $this->hasDefaultValue(); + } + + /** + * @return bool + * True if this is a required parameter + * @suppress PhanUnreferencedPublicMethod provided for API completeness + */ + public function isRequired(): bool + { + return !$this->isOptional(); + } + + /** + * @return bool + * True if this parameter is variadic, i.e. can + * take an unlimited list of parameters and express + * them as an array. + */ + public function isVariadic(): bool + { + return false; + } + + /** + * Returns the Parameter in the form expected by a caller. + * + * If this parameter is variadic (e.g. `DateTime ...$args`), then this + * would return a parameter with the type of the elements (e.g. `DateTime`) + * + * If this parameter is not variadic, returns $this. + * + * @return static (usually $this) + */ + public function asNonVariadic() + { + return $this; + } + + /** + * If this Parameter is variadic, calling `getUnionType` + * will return an array type such as `DateTime[]`. This + * method will return the element type (such as `DateTime`) + * for variadic parameters. + */ + public function getNonVariadicUnionType(): UnionType + { + return self::getUnionType(); + } + + /** + * @return bool - True when this is a non-variadic clone of a variadic parameter. + * (We avoid bugs by adding new types to a variadic parameter if this is cloned.) + * However, error messages still need to convert variadic parameters to a string. + */ + public function isCloneOfVariadic(): bool + { + return false; + } + + /** + * Add the given union type to this parameter's union type + * + * @param UnionType $union_type + * The type to add to this parameter's union type + */ + public function addUnionType(UnionType $union_type): void + { + parent::setUnionType(self::getUnionType()->withUnionType($union_type)); + } + + /** + * Add the given type to this parameter's union type + * + * @param Type $type + * The type to add to this parameter's union type + */ + public function addType(Type $type): void + { + parent::setUnionType(self::getUnionType()->withType($type)); + } + + /** + * @return bool + * True if this parameter is pass-by-reference + * i.e. prefixed with '&'. + */ + public function isPassByReference(): bool + { + return $this->getFlagsHasState(ast\flags\PARAM_REF); + } + + /** + * Returns an enum value indicating how this reference parameter is changed by the caller. + * + * E.g. for REFERENCE_WRITE_ONLY, the reference parameter ignores the passed in value and always replaces it with another type. + * (added with (at)phan-output-parameter in PHPDoc or with special prefixes in FunctionSignatureMap.php) + */ + public function getReferenceType(): int + { + $flags = $this->getPhanFlags(); + if (Flags::bitVectorHasState($flags, Flags::IS_IGNORED_REFERENCE)) { + return self::REFERENCE_IGNORED; + } elseif (Flags::bitVectorHasState($flags, Flags::IS_READ_REFERENCE)) { + return self::REFERENCE_READ_WRITE; + } elseif (Flags::bitVectorHasState($flags, Flags::IS_WRITE_REFERENCE)) { + return self::REFERENCE_WRITE_ONLY; + } + return self::REFERENCE_DEFAULT; + } + + /** + * Records that this parameter is an output reference + * (it overwrites the value of the argument by reference) + */ + public function setIsOutputReference(): void + { + $this->enablePhanFlagBits(Flags::IS_WRITE_REFERENCE); + $this->disablePhanFlagBits(Flags::IS_READ_REFERENCE); + } + + /** + * Records that this parameter is an ignored reference + * (it should be assumed that the reference does not affect types in a meaningful way for the caller) + */ + public function setIsIgnoredReference(): void + { + $this->enablePhanFlagBits(Flags::IS_IGNORED_REFERENCE); + $this->disablePhanFlagBits(Flags::IS_READ_REFERENCE | Flags::IS_WRITE_REFERENCE); + } + + private function setIsUsingNullableSyntax(): void + { + $this->enablePhanFlagBits(Flags::IS_PARAM_USING_NULLABLE_SYNTAX); + } + + /** + * Is this a parameter that uses the nullable `?` syntax in the actual declaration? + * + * E.g. this will be true for `?int $myParam = null`, but false for `int $myParam = null` + * + * This is needed to deal with edge cases of analysis. + */ + public function isUsingNullableSyntax(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_PARAM_USING_NULLABLE_SYNTAX); + } + + public function __toString(): string + { + $string = ''; + $flags = $this->getFlags(); + if ($flags & self::PARAM_MODIFIER_VISIBILITY_FLAGS) { + $string .= $flags & ast\flags\PARAM_MODIFIER_PUBLIC ? 'public ' : + ($flags & ast\flags\PARAM_MODIFIER_PROTECTED ? 'protected ' : 'private '); + } + + $union_type = $this->getNonVariadicUnionType(); + if (!$union_type->isEmpty()) { + $string .= $union_type->__toString() . ' '; + } + + if ($this->isPassByReference()) { + $string .= '&'; + } + + if ($this->isVariadic()) { + $string .= '...'; + } + + $string .= "\${$this->getName()}"; + + if ($this->hasDefaultValue() && !$this->isVariadic()) { + $string .= ' = ' . $this->generateDefaultNodeRepresentation(); + } + + return $string; + } + + /** + * Convert this parameter to a stub that can be used by `tool/make_stubs` + * + * @param bool $is_internal is this being requested for the language server instead of real PHP code? + * @suppress PhanAccessClassConstantInternal + */ + public function toStubString(bool $is_internal = false): string + { + $string = ''; + + $union_type = $this->getNonVariadicUnionType(); + if (!$union_type->isEmpty()) { + $string .= $union_type->__toString() . ' '; + } + + if ($this->isPassByReference()) { + $string .= '&'; + } + + if ($this->isVariadic()) { + $string .= '...'; + } + + $name = $this->getName(); + if (!\preg_match('@' . Builder::WORD_REGEX . '@', $name)) { + // Some PECL extensions have invalid parameter names. + // Replace invalid characters with U+FFFD replacement character. + $name = \preg_replace('@[^a-zA-Z0-9_\x7f-\xff]@', '�', $name); + if (!\preg_match('@' . Builder::WORD_REGEX . '@', $name)) { + $name = '_' . $name; + } + } + + $string .= "\$$name"; + + if ($this->hasDefaultValue() && !$this->isVariadic()) { + $string .= ' = ' . $this->generateDefaultNodeRepresentation($is_internal); + } + + return $string; + } + + private function generateDefaultNodeRepresentation(bool $is_internal = true): string + { + if (is_string($this->default_value_representation)) { + return $this->default_value_representation; + } + if (is_string($this->default_value_constant_name)) { + return '\\' . $this->default_value_constant_name; + } + $default_value = $this->default_value; + if ($default_value instanceof Node) { + $kind = $default_value->kind; + if (\in_array($kind, [ast\AST_CONST, ast\AST_CLASS_CONST, ast\AST_MAGIC_CONST], true)) { + $default_repr = ASTReverter::toShortString($default_value); + } elseif ($kind === ast\AST_NAME) { + $default_repr = (string)$default_value->children['name']; + } elseif ($kind === ast\AST_ARRAY) { + return '[]'; + } else { + return 'unknown'; + } + } else { + $default_repr = StringUtil::varExportPretty($default_value); + } + if (\strtolower($default_repr) === 'null') { + $default_repr = 'null'; + // If we're certain the parameter isn't nullable, + // then render the default as `default`, not `null` + if ($is_internal) { + $union_type = $this->getNonVariadicUnionType(); + if (!$this->default_value_from_reflection && !$union_type->isEmpty() && !$union_type->containsNullable()) { + return 'unknown'; + } + } + } + return $default_repr; + } + + /** + * Convert this parameter to a stub that can be used for issue messages. + * + * @suppress PhanAccessClassConstantInternal + */ + public function getShortRepresentationForIssue(bool $is_internal = false): string + { + $string = ''; + + $union_type_string = $this->getUnionTypeRepresentationForIssue(); + if ($union_type_string !== '') { + $string = $union_type_string . ' '; + } + if ($this->isPassByReference()) { + $string .= '&'; + } + + if ($this->isVariadic()) { + $string .= '...'; + } + + $name = $this->getName(); + if (!\preg_match('@' . Builder::WORD_REGEX . '@', $name)) { + // Some PECL extensions have invalid parameter names. + // Replace invalid characters with U+FFFD replacement character. + $name = \preg_replace('@[^a-zA-Z0-9_\x7f-\xff]@', '�', $name); + if (!\preg_match('@' . Builder::WORD_REGEX . '@', $name)) { + $name = '_' . $name; + } + } + + $string .= "\$$name"; + + if ($this->hasDefaultValue() && !$this->isVariadic()) { + $default_value = $this->default_value; + if ($default_value instanceof Node) { + $kind = $default_value->kind; + if (\in_array($kind, [ast\AST_CONST, ast\AST_CLASS_CONST, ast\AST_MAGIC_CONST], true)) { + $default_repr = ASTReverter::toShortString($default_value); + } elseif ($kind === ast\AST_NAME) { + $default_repr = (string)$default_value->children['name']; + } elseif ($kind === ast\AST_ARRAY) { + $default_repr = '[]'; + } else { + $default_repr = 'unknown'; + } + } else { + $default_repr = StringUtil::varExportPretty($default_value); + if (strlen($default_repr) >= 50) { + $default_repr = 'unknown'; + } + } + if (\strtolower($default_repr) === 'null') { + $default_repr = 'null'; + // If we're certain the parameter isn't nullable, + // then render the default as `unknown`, not `null` + if ($is_internal) { + $union_type = $this->getNonVariadicUnionType(); + if (!$this->default_value_from_reflection && !$union_type->isEmpty() && !$union_type->containsNullable()) { + $default_repr = 'unknown'; + } + } + } + $string .= ' = ' . $default_repr; + } + + return $string; + } + + /** + * Returns a limited length union type representation for issue messages. + * Long types are truncated or omitted. + */ + private function getUnionTypeRepresentationForIssue(): string + { + $union_type = $this->getNonVariadicUnionType()->asNormalizedTypes(); + if ($union_type->isEmpty()) { + return ''; + } + $real_union_type = $union_type->getRealUnionType(); + if ($union_type->typeCount() >= 3) { + if (!$real_union_type->isEmpty()) { + $real_union_type_string = $real_union_type->__toString(); + if (strlen($real_union_type_string) <= 100) { + return $real_union_type_string; + } + } + return ''; + } + + // TODO: hide template types, generic array or real array types + $union_type_string = $union_type->__toString(); + if ($union_type_string === 'mixed') { + return ''; + } + if (strlen($union_type_string) < 100) { + return $union_type_string; + } + $real_union_type_string = $real_union_type->__toString(); + if (strlen($real_union_type_string) <= 100) { + return $real_union_type_string; + } + return ''; + } + + /** + * Converts this to a ClosureDeclarationParameter that can be used in FunctionLikeDeclarationType instances. + * + * E.g. when analyzing code such as `$x = Closure::fromCallable('some_function')`, + * this is used on parameters of `some_function()` to infer the create the parameter types of the inferred type. + */ + public function asClosureDeclarationParameter(): ClosureDeclarationParameter + { + $param_type = $this->getNonVariadicUnionType(); + if ($param_type->isEmpty()) { + $param_type = MixedType::instance(false)->asPHPDocUnionType(); + } + return new ClosureDeclarationParameter( + $param_type, + $this->isVariadic(), + $this->isPassByReference(), + $this->isOptional() + ); + } + + /** + * @param FunctionInterface $function - The function that has this Parameter. + * @return Context a Context with the line number of this parameter + */ + public function createContext(FunctionInterface $function): Context + { + return clone($function->getContext())->withLineNumberStart($this->getFileRef()->getLineNumberStart()); + } + + /** + * Returns true if the non-variadic type of this declared parameter is empty. + * e.g. `$x`, `...$y` + */ + public function hasEmptyNonVariadicType(): bool + { + return self::getUnionType()->isEmpty(); + } + + /** + * Copy the information about default values from $other + */ + public function copyDefaultValueFrom(Parameter $other): void + { + $this->default_value = $other->default_value; + $this->default_value_type = $other->default_value_type; + if ($other->default_value_from_reflection) { + $this->default_value_from_reflection = true; + } + } + + /** + * Sets whether phan should warn if this parameter is provided + * @suppress PhanUnreferencedPublicMethod this may be set by phpdoc comments in the future. + */ + public function setShouldWarnIfProvided(bool $should_warn_if_provided): void + { + $this->should_warn_if_provided = $this->hasDefaultValue() && $should_warn_if_provided; + } + + /** + * Returns true if this should warn if the parameter is provided + */ + public function shouldWarnIfProvided(): bool + { + return $this->should_warn_if_provided; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/PassByReferenceVariable.php b/bundled-libs/phan/phan/src/Phan/Language/Element/PassByReferenceVariable.php new file mode 100644 index 000000000..44b59380d --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/PassByReferenceVariable.php @@ -0,0 +1,166 @@ +parameter = $parameter; + $this->element = $element; + $this->type = $element->getNonVariadicUnionType(); + if ($element instanceof Property) { + $this->code_base = $code_base; + $this->context_of_created_reference = $context_of_created_reference; + } + } + + public function getName(): string + { + return $this->parameter->getName(); + } + + /** + * Variables can't be variadic. This is the same as getUnionType for + * variables, but not necessarily for subclasses. Method will return + * the element type (such as `DateTime`) for variadic parameters. + */ + public function getNonVariadicUnionType(): UnionType + { + return $this->type; + } + + public function getUnionType(): UnionType + { + return $this->type; + } + + /** + * @suppress PhanAccessMethodInternal + */ + public function setUnionType(UnionType $type): void + { + $this->type = $type; + if ($this->element instanceof Property && $this->code_base) { + // TODO: Also warn about incompatible types + AssignmentVisitor::addTypesToPropertyStandalone( + $this->code_base, + $this->element->getContext(), + $this->element, + $type + ); + return; + } + $this->element->setUnionType($type->eraseRealTypeSetRecursively()); + } + + public function getFlags(): int + { + return $this->element->getFlags(); + } + + public function getFlagsHasState(int $bits): bool + { + return $this->element->getFlagsHasState($bits); + } + + public function setFlags(int $flags): void + { + $this->element->setFlags($flags); + } + + public function getPhanFlags(): int + { + return $this->element->getPhanFlags(); + } + + public function getPhanFlagsHasState(int $bits): bool + { + return $this->element->getPhanFlagsHasState($bits); + } + + public function setPhanFlags(int $phan_flags): void + { + $this->element->setPhanFlags($phan_flags); + } + + public function getFileRef(): FileRef + { + return $this->element->getFileRef(); + } + + /** + * Returns the context where this reference was created. + * This is currently only available for references to properties. + */ + public function getContextOfCreatedReference(): ?Context + { + return $this->context_of_created_reference; + } + + /** + * Is the variable/property this is referring to part of a PHP module? + * (only possible for properties) + * @suppress PhanUnreferencedPublicMethod this may be called by plugins or Phan in the future + */ + public function isPHPInternal(): bool + { + return $this->element instanceof AddressableElement && $this->element->isPHPInternal(); + } + + /** + * Get the argument passed in to this object. + * @return TypedElement|UnaddressableTypedElement + */ + public function getElement() + { + return $this->element; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Property.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Property.php new file mode 100644 index 000000000..063b655a3 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Property.php @@ -0,0 +1,524 @@ +getScope(), + $fqsen + ); + $context = $context->withScope($internal_scope); + parent::__construct( + $context, + $name, + $type, + $flags, + $fqsen + ); + + // Presume that this is the original definition + // of this property, and let it be overwritten + // if it isn't. + $this->setDefiningFQSEN($fqsen); + $this->real_defining_fqsen = $fqsen; + $this->real_union_type = $real_union_type; + + // Set an internal scope, so that issue suppressions can be placed on property doc comments. + // (plugins acting on properties would then pick those up). + // $fqsen is used to locate this property. + $this->setInternalScope($internal_scope); + } + + /** + * @return FullyQualifiedPropertyName the FQSEN with the original definition (Even if this is private/protected and inherited from a trait). Used for dead code detection. + * Inheritance tests use getDefiningFQSEN() so that access checks won't break. + * + * @suppress PhanPartialTypeMismatchReturn TODO: Allow subclasses to make property types more specific + */ + public function getRealDefiningFQSEN(): FullyQualifiedPropertyName + { + return $this->real_defining_fqsen ?? $this->getDefiningFQSEN(); + } + + /** + * Returns the visibility for this property (for issue messages and stubs) + */ + public function getVisibilityName(): string + { + if ($this->isPrivate()) { + return 'private'; + } elseif ($this->isProtected()) { + return 'protected'; + } else { + return 'public'; + } + } + + public function __toString(): string + { + $string = $this->getVisibilityName() . ' '; + + if ($this->isStatic()) { + $string .= 'static '; + } + + // Since the UnionType can be a future, and that + // can throw an exception, we catch it and ignore it + try { + $union_type = $this->getUnionType()->__toString(); + if ($union_type !== '') { + $string .= "$union_type "; + } // Don't emit 2 spaces if there is no union type + } catch (\Exception $_) { + // do nothing + } + + $string .= "\${$this->name}"; + + return $string; + } + + /** + * Returns a representation of the visibility for issue messages. + */ + public function asVisibilityAndFQSENString(): string + { + return $this->getVisibilityName() . ' ' . $this->asPropertyFQSENString(); + } + + /** + * Returns a representation of the property's FQSEN for issue messages. + */ + public function asPropertyFQSENString(): string + { + return $this->getClassFQSEN()->__toString() . + ($this->isStatic() ? '::$' : '->') . + $this->name; + } + + /** + * @return string the representation of this FQSEN for issue messages. + * @override + */ + public function getRepresentationForIssue(): string + { + return $this->asPropertyFQSENString(); + } + + /** + * Override the default getter to fill in a future + * union type if available. + * @throws IssueException if getFutureUnionType fails. + */ + public function getUnionType(): UnionType + { + if (null !== ($union_type = $this->getFutureUnionType())) { + $this->setUnionType(parent::getUnionType()->withUnionType($union_type->asNonLiteralType())); + } + + return parent::getUnionType(); + } + + /** + * @return FullyQualifiedPropertyName + * The fully-qualified structural element name of this + * structural element + */ + public function getFQSEN(): FullyQualifiedPropertyName + { + return $this->fqsen; + } + + public function getMarkupDescription(): string + { + $string = $this->getVisibilityName() . ' '; + + if ($this->isStatic()) { + $string .= 'static '; + } + + $string .= "\${$this->name}"; + + return $string; + } + + + /** + * Returns a stub declaration for this property that can be used to build a class stub + * in `tool/make_stubs`. + */ + public function toStub(): string + { + $string = ' ' . $this->getVisibilityName() . ' '; + + if ($this->isStatic()) { + $string .= 'static '; + } + + $string .= "\${$this->name}"; + $string .= ';'; + + return $string; + } + + /** + * Used by daemon mode to restore an element to the state it had before parsing. + * @internal + */ + public function createRestoreCallback(): ?Closure + { + $future_union_type = $this->future_union_type; + if ($future_union_type === null) { + // We already inferred the type for this class constant/global constant. + // Nothing to do. + return null; + } + // If this refers to a class constant in another file, + // the resolved union type might change if that file changes. + return function () use ($future_union_type): void { + $this->future_union_type = $future_union_type; + // Probably don't need to call setUnionType(mixed) again... + }; + } + + /** + * Returns true if at least one of the references to this property was **reading** the property + * + * Precondition: Config::get_track_references() === true + */ + public function hasReadReference(): bool + { + return $this->getPhanFlagsHasState(Flags::WAS_PROPERTY_READ); + } + + public function setHasReadReference(): void + { + $this->enablePhanFlagBits(Flags::WAS_PROPERTY_READ); + } + + /** + * Returns true if at least one of the references to this property was **writing** the property + * + * Precondition: Config::get_track_references() === true + */ + public function hasWriteReference(): bool + { + return $this->getPhanFlagsHasState(Flags::WAS_PROPERTY_WRITTEN); + } + + public function setHasWriteReference(): void + { + $this->enablePhanFlagBits(Flags::WAS_PROPERTY_WRITTEN); + } + + /** + * Copy addressable references from an element of the same subclass + * @override + */ + public function copyReferencesFrom(AddressableElement $element): void + { + if ($this === $element) { + // Should be impossible + return; + } + if (!($element instanceof Property)) { + throw new TypeError('Expected $element to be Phan\Language\Element\Property in ' . __METHOD__); + } + foreach ($element->reference_list as $key => $file_ref) { + $this->reference_list[$key] = $file_ref; + } + if ($element->hasReadReference()) { + $this->setHasReadReference(); + } + if ($element->hasWriteReference()) { + $this->setHasWriteReference(); + } + } + + /** + * @internal + */ + protected const _IS_DYNAMIC_OR_MAGIC = Flags::IS_FROM_PHPDOC | Flags::IS_DYNAMIC_PROPERTY; + + /** + * Equivalent to $this->isDynamicProperty() || $this->isFromPHPDoc() + * i.e. this is a property that is not created from an AST_PROP_ELEM Node. + */ + public function isDynamicOrFromPHPDoc(): bool + { + return ($this->getPhanFlags() & self::_IS_DYNAMIC_OR_MAGIC) !== 0; + } + + /** + * @return bool + * True if this is a magic phpdoc property (declared via (at)property (-read,-write,) on class declaration phpdoc) + */ + public function isFromPHPDoc(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_FROM_PHPDOC); + } + + /** + * @param bool $from_phpdoc - True if this is a magic phpdoc property (declared via (at)property (-read,-write,) on class declaration phpdoc) + * @suppress PhanUnreferencedPublicMethod the caller now just sets all phan flags at once (including IS_READ_ONLY) + */ + public function setIsFromPHPDoc(bool $from_phpdoc): void + { + $this->setPhanFlags( + Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_FROM_PHPDOC, + $from_phpdoc + ) + ); + } + + /** + * Record whether this property contains `static` anywhere in the original union type. + * + * @param bool $has_static + */ + public function setHasStaticInUnionType(bool $has_static): void + { + $this->setPhanFlags( + Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::HAS_STATIC_UNION_TYPE, + $has_static + ) + ); + } + + /** + * Does this property contain `static` anywhere in the original union type? + */ + public function hasStaticInUnionType(): bool + { + return $this->getPhanFlagsHasState(Flags::HAS_STATIC_UNION_TYPE); + } + + /** + * Was this property undeclared (and created at runtime)? + */ + public function isDynamicProperty(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_DYNAMIC_PROPERTY); + } + + /** + * Is this property declared in a way hinting that it should only be written to? + * (E.g. magic properties declared as (at)property-read, regular properties with (at)phan-read-only) + */ + public function isReadOnly(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_READ_ONLY); + } + + /** + * Record whether this property is read-only. + * TODO: Warn about combining IS_READ_ONLY and IS_WRITE_ONLY + */ + public function setIsReadOnly(bool $is_read_only): void + { + $this->setPhanFlags( + Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_READ_ONLY, + $is_read_only + ) + ); + } + + /** + * Is this property declared in a way hinting that it should only be written to? + * (E.g. magic properties declared as (at)property-write, regular properties with (at)phan-write-only) + */ + public function isWriteOnly(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_WRITE_ONLY); + } + + public function setIsDynamicProperty(bool $is_dynamic): void + { + $this->setPhanFlags( + Flags::bitVectorWithState( + $this->getPhanFlags(), + Flags::IS_DYNAMIC_PROPERTY, + $is_dynamic + ) + ); + } + + public function inheritStaticUnionType(FullyQualifiedClassName $old, FullyQualifiedClassName $new): void + { + $union_type = $this->getUnionType(); + foreach ($union_type->getTypeSet() as $type) { + if (!$type->isObjectWithKnownFQSEN()) { + continue; + } + // $type is the name of a class - replace it with $new and preserve nullability. + if (FullyQualifiedClassName::fromType($type) === $old) { + $union_type = $union_type + ->withoutType($type) + ->withType($new->asType()->withIsNullable($type->isNullable())); + } + } + $this->setUnionType($union_type); + } + + /** + * @return UnionType|null + * Get the UnionType from a future union type defined + * on this object or null if there is no future + * union type. + * @override + * @suppress PhanAccessMethodInternal + */ + public function getFutureUnionType(): ?UnionType + { + $future_union_type = $this->future_union_type; + if ($future_union_type === null) { + return null; + } + + // null out the future_union_type before + // we compute it to avoid unbounded + // recursion + $this->future_union_type = null; + + try { + $union_type = $future_union_type->get(); + if (!$this->real_union_type->isEmpty() + && !$union_type->canStrictCastToUnionType($future_union_type->getCodeBase(), $this->real_union_type)) { + Issue::maybeEmit( + $future_union_type->getCodeBase(), + $future_union_type->getContext(), + Issue::TypeMismatchPropertyDefaultReal, + $future_union_type->getContext()->getLineNumberStart(), + $this->real_union_type, + $this->name, + ASTReverter::toShortString($future_union_type->getNode()), + $union_type + ); + } + } catch (IssueException $_) { + $union_type = UnionType::empty(); + } + + // Don't set 'null' as the type if that's the default + // given that its the default. + if ($union_type->isType(NullType::instance(false))) { + $union_type = UnionType::empty(); + } else { + $union_type = $union_type->eraseRealTypeSetRecursively(); + } + + return $union_type->withRealTypeSet($this->real_union_type->getTypeSet()); + } + + public function getRealUnionType(): UnionType + { + return $this->real_union_type; + } + + public function setPHPDocUnionType(UnionType $type): void + { + $this->phpdoc_union_type = $type; + } + + public function getPHPDocUnionType(): UnionType + { + return $this->phpdoc_union_type ?? UnionType::empty(); + } + + /** + * Record the union type of the default value (for declared properties) + */ + public function setDefaultType(UnionType $type): void + { + $this->default_type = $type; + } + + /** + * Return the recorded union type of the default value (for declared properties). + * This is null if there is no declared type. + * (TODO: Consider ways to represent an "undefined" state for php 7.4 typed properties) + */ + public function getDefaultType(): ?UnionType + { + return $this->default_type; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/TraitAdaptations.php b/bundled-libs/phan/phan/src/Phan/Language/Element/TraitAdaptations.php new file mode 100644 index 000000000..651a1c0f6 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/TraitAdaptations.php @@ -0,0 +1,45 @@ + + * maps alias methods from this trait + * to the info about the source method + */ + public $alias_methods = []; + + /** + * @var array Has an entry mapping name to true if a method with a given name is hidden. + */ + public $hidden_methods = []; + + public function __construct(FullyQualifiedClassName $trait_fqsen) + { + $this->trait_fqsen = $trait_fqsen; + } + + /** + * Gets the trait's FQSEN + */ + public function getTraitFQSEN(): FullyQualifiedClassName + { + return $this->trait_fqsen; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/TraitAliasSource.php b/bundled-libs/phan/phan/src/Phan/Language/Element/TraitAliasSource.php new file mode 100644 index 000000000..6528083dd --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/TraitAliasSource.php @@ -0,0 +1,59 @@ +source_method_name = $source_method_name; + $this->alias_lineno = $alias_lineno; + $this->alias_visibility_flags = $alias_visibility_flags; + } + + /** + * Returns the name of the method which this is an alias of. + */ + public function getSourceMethodName(): string + { + return $this->source_method_name; + } + + /** + * Returns the line number where this trait method alias was created + * (in the class using traits). + */ + public function getAliasLineno(): int + { + return $this->alias_lineno; + } + + /** + * Returns the overridden visibility modifier, or 0 if the visibility didn't change + */ + public function getAliasVisibilityFlags(): int + { + return $this->alias_visibility_flags; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/TypedElement.php b/bundled-libs/phan/phan/src/Phan/Language/Element/TypedElement.php new file mode 100644 index 000000000..e73417680 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/TypedElement.php @@ -0,0 +1,333 @@ + + * A set of issues types to be suppressed. + * Maps to the number of times an issue type was suppressed. + */ + private $suppress_issue_list = []; + + /** + * @param Context $context + * The context in which the structural element lives + * + * @param string $name + * The name of the typed structural element + * + * @param UnionType $type + * A '|' delimited set of types satisfied by this + * typed structural element. + * + * @param int $flags + * The flags property contains node specific flags. It is + * always defined, but for most nodes it is always zero. + * ast\kind_uses_flags() can be used to determine whether + * a certain kind has a meaningful flags value. + */ + public function __construct( + Context $context, + string $name, + UnionType $type, + int $flags + ) { + $this->context = clone($context); + $this->name = $name; + $this->type = $type; + $this->flags = $flags; + $this->setIsPHPInternal($context->isPHPInternal()); + } + + /** + * After a clone is called on this object, clone our + * type and fqsen so that they survive copies intact + * + * @return null + */ + public function __clone() + { + $this->context = clone($this->context); + } + + /** + * @return string + * The (not fully-qualified) name of this element. + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return UnionType + * Get the type of this structural element + */ + public function getUnionType(): UnionType + { + return $this->type; + } + + /** + * @param UnionType $type + * Set the type of this element + * + * TODO: A helper addUnionType(), accounting for variadic + */ + public function setUnionType(UnionType $type): void + { + $this->type = $type; + } + + /** + * Variables can't be variadic. This is the same as getUnionType for + * variables, but not necessarily for subclasses. Method will return + * the element type (such as `DateTime`) for variadic parameters. + */ + public function getNonVariadicUnionType(): UnionType + { + return $this->getUnionType(); + } + + public function getFlags(): int + { + return $this->flags; + } + + /** + * @param int $flag + * The flag we'd like to get the state for + * + * @return bool + * True if all bits in the ast\Node flags are enabled in the bit + * vector, else false. + */ + public function getFlagsHasState(int $flag): bool + { + return ($this->flags & $flag) === $flag; + } + + + public function setFlags(int $flags): void + { + $this->flags = $flags; + } + + public function getPhanFlags(): int + { + return $this->phan_flags; + } + + /** + * @param int $flag + * The flag we'd like to get the state for + * + * @return bool + * True if all bits in the Phan flags are enabled in the bit + * vector, else false. + */ + public function getPhanFlagsHasState(int $flag): bool + { + return ($this->phan_flags & $flag) === $flag; + } + + public function setPhanFlags(int $phan_flags): void + { + $this->phan_flags = $phan_flags; + } + + /** + * @param int $bits combination of flags from Flags::* constants to enable + */ + public function enablePhanFlagBits(int $bits): void + { + $this->phan_flags |= $bits; + } + + /** + * @param int $bits combination of flags from Flags::* constants to disable + * @suppress PhanUnreferencedPublicMethod keeping this for consistency + */ + public function disablePhanFlagBits(int $bits): void + { + $this->phan_flags &= (~$bits); + } + + /** + * @return Context + * The context in which this structural element exists + */ + public function getContext(): Context + { + return $this->context; + } + + /** + * @return FileRef + * A reference to where this element was found + */ + public function getFileRef(): FileRef + { + // TODO: Kill the context and make this a pure + // FileRef. + return $this->context; + } + + /** + * @return bool + * True if this element is marked as deprecated + */ + public function isDeprecated(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_DEPRECATED); + } + + /** + * @param bool $is_deprecated + * Set this element as deprecated + */ + public function setIsDeprecated(bool $is_deprecated): void + { + $this->phan_flags = Flags::bitVectorWithState( + $this->phan_flags, + Flags::IS_DEPRECATED, + $is_deprecated + ); + } + + /** + * Set the set of issue names to suppress. + * If the values are 0, the suppressions haven't been used yet. + * + * @param array $suppress_issue_set + */ + public function setSuppressIssueSet(array $suppress_issue_set): void + { + $this->suppress_issue_list = $suppress_issue_set; + } + + /** + * @return array + */ + public function getSuppressIssueList(): array + { + return $this->suppress_issue_list ?: []; + } + + /** + * Increments the number of times $issue_name was suppressed. + */ + public function incrementSuppressIssueCount(string $issue_name): void + { + ++$this->suppress_issue_list[$issue_name]; + } + + /** + * @return bool + * True if this element would like to suppress the given + * issue name + * @see self::checkHasSuppressIssueAndIncrementCount() for the most common usage + */ + public function hasSuppressIssue(string $issue_name): bool + { + return isset($this->suppress_issue_list[$issue_name]); + } + + /** + * @return bool + * True if this element would like to suppress the given + * issue name. + * + * If this is true, this automatically calls incrementSuppressIssueCount. + * Most callers should use this, except for uses similar to UnusedSuppressionPlugin. + */ + public function checkHasSuppressIssueAndIncrementCount(string $issue_name): bool + { + if ($this->hasSuppressIssue($issue_name)) { + $this->incrementSuppressIssueCount($issue_name); + return true; + } + return false; + } + + /** + * @return bool + * True if this was an internal PHP object + */ + public function isPHPInternal(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_PHP_INTERNAL); + } + + private function setIsPHPInternal(bool $is_internal): void + { + $this->phan_flags = Flags::bitVectorWithState( + $this->phan_flags, + Flags::IS_PHP_INTERNAL, + $is_internal + ); + } + + /** + * This method must be called before analysis + * begins. + * @unused-param $code_base + * @suppress PhanUnreferencedPublicMethod not called directly, a future version may remove this. + */ + public function hydrate(CodeBase $code_base): void + { + // Do nothing unless overridden + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/TypedElementInterface.php b/bundled-libs/phan/phan/src/Phan/Language/Element/TypedElementInterface.php new file mode 100644 index 000000000..e0588a012 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/TypedElementInterface.php @@ -0,0 +1,40 @@ +type - getUnionType() is overridden by VariadicParameter + */ + protected $type; + + /** + * @var int + * The flags property contains node specific flags. It is + * always defined, but for most nodes it is always zero. + * ast\kind_uses_flags() can be used to determine whether + * a certain kind has a meaningful flags value. + */ + private $flags = 0; + + /** + * @var int + * This property contains node specific flags that + * are internal to Phan. + */ + private $phan_flags = 0; + + /** + * @param Context $context + * The Context in which the structural element lives. + * + * @param string $name + * The name of the typed structural element + * + * @param UnionType $type + * A '|' delimited set of types satisfied by this + * typed structural element. + * + * @param int $flags + * The flags property contains node specific flags. It is + * always defined, but for most nodes it is always zero. + * ast\kind_uses_flags() can be used to determine whether + * a certain kind has a meaningful flags value. + */ + public function __construct( + FileRef $context, + string $name, + UnionType $type, + int $flags + ) { + if (Config::getValue('record_variable_context_and_scope')) { + // The full Context is being recorded for plugins such as the taint check plugin for wikimedia. Note that + // 1. Fetching record_variable_context_and_scope is inlined for performance + // 2. Phan allows phpdoc parameter types to be more specific than (e.g. subclasses of) real types. + $this->file_ref = $context; + } else { + // Convert the Context to a FileRef, to avoid creating a reference + // cycle that can't be garbage collected) + $this->file_ref = FileRef::copyFileRef($context); + } + $this->name = $name; + $this->type = $type; + $this->flags = $flags; + } + + /** + * @return string + * The (not fully-qualified) name of this element. + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return UnionType + * Get the type of this structural element + * If this is a parameter, get the variadic version seen inside the function. + */ + public function getUnionType(): UnionType + { + return $this->type; + } + + /** + * @return UnionType + * Get the type of this structural element. + * If this is a parameter, get the non-variadic version seen by callers for checking arguments. + * + * @suppress PhanUnreferencedPublicMethod possibly used by PassByReferenceVariable + */ + public function getNonVariadicUnionType(): UnionType + { + return $this->type; + } + + /** + * @param UnionType $type + * Set the type of this element + */ + public function setUnionType(UnionType $type): void + { + $this->type = $type; + } + + protected function convertToNullable(): void + { + // Avoid a redundant clone of nonNullableClone() + $type = $this->type; + if ($type->isEmpty() || $type->containsNullable()) { + return; + } + $this->type = $type->nullableClone(); + } + + /** + * Returns the flags of the node declaring/defining this element. + */ + public function getFlags(): int + { + return $this->flags; + } + + /** + * @param int $flag + * The flag we'd like to get the state for + * + * @return bool + * True if all bits in the ast\Node flags are enabled in the bit + * vector, else false. + */ + public function getFlagsHasState(int $flag): bool + { + return ($this->flags & $flag) === $flag; + } + + + /** + * @param int $flags + * + * @suppress PhanUnreferencedPublicMethod unused, other modifiers are used by Phan right now + */ + public function setFlags(int $flags): void + { + $this->flags = $flags; + } + + /** + * Records the Phan flags for this element + * @see \Phan\Flags + */ + public function getPhanFlags(): int + { + return $this->phan_flags; + } + + /** + * @param int $flag + * The flag we'd like to get the state for + * + * @return bool + * True if all bits in the phan flag are enabled in the bit + * vector, else false. + */ + public function getPhanFlagsHasState(int $flag): bool + { + return ($this->phan_flags & $flag) === $flag; + } + + /** + * @param int $phan_flags + * + * @suppress PhanUnreferencedPublicMethod potentially used in the future + */ + public function setPhanFlags(int $phan_flags): void + { + $this->phan_flags = $phan_flags; + } + + /** + * Enable an individual bit of phan flags. + */ + public function enablePhanFlagBits(int $new_bits): void + { + $this->phan_flags |= $new_bits; + } + + /** + * Disable an individual bit of phan flags. + */ + public function disablePhanFlagBits(int $new_bits): void + { + $this->phan_flags &= (~$new_bits); + } + + /** + * @return FileRef + * A reference to where this element was found. This will return a Context object if + * `record_variable_context_and_scope` is true, and a FileRef otherwise. + */ + public function getFileRef(): FileRef + { + return $this->file_ref; + } + + /** + * Returns whether this element stores Context and Scope. + * @suppress PhanUnreferencedPublicMethod + */ + public function storesContext(): bool + { + return Config::getValue('record_variable_context_and_scope'); + } + + abstract public function __toString(): string; +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/Variable.php b/bundled-libs/phan/phan/src/Phan/Language/Element/Variable.php new file mode 100644 index 000000000..717d7ad40 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/Variable.php @@ -0,0 +1,314 @@ + - Maps from a built in superglobal name to a UnionType spec string. + * The string array keys **can** be integers in edge cases, but those should be rare. + * (e.g. ${4} = 'x'; adds 4 to $GLOBALS. + * And user-provided input of integer representations of strings as keys would also become integers. + */ + public const _BUILTIN_SUPERGLOBAL_TYPES = [ + '_GET' => 'array', + '_POST' => 'array', + '_COOKIE' => 'array', + '_REQUEST' => 'array', + '_SERVER' => 'array', + '_ENV' => 'array', + '_FILES' => 'array>>', // Can have multiple files with the same name. + '_SESSION' => 'array', + 'GLOBALS' => 'array', + 'http_response_header' => 'list|null', // Revisit when we implement sub-block type refining + ]; + + /** + * @var array + * @internal this will be protected in a future release + * + * NOTE: The string array keys of superglobals **can** be integers in edge cases, but those should be rare. + * (e.g. ${4} = 'x'; adds 4 to $GLOBALS. + * And user-provided input of integer representations of strings as keys would also become integers. + */ + public const _BUILTIN_GLOBAL_TYPES = [ + '_GET' => 'array', + '_POST' => 'array', + '_COOKIE' => 'array', + '_REQUEST' => 'array', + '_SERVER' => 'array', + '_ENV' => 'array', + '_FILES' => 'array>|array>|array>>|array>>', // Can have multiple files with the same name. + '_SESSION' => 'array', + 'GLOBALS' => 'array', + 'http_response_header' => 'list|null', // Revisit when we implement sub-block type refining + 'argv' => 'list', + 'argc' => 'int', + ]; + + /** + public function __construct( + Context $context, + string $name, + UnionType $type, + int $flags + ) { + parent::__construct( + $context, + $name, + $type, + $flags + ); + } + */ + + /** + * @return bool + * This will always return false in so far as variables + * cannot be passed by reference. + * @suppress PhanUnreferencedPublicMethod this is added for convenience for plugins + */ + public function isPassByReference(): bool + { + return false; + } + + /** + * @return bool + * This will always return false in so far as variables + * cannot be variadic + * @suppress PhanUnreferencedPublicMethod this may be useful in the future. + */ + public function isVariadic(): bool + { + return false; + } + + /** + * @param Node $node + * An AST_VAR node + * + * @param Context $context + * The context in which the variable is found + * + * @param CodeBase $code_base + * + * @return Variable + * A variable begotten from a node + * + * @throws IssueException + */ + public static function fromNodeInContext( + Node $node, + Context $context, + CodeBase $code_base, + bool $should_check_type = true + ): Variable { + + $variable_name = (new ContextNode( + $code_base, + $context, + $node + ))->getVariableName(); + + $scope = $context->getScope(); + $variable = $scope->getVariableByNameOrNull($variable_name); + if ($variable) { + return clone($variable); + } + + // Get the type of the assignment + $union_type = $should_check_type + ? UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node) + : UnionType::empty(); + + $variable = new Variable( + $context->withLineNumberStart($node->lineno), + $variable_name, + $union_type, + 0 + ); + + return $variable; + } + + /** + * @return bool + * True if the variable with the given name is a + * superglobal + * Implies Variable::isHardcodedGlobalVariableWithName($name) is true + */ + public static function isSuperglobalVariableWithName( + string $name + ): bool { + if (\array_key_exists($name, self::_BUILTIN_SUPERGLOBAL_TYPES)) { + return true; + } + return \in_array($name, Config::getValue('runkit_superglobals'), true); + } + + /** + * Is $name a valid variable identifier? + */ + public static function isValidIdentifier( + string $name + ): bool { + return \preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $name) > 0; + } + + /** + * Returns true for all superglobals and variables in globals_type_map. + */ + public static function isHardcodedGlobalVariableWithName( + string $name + ): bool { + return self::isSuperglobalVariableWithName($name) || + \array_key_exists($name, self::_BUILTIN_GLOBAL_TYPES) || + \array_key_exists($name, Config::getValue('globals_type_map')); + } + + /** + * Returns true for all superglobals (if is_in_global_scope, also for variables in globals_type_map/built in globals) + */ + public static function isHardcodedVariableInScopeWithName( + string $name, + bool $is_in_global_scope + ): bool { + return $is_in_global_scope ? self::isHardcodedGlobalVariableWithName($name) + : self::isSuperglobalVariableWithName($name); + } + + /** + * @return ?UnionType + * Returns UnionType (Possible with empty set) if and only + * if isHardcodedGlobalVariableWithName is true. Returns null + * otherwise. + */ + public static function getUnionTypeOfHardcodedGlobalVariableWithName( + string $name + ): ?UnionType { + if (\array_key_exists($name, self::_BUILTIN_GLOBAL_TYPES)) { + // More efficient than using context. + // Note that global constants can be modified by user code + return UnionType::fromFullyQualifiedPHPDocString(self::_BUILTIN_GLOBAL_TYPES[$name]); + } + + if (\array_key_exists($name, Config::getValue('globals_type_map')) + || \in_array($name, Config::getValue('runkit_superglobals'), true) + ) { + $type_string = Config::getValue('globals_type_map')[$name] ?? ''; + // Want to allow 'resource' or 'mixed' as a type here, + return UnionType::fromStringInContext($type_string, new Context(), Type::FROM_PHPDOC); + } + + return null; + } + + /** + * @return ?UnionType + * Returns UnionType (Possible with empty set) if and only + * if isHardcodedVariableInScopeWithName is true. Returns null + * otherwise. + */ + public static function getUnionTypeOfHardcodedVariableInScopeWithName( + string $name, + bool $is_in_global_scope + ): ?UnionType { + if (\array_key_exists($name, $is_in_global_scope ? self::_BUILTIN_GLOBAL_TYPES : self::_BUILTIN_SUPERGLOBAL_TYPES)) { + // More efficient than using context. + // Note that global constants can be modified by user code + return UnionType::fromFullyQualifiedPHPDocString(self::_BUILTIN_GLOBAL_TYPES[$name]); + } + + if (($is_in_global_scope && \array_key_exists($name, Config::getValue('globals_type_map'))) + || \in_array($name, Config::getValue('runkit_superglobals'), true) + ) { + $type_string = Config::getValue('globals_type_map')[$name] ?? ''; + // Want to allow 'resource' or 'mixed' as a type here, + return UnionType::fromStringInContext($type_string, new Context(), Type::FROM_PHPDOC); + } + + return null; + } + + /** + * Variables can't be variadic. This is the same as + * getUnionType for variables, but not necessarily + * for subclasses. Method will return the element + * type (such as `DateTime`) for variadic parameters. + */ + public function getNonVariadicUnionType(): UnionType + { + return parent::getUnionType(); + } + + /** + * @return static - A clone of this object, where isVariadic() is false + * Used for analyzing the context **inside** of this method + */ + public function cloneAsNonVariadic() + { + return clone($this); + } + + public function __toString(): string + { + $string = ''; + + if (!$this->type->isEmpty()) { + $string .= "{$this->type} "; + } + + return "$string\${$this->getName()}"; + } + + /** + * Returns a representation that can be used to debug issues with union types. + * The representation may change - this should not be used for issue messages, etc. + * @suppress PhanUnreferencedPublicMethod + */ + public function getDebugRepresentation(): string + { + $string = ''; + + if (!$this->type->isEmpty()) { + $string .= "{$this->type->getDebugRepresentation()} "; + } + + return "$string\${$this->getName()}"; + } + + /** + * Determine which issue type should be used when Phan finds an undefined var + * + * @param Context $context + * @param string $variable_name + */ + public static function chooseIssueForUndeclaredVariable(Context $context, string $variable_name): string + { + if ($variable_name === 'this') { + return Issue::UndeclaredThis; + } + + return $context->isInGlobalScope() ? Issue::UndeclaredGlobalVariable : Issue::UndeclaredVariable; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Element/VariadicParameter.php b/bundled-libs/phan/phan/src/Phan/Language/Element/VariadicParameter.php new file mode 100644 index 000000000..e4e92e46e --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Element/VariadicParameter.php @@ -0,0 +1,155 @@ +isCloneOfVariadic()) { + $result->convertToNonVariadic(); + $result->enablePhanFlagBits(Flags::IS_CLONE_OF_VARIADIC); + $result->has_empty_non_variadic_type = $this->hasEmptyNonVariadicType(); + } + return $result; + } + + private function convertToNonVariadic(): void + { + // Avoid a redundant clone of toGenericArray() + $this->type = $this->getUnionType(); + } + + /** + * @return bool - True when this is a non-variadic clone of a variadic parameter. + * (We avoid bugs by adding new types to a variadic parameter if this is cloned.) + * However, error messages still need to convert variadic parameters to a string. + * @override + */ + public function isCloneOfVariadic(): bool + { + return $this->getPhanFlagsHasState(Flags::IS_CLONE_OF_VARIADIC); + } + + /** + * @return bool + * True if this parameter is variadic, i.e. can + * take an unlimited list of parameters and express + * them as an array. + * @override + */ + public function isVariadic(): bool + { + return true; + } + + /** + * @return bool + * True if this is an optional parameter (true because this is variadic) + * @override + */ + public function isOptional(): bool + { + return true; + } + + /** + * @return bool + * True if this is a required parameter (false because this is variadic) + * @override + */ + public function isRequired(): bool + { + return false; + } + + /** + * Returns the Parameter in the form expected by a caller. + * + * If this parameter is variadic (e.g. `DateTime ...$args`), then this + * would return a parameter with the type of the elements (e.g. `DateTime`) + * + * If this parameter is not variadic, returns $this. + * + * @return static + * @override + */ + public function asNonVariadic() + { + // TODO: Is it possible to cache this while maintaining + // correctness? PostOrderAnalysisVisitor clones the + // value to avoid it being reused. + // + // Also, figure out if the cloning still working correctly + // after this PR for fixing variadic args. Create a single + // Parameter instance for analyzing callers of the + // corresponding method/function. + // e.g. $this->getUnionType() is of type T[] + // $this->non_variadic->getUnionType() is of type T + return new Parameter( + // @phan-suppress-next-line PhanTypeMismatchArgument Here it's fine to pass a FileRef + $this->getFileRef(), + $this->getName(), + $this->type, + Flags::bitVectorWithState($this->getFlags(), \ast\flags\PARAM_VARIADIC, false) + ); + } + + /** + * If this Parameter is variadic, calling `getUnionType` + * will return an array type such as `DateTime[]`. This + * method will return the element type (such as `DateTime`) + * for variadic parameters. + */ + public function getNonVariadicUnionType(): UnionType + { + return $this->type; + } + + /** + * If this parameter is variadic (e.g. `DateTime ...$args`), + * then this returns the corresponding array type(s) of $args. + * (e.g. `list`) + * + * NOTE: For analyzing the code within a function, + * code should pass $param->cloneAsNonVariadic() instead. + * Modifying/analyzing the clone should work without any bugs. + * + * TODO(Issue #376) : We will probably want to be able to modify + * the underlying variable, e.g. by creating + * `class UnionTypeGenericArrayView extends UnionType`. + * Otherwise, type inference of `...$args` based on the function + * source will be less effective without phpdoc types. + * + * @override + */ + public function getUnionType(): UnionType + { + if (!$this->isCloneOfVariadic()) { + return parent::getUnionType()->asNonEmptyListTypes(); + } + return $this->type; + } + + public function hasEmptyNonVariadicType(): bool + { + return $this->has_empty_non_variadic_type ?? parent::getUnionType()->isEmpty(); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/ElementContext.php b/bundled-libs/phan/phan/src/Phan/Language/ElementContext.php new file mode 100644 index 000000000..266e8c30e --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/ElementContext.php @@ -0,0 +1,50 @@ +copyPropertiesFrom($element->getContext()); + $this->element = $element; + } + + public function isInElementScope(): bool + { + return true; + } + + /** + * Manually free the element reference to avoid the gc loop of + * Element -> Parameter -> ElementContext -> Element + * + * (Phan runs without garbage collection for performance reasons) + */ + public function freeElementReference(): void + { + $this->element = null; + } + + public function getElementInScope(CodeBase $code_base): TypedElement + { + return $this->element ?? parent::getElementInScope($code_base); + } + + public function isInGlobalScope(): bool + { + return false; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/EmptyUnionType.php b/bundled-libs/phan/phan/src/Phan/Language/EmptyUnionType.php new file mode 100644 index 000000000..a5b8b2288 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/EmptyUnionType.php @@ -0,0 +1,1682 @@ +asPHPDocUnionType(); + } + + /** + * Returns a new union type + * which removes this type from the list of types, + * keeping the keys in a consecutive order. + * + * Each type in $this->type_set occurs exactly once. + * @override + */ + public function withoutType(Type $type): UnionType + { + return $this; + } + + /** + * @return bool + * True if this union type contains the given named + * type. + * @override + */ + public function hasType(Type $type): bool + { + return false; + } + + /** + * Returns a union type which adds the given phpdoc/real types to this type + * @override + */ + public function withUnionType(UnionType $union_type): UnionType + { + return $union_type->eraseRealTypeSetRecursively(); + } + + /** + * @return bool + * True if this type has a type referencing the + * class context in which it exists such as 'self' + * or '$this' + * @override + */ + public function hasSelfType(): bool + { + return false; + } + + /** + * @return bool + * True if this union type has any types that are bool/false/true types + * @override + */ + public function hasTypeInBoolFamily(): bool + { + return false; + } + + /** + * Returns the types for which is_bool($x) would be true. + * + * @return UnionType + * A UnionType with known bool types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function getTypesInBoolFamily(): UnionType + { + return $this; + } + + /** + * @param CodeBase $code_base + * The code base to look up classes against + * + * TODO: Defer resolving the template parameters until parse ends. Low priority. + * + * @return UnionType[] + * A map from template type identifiers to the UnionType + * to replace it with + */ + public function getTemplateParameterTypeMap( + CodeBase $code_base + ): array { + return []; + } + + + /** + * @param UnionType[] $template_parameter_type_map + * A map from template type identifiers to concrete types + * + * @return UnionType + * This UnionType with any template types contained herein + * mapped to concrete types defined in the given map. + */ + public function withTemplateParameterTypeMap( + array $template_parameter_type_map + ): UnionType { + return $this; + } + + /** + * @return bool + * True if this union type has any types that are generic + * types + * @override + */ + public function hasTemplateType(): bool + { + return false; + } + + /** @override */ + public function hasTemplateTypeRecursive(): bool + { + return false; + } + + /** @override */ + public function withoutTemplateTypeRecursive(): UnionType + { + return $this; + } + + /** @override */ + public function eraseTemplatesRecursive(): UnionType + { + return $this; + } + + /** + * @return bool + * True if this union type has any types that have generic + * types + * @override + */ + public function hasTemplateParameterTypes(): bool + { + return false; + } + + /** + * @return bool + * True if this type has a type referencing the + * class context 'static'. + * @override + */ + public function hasStaticType(): bool + { + return false; + } + + /** + * @return UnionType + * A new UnionType with any references to 'static' resolved + * in the given context. + */ + public function withStaticResolvedInContext( + Context $context + ): UnionType { + return $this; + } + + /** + * @return UnionType + * A new UnionType with any references to 'static' resolved + * in the given context. + */ + public function withStaticResolvedInFunctionLike( + FunctionInterface $function + ): UnionType { + return $this; + } + + /** + * @return UnionType + * A new UnionType *plus* any references to 'self' (but not 'static') resolved + * in the given context. + */ + public function withAddedClassForResolvedSelf( + Context $context + ): UnionType { + return $this; + } + + /** + * @return UnionType + * A new UnionType with any references to 'self' (but not 'static') resolved + * in the given context. (the type of 'self' is replaced) + */ + public function withSelfResolvedInContext( + Context $context + ): UnionType { + return $this; + } + + /** + * @return bool + * True if and only if this UnionType contains + * the given type and no others. + * @override + */ + public function isType(Type $type): bool + { + return false; + } + + /** + * @return bool + * True if this UnionType is exclusively native + * types + * @override + */ + public function isNativeType(): bool + { + return false; + } + + /** + * @return bool + * True iff this union contains the exact set of types + * represented in the given union type. + * @override + */ + public function isEqualTo(UnionType $union_type): bool + { + return $union_type instanceof EmptyUnionType || ($union_type->isEmpty() && !$union_type->isPossiblyUndefined()); + } + + /** + * @override + */ + public function isIdenticalTo(UnionType $union_type): bool + { + return $union_type->isEmpty() && !$union_type->isPossiblyUndefined() && !$union_type->getRealTypeSet(); + } + + /** + * @return bool + * True iff this union contains a type that's also in + * the other union type. + */ + public function hasCommonType(UnionType $union_type): bool + { + return false; + } + + /** + * @return bool - True if not empty and at least one type is NullType or nullable. + */ + public function containsNullable(): bool + { + return false; + } + + /** + * @return bool - True if not empty and at least one type is NullType or nullable. + */ + public function containsNullableLabeled(): bool + { + return false; + } + + /** + * @override + */ + public function containsNonMixedNullable(): bool + { + return false; + } + + /** + * @return bool - True if not empty and at least one type is NullType or mixed. + */ + public function containsNullableOrMixed(): bool + { + return false; + } + + /** + * @return bool - True if empty or at least one type is NullType or nullable. + */ + public function containsNullableOrIsEmpty(): bool + { + return true; + } + + public function isNull(): bool + { + return false; + } + + public function isRealTypeNullOrUndefined(): bool + { + return false; + } + + /** + * @return bool - True if not empty, not possibly undefined, and at least one type is NullType or nullable. + */ + public function containsNullableOrUndefined(): bool + { + return false; + } + + /** @override */ + public function nonNullableClone(): UnionType + { + return UnionType::fromFullyQualifiedRealString('non-null-mixed'); + } + + /** @override */ + public function nullableClone(): UnionType + { + return $this; + } + + /** @override */ + public function withNullableRealTypes(): UnionType + { + return $this; + } + + /** @override */ + public function withIsNullable(bool $is_nullable): UnionType + { + return $is_nullable ? $this : $this->nonNullableClone(); + } + + /** + * @return bool - True if type set is not empty and at least one type is NullType or nullable or FalseType or BoolType. + * (I.e. the type is always falsey, or both sometimes falsey with a non-falsey type it can be narrowed down to) + * This does not include values such as `IntType`, since there is currently no `NonZeroIntType`. + * @override + */ + public function containsFalsey(): bool + { + return false; + } + + /** @override */ + public function nonFalseyClone(): UnionType + { + return UnionType::fromFullyQualifiedRealString('non-empty-mixed'); + } + + /** + * @return bool - True if type set is not empty and at least one type is NullType or nullable or FalseType or BoolType. + * (I.e. the type is always falsey, or both sometimes falsey with a non-falsey type it can be narrowed down to) + * This does not include values such as `IntType`, since there is currently no `NonZeroIntType`. + * @override + */ + public function containsTruthy(): bool + { + return false; + } + + /** @override */ + public function nonTruthyClone(): UnionType + { + return $this; + } + + /** + * @return bool - True if type set is not empty and at least one type is BoolType or FalseType + * @override + */ + public function containsFalse(): bool + { + return false; + } + + /** + * @return bool - True if type set is not empty and at least one type is BoolType or TrueType + * @override + */ + public function containsTrue(): bool + { + return false; + } + + public function nonFalseClone(): UnionType + { + return $this; + } + + public function nonTrueClone(): UnionType + { + return $this; + } + + public function isExclusivelyNarrowedFormOf(CodeBase $code_base, UnionType $other): bool + { + return $other->isEmpty(); + } + + /** + * @param Type[] $type_list + * A list of types + * + * @return bool + * True if this union type contains any of the given + * named types + */ + public function hasAnyType(array $type_list): bool + { + return false; + } + + /** + * @return bool + * True if this type has any subtype of `iterable` type (e.g. Traversable, Array). + */ + public function hasIterable(): bool + { + return false; + } + + public function iterableTypesStrictCast(CodeBase $code_base): UnionType + { + return IterableType::instance(false)->asRealUnionType(); + } + + public function countableTypesStrictCast(CodeBase $code_base): UnionType + { + return UnionType::fromFullyQualifiedRealString('array|\Countable'); + } + + public function iterableTypesStrictCastAssumeTraversable(CodeBase $code_base): UnionType + { + return IterableType::instance(false)->asRealUnionType(); + } + + /** + * @return int + * The number of types in this union type + */ + public function typeCount(): int + { + return 0; + } + + /** + * @return bool + * True if this Union has no types + */ + public function isEmpty(): bool + { + return true; + } + + /** + * @return bool + * True if this Union has no types or is the mixed type + */ + public function isEmptyOrMixed(): bool + { + return true; + } + + /** + * @param UnionType $target + * The type we'd like to see if this type can cast + * to + * + * @param CodeBase $code_base + * The code base used to expand types + * + * @return bool + * Test to see if this type can be cast to the + * given type after expanding both union types + * to include all ancestor types + * + * TODO: ensure that this is only called after the parse phase is over. + */ + public function canCastToExpandedUnionType( + UnionType $target, + CodeBase $code_base + ): bool { + return true; // Empty can cast to anything. + } + + /** + * @param UnionType $target + * A type to check to see if this can cast to it + * + * @return bool + * True if this type is allowed to cast to the given type + * i.e. int->float is allowed while float->int is not. + */ + public function canCastToUnionType( + UnionType $target + ): bool { + return true; // Empty can cast to anything. See parent implementation. + } + + public function canCastToUnionTypeWithoutConfig( + UnionType $target + ): bool { + return true; // Empty can cast to anything. See parent implementation. + } + + /** + * Precondition: $this->canCastToUnionType() is false. + * + * This tells us if it would have succeeded if the source type was not nullable. + * + * @internal + * @override + */ + public function canCastToUnionTypeIfNonNull(UnionType $target): bool + { + // TODO: Better check for isPossiblyNonNull + return UnionType::fromFullyQualifiedRealString('non-null-mixed')->canCastToUnionType($target); + } + + public function canCastToUnionTypeHandlingTemplates( + UnionType $target, + CodeBase $code_base + ): bool { + return true; + } + + /** + * @return bool + * True if all types in this union are scalars + */ + public function isScalar(): bool + { + return false; + } + + /** + * @return bool + * True if any types in this union are a printable scalar, or this is the empty union type + */ + public function hasPrintableScalar(): bool + { + return true; + } + + /** + * @return bool + * True if any types in this union are a printable scalar, or this is the empty union type + */ + public function hasValidBitwiseOperand(): bool + { + return true; + } + + /** + * @return bool + * True if this union has array-like types (is of type array, is + * a generic array, or implements ArrayAccess). + */ + public function hasArrayLike(): bool + { + return false; + } + + /** + * @unused-param $code_base + * @override + */ + public function asArrayOrArrayAccessSubTypes(CodeBase $code_base): UnionType + { + return $this; + } + + /** + * @return bool + * True if this union has array-like types (is of type array, is + * a generic array, or implements ArrayAccess). + */ + public function hasGenericArray(): bool + { + return false; + } + + /** + * @return bool + * True if this union contains the ArrayAccess type. + * (Call asExpandedTypes() first to check for subclasses of ArrayAccess) + */ + public function hasArrayAccess(): bool + { + return false; + } + + /** + * @return bool + * True if this union contains the Traversable type. + * (Call asExpandedTypes() first to check for subclasses of Traversable) + */ + public function hasTraversable(): bool + { + return false; + } + + /** + * @return bool + * True if this union type represents types that are + * array-like, and nothing else (e.g. can't be null). + * If any of the array-like types are nullable, this returns false. + */ + public function isExclusivelyArrayLike(): bool + { + return false; + } + + /** + * @return bool + * True if this union type represents types that are arrays + * or generic arrays, but nothing else. + * @override + */ + public function isExclusivelyArray(): bool + { + return false; + } + + /** + * @return UnionType + * Get the subset of types which are not native + */ + public function nonNativeTypes(): UnionType + { + return $this; + } + + /** + * A memory efficient way to create a UnionType from a filter operation. + * If this the filter preserves everything, returns $this instead + */ + public function makeFromFilter(Closure $cb): UnionType + { + return $this; // filtering empty results in empty + } + + /** + * @param Context $context + * The context in which we're resolving this union + * type. + * + * @return Generator + * @suppress PhanTypeMismatchGeneratorYieldValue (deliberate empty stub code) + * + * A list of class FQSENs representing the non-native types + * associated with this UnionType + * + * @throws CodeBaseException + * An exception is thrown if a non-native type does not have + * an associated class + * + * @throws IssueException + * An exception is thrown if static is used as a type outside of an object + * context + * + * TODO: Add a method to ContextNode to directly get FQSEN instead? + * @suppress PhanImpossibleCondition deliberately making a generator yielding nothing + */ + public function asClassFQSENList( + Context $context + ): Generator { + if (false) { + yield; + } + } + + /** + * @param CodeBase $code_base + * The code base in which to find classes + * + * @param Context $context + * The context in which we're resolving this union + * type. + * + * @return Generator + * + * A list of classes representing the non-native types + * associated with this UnionType + * + * @throws CodeBaseException + * An exception is thrown if a non-native type does not have + * an associated class + * + * @throws IssueException + * An exception is thrown if static is used as a type outside of an object + * context + * + * @suppress PhanEmptyYieldFrom this is deliberate + */ + public function asClassList( + CodeBase $code_base, + Context $context + ): Generator { + yield from []; + } + + /** + * Takes "a|b[]|c|d[]|e" and returns "a|c|e" + * + * @return UnionType + * A UnionType with generic array types filtered out + * + * @override + */ + public function nonGenericArrayTypes(): UnionType + { + return $this; + } + + /** + * Takes "a|b[]|c|d[]|e" and returns "b[]|d[]" + * + * @return UnionType + * A UnionType with generic array types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function genericArrayTypes(): UnionType + { + return $this; + } + + /** + * Takes "MyClass|int|array|?object" and returns "MyClass|?object" + * + * @return UnionType + * A UnionType with known object types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function objectTypes(): UnionType + { + return $this; + } + + public function objectTypesStrict(): UnionType + { + return ObjectType::instance(false)->asRealUnionType(); + } + + public function objectTypesStrictAllowEmpty(): UnionType + { + return $this; + } + + /** + * Takes "MyClass|int|array|?object" and returns "MyClass|?object" + * + * @return UnionType + * A UnionType with known object types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function objectTypesWithKnownFQSENs(): UnionType + { + return $this; + } + + /** + * Returns true if objectTypes would be non-empty. + */ + public function hasObjectTypes(): bool + { + return false; + } + + /** + * Returns the types for which is_scalar($x) would be true. + * This means null/nullable is removed. + * Takes "MyClass|int|?bool|array|?object" and returns "int|bool" + * Takes "?MyClass" and returns an empty union type. + * + * @return UnionType + * A UnionType with known scalar types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function scalarTypes(): UnionType + { + return $this; + } + + /** + * Returns the types for which is_callable($x) would be true. + * TODO: Check for __invoke()? + * Takes "Closure|false" and returns "Closure" + * Takes "?MyClass" and returns an empty union type. + * + * @return UnionType + * A UnionType with known callable types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function callableTypes(): UnionType + { + return $this; + } + + /** + * Returns true if this has one or more callable types + * TODO: Check for __invoke()? + * Takes "Closure|false" and returns true + * Takes "?MyClass" and returns false + * + * @return bool + * A UnionType with known callable types kept, other types filtered out. + * + * @see self::callableTypes() + * + * @override + */ + public function hasCallableType(): bool + { + return false; // has no types + } + + /** + * Returns the types for which is_int($x) would be true. + * + * @return UnionType + * A UnionType with known int types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function intTypes(): UnionType + { + return $this; + } + + public function floatTypes(): UnionType + { + return $this; + } + + /** + * Returns the types for which is_string($x) would be true. + * + * @return UnionType + * A UnionType with known string types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function stringTypes(): UnionType + { + return $this; + } + + public function isExclusivelyStringTypes(): bool + { + return true; + } + + /** + * Returns the types for which is_numeric($x) is possibly true. + * + * @return UnionType + * A UnionType with known numeric types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function numericTypes(): UnionType + { + return $this; + } + + /** + * Returns true if every type in this type is callable. + * TODO: Check for __invoke()? + * Takes "callable" and returns true + * Takes "callable|false" and returns false + * + * @return bool + * A UnionType with known callable types kept, other types filtered out. + * + * @see nonGenericArrayTypes + */ + public function isExclusivelyCallable(): bool + { + return true; // !$this->hasTypeMatchingCallback(empty) + } + + /** + * Takes "a|b[]|c|d[]|e|array|ArrayAccess" and returns "a|c|e|ArrayAccess" + * + * @return UnionType + * A UnionType with generic types(as well as the non-generic type "array") + * filtered out. + * + * @see nonGenericArrayTypes + */ + public function nonArrayTypes(): UnionType + { + return $this; + } + + public function arrayTypes(): UnionType + { + return $this; + } + + /** + * @return bool + * True if this is exclusively generic types + */ + public function isGenericArray(): bool + { + return false; // empty + } + + /** + * @return bool + * True if any of the types in this UnionType made $matcher_callback return true + */ + public function hasTypeMatchingCallback(Closure $matcher_callback): bool + { + return false; + } + + public function hasRealTypeMatchingCallback(Closure $matcher_callback): bool + { + return false; + } + + public function hasPhpdocOrRealTypeMatchingCallback(Closure $matcher_callback): bool + { + return false; + } + + /** + * @return bool + * True if all of the types in this UnionType made $matcher_callback return true + */ + public function allTypesMatchCallback(Closure $matcher_callback): bool + { + return true; + } + + /** + * @return Type|false + * Returns the first type in this UnionType made $matcher_callback return true + */ + public function findTypeMatchingCallback(Closure $matcher_callback) + { + return false; // empty, no types + } + + /** + * Takes "a|b[]|c|d[]|e" and returns "b|d" + * + * @return UnionType + * The subset of types in this + */ + public function genericArrayElementTypes(bool $add_real_types = false): UnionType + { + return $this; // empty + } + + /** + * Takes "b|d[]" and returns "b[]|d[][]" + * + * @param int $key_type + * Corresponds to the type of the array keys. Set this to a GenericArrayType::KEY_* constant. + * + * @return UnionType + * The subset of types in this + */ + public function elementTypesToGenericArray(int $key_type): UnionType + { + return $this; + } + + /** + * @param Closure(Type):Type $closure + * A closure mapping `Type` to `Type` + * + * @return UnionType + * A new UnionType with each type mapped through the + * given closure + * @override + */ + public function asMappedUnionType(Closure $closure): UnionType + { + return $this; // empty + } + + public function asMappedListUnionType(Closure $closure): UnionType + { + return $this; // empty + } + + /** + * @param Closure(UnionType):UnionType $closure + * @override + */ + public function withMappedElementTypes(Closure $closure): UnionType + { + return $this; + } + + /** + * @param int $key_type + * Corresponds to the type of the array keys. Set this to a GenericArrayType::KEY_* constant. + * + * @return UnionType + * Get a new type for each type in this union which is + * the generic array version of this type. For instance, + * 'int|float' will produce 'int[]|float[]'. + * + * If $this is an empty UnionType, this method will produce an empty UnionType + */ + public function asGenericArrayTypes(int $key_type): UnionType + { + return $this; // empty + } + + public function asListTypes(): UnionType + { + return $this; + } + + /** + * @return UnionType + * Get a non-empty union type with a new type for each type in this union which is + * the generic array version of this type. For instance, + * 'int|float' will produce 'int[]|float[]'. + * + * If $this is an empty UnionType, this method will produce 'array' + */ + public function asNonEmptyGenericArrayTypes(int $key_type): UnionType + { + static $cache = []; + return ($cache[$key_type] ?? ($cache[$key_type] = MixedType::instance(false)->asGenericArrayType($key_type)->asRealUnionType())); + } + + public function asNonEmptyAssociativeArrayTypes(int $key_type): UnionType + { + static $cache = []; + return ($cache[$key_type] ?? ($cache[$key_type] = AssociativeArrayType::fromElementType(MixedType::instance(false), false, $key_type)->asRealUnionType())); + } + + /** + * @unused-param $can_reduce_size + */ + public function withAssociativeArrays(bool $can_reduce_size): UnionType + { + return $this; + } + + public function withIntegerKeyArraysAsLists(): UnionType + { + return $this; + } + + public function asNonEmptyListTypes(): UnionType + { + static $type = null; + return ($type ?? ($type = ListType::fromElementType(MixedType::instance(false), false)->asRealUnionType())); + } + + /** + * @param CodeBase $code_base + * The code base to use in order to find super classes, etc. + * + * @param int $recursion_depth + * This thing has a tendency to run-away on me. This tracks + * how bad I messed up by seeing how far the expanded types + * go + * + * @return UnionType + * Expands all class types to all inherited classes returning + * a superset of this type. + */ + public function asExpandedTypes( + CodeBase $code_base, + int $recursion_depth = 0 + ): UnionType { + return $this; + } + + /** + * @param CodeBase $code_base + * The code base to use in order to find super classes, etc. + * + * @param int $recursion_depth + * This thing has a tendency to run-away on me. This tracks + * how bad I messed up by seeing how far the expanded types + * go + * + * @return UnionType + * Expands all class types to all inherited classes returning + * a superset of this type. + */ + public function asExpandedTypesPreservingTemplate( + CodeBase $code_base, + int $recursion_depth = 0 + ): UnionType { + return $this; + } + + public function replaceWithTemplateTypes(UnionType $template_union_type): UnionType + { + return $template_union_type->eraseRealTypeSetRecursively(); + } + + public function hasTypeWithFQSEN(Type $other): bool + { + return false; + } + + public function getTypesWithFQSEN(Type $other): UnionType + { + return $this; + } + + /** + * As per the Serializable interface + * + * @return string + * A serialized representation of this type + * + * @see \Serializable + */ + public function serialize(): string + { + return ''; + } + + /** + * @return string + * A human-readable string representation of this union + * type + */ + public function __toString(): string + { + return ''; + } + + /** + * @return UnionType - A normalized version of this union type (May or may not be the same object, if no modifications were made) + * + * The following normalization rules apply + * + * 1. If one of the types is null or nullable, convert all types to nullable and remove "null" from the union type + * 2. If both "true" and "false" (possibly nullable) coexist, or either coexists with "bool" (possibly nullable), + * then remove "true" and "false" + */ + public function asNormalizedTypes(): UnionType + { + return $this; + } + + public function hasTopLevelArrayShapeTypeInstances(): bool + { + return false; + } + + /** @override */ + public function hasArrayShapeOrLiteralTypeInstances(): bool + { + return false; + } + + /** @override */ + public function hasArrayShapeTypeInstances(): bool + { + return false; + } + + /** @override */ + public function hasMixedType(): bool + { + return false; + } + + /** @override */ + public function withFlattenedArrayShapeTypeInstances(): UnionType + { + return $this; + } + + /** @override */ + public function withPossiblyEmptyArrays(): UnionType + { + return $this; + } + + /** @override */ + public function withFlattenedTopLevelArrayShapeTypeInstances(): UnionType + { + return $this; + } + + /** @override */ + public function withFlattenedArrayShapeOrLiteralTypeInstances(): UnionType + { + return $this; + } + + public function hasPossiblyObjectTypes(): bool + { + return false; + } + + public function isExclusivelyBoolTypes(): bool + { + return false; + } + + public function generateUniqueId(): string + { + return ''; + } + + public function hasTopLevelNonArrayShapeTypeInstances(): bool + { + return false; + } + + public function shouldBeReplacedBySpecificTypes(): bool + { + return true; + } + + /** + * @param int|string|float|bool $field_key + */ + public function withoutArrayShapeField($field_key): UnionType + { + return $this; + } + + public function withoutSubclassesOf(CodeBase $code_base, Type $object_type): UnionType + { + return $this; + } + + public function canAnyTypeStrictCastToUnionType(CodeBase $code_base, UnionType $target, bool $allow_casting = true): bool + { + return true; + } + + public function canStrictCastToUnionType(CodeBase $code_base, UnionType $target): bool + { + return true; + } + + public function hasArray(): bool + { + return false; + } + + public function hasClassWithToStringMethod(CodeBase $code_base, Context $context): bool + { + return false; + } + + public function isExclusivelyGenerators(): bool + { + return false; + } + + /** @suppress PhanThrowTypeAbsentForCall */ + public function asGeneratorTemplateType(): Type + { + return Type::fromFullyQualifiedString('\Generator'); + } + + /** + * @unused-param $code_base + * @override + */ + public function iterableKeyUnionType(CodeBase $code_base): UnionType + { + return $this; + } + + /** + * @unused-param $code_base + * @override + */ + public function iterableValueUnionType(CodeBase $code_base): UnionType + { + return $this; + } + + /** + * @return Generator + * @suppress PhanTypeMismatchGeneratorYieldValue (deliberate empty stub code) + * @suppress PhanTypeMismatchGeneratorYieldKey (deliberate empty stub code) + * @suppress PhanImpossibleCondition + */ + public function getReferencedClasses(): Generator + { + if (false) { + yield; + } + } + + public function hasIntType(): bool + { + return false; + } + + public function hasNonNullIntType(): bool + { + return false; + } + + public function isExclusivelyRealFloatTypes(): bool + { + return false; + } + + public function isNonNullIntType(): bool + { + return false; + } + + public function isIntTypeOrNull(): bool + { + return false; + } + + public function isNonNullIntOrFloatType(): bool + { + return false; + } + + public function isNonNullNumberType(): bool + { + return false; + } + + public function hasStringType(): bool + { + return false; + } + + public function hasNonNullStringType(): bool + { + return false; + } + + public function isNonNullStringType(): bool + { + return false; + } + + public function hasLiterals(): bool + { + return false; + } + + public function asNonLiteralType(): UnionType + { + return $this; + } + + public function applyUnaryMinusOperator(): UnionType + { + return UnionType::fromFullyQualifiedRealString('int|float'); + } + + public function applyUnaryBitwiseNotOperator(): UnionType + { + return UnionType::fromFullyQualifiedPHPDocAndRealString('int', 'int|string'); + } + + public function applyUnaryPlusOperator(): UnionType + { + return UnionType::fromFullyQualifiedRealString('int|float'); + } + + public function applyUnaryNotOperator(): UnionType + { + return UnionType::fromFullyQualifiedRealString('bool'); + } + + public function applyBoolCast(): UnionType + { + return UnionType::fromFullyQualifiedRealString('bool'); + } + + /** @return null */ + public function asSingleScalarValueOrNull() + { + return null; + } + + public function asSingleScalarValueOrNullOrSelf() + { + return $this; + } + + public function asValueOrNullOrSelf() + { + return $this; + } + + public function asStringScalarValues(): array + { + return []; + } + + public function asIntScalarValues(): array + { + return []; + } + + public function asScalarValues(bool $strict = false): ?array + { + return []; + } + + public function containsDefiniteNonObjectType(): bool + { + return false; + } + + public function containsDefiniteNonObjectAndNonClassType(): bool + { + return false; + } + + public function containsDefiniteNonCallableType(): bool + { + return false; + } + + public function hasPossiblyCallableType(): bool + { + return true; + } + + public function getTypeAfterIncOrDec(): UnionType + { + return $this; + } + + public function getTemplateTypeExtractorClosure(CodeBase $code_base, TemplateType $template_type): ?Closure + { + return null; + } + + public function usesTemplateType(TemplateType $template_type): bool + { + return false; + } + + public function isVoidType(): bool + { + return false; + } + + public function withRealType(Type $type): UnionType + { + return $type->asRealUnionType(); + } + + public function getRealTypeSet(): array + { + return []; + } + + public function hasRealTypeSet(): bool + { + return false; + } + + public function eraseRealTypeSet(): UnionType + { + return $this; + } + + public function eraseRealTypeSetRecursively(): UnionType + { + return $this; + } + + public function hasAnyTypeOverlap(CodeBase $code_base, UnionType $other): bool + { + return true; + } + + public function hasAnyWeakTypeOverlap(UnionType $other): bool + { + return true; + } + + public function canCastToDeclaredType(CodeBase $code_base, Context $context, UnionType $other): bool + { + return true; + } + + /** + * @param ?list $real_type_set + */ + public function withRealTypeSet(?array $real_type_set): UnionType + { + if (!$real_type_set) { + return $this; + } + return UnionType::of($real_type_set, $real_type_set); + } + + public function getRealUnionType(): UnionType + { + return $this; + } + + public function asRealUnionType(): UnionType + { + return $this; + } + + public function arrayTypesStrictCast(): UnionType + { + return ArrayType::instance(false)->asRealUnionType(); + } + + public function arrayTypesStrictCastAllowEmpty(): UnionType + { + return $this; + } + + public function boolTypes(): UnionType + { + return BoolType::instance(false)->asRealUnionType(); + } + + public function scalarTypesStrict(bool $allow_empty = false): UnionType + { + if ($allow_empty) { + return $this; + } + return UnionType::fromFullyQualifiedRealString('int|float|string|bool'); + } + + public function isExclusivelyRealTypes(): bool + { + return false; + } + + public function getDebugRepresentation(): string + { + return '(empty union type)'; + } + + public function canPossiblyCastToClass(CodeBase $code_base, Type $class_type): bool + { + return true; + } + + public function isExclusivelySubclassesOf(CodeBase $code_base, Type $class_type): bool + { + return false; + } + + /** + * Returns true if this type has types for which `+expr` isn't an integer. + */ + public function hasTypesCoercingToNonInt(): bool + { + return true; + } + + public function isEmptyArrayShape(): bool + { + return false; + } + + public function hasSubtypeOf(UnionType $type): bool + { + return true; + } + + public function isStrictSubtypeOf(CodeBase $code_base, UnionType $type): bool + { + return true; + } + + public function isDefinitelyUndefined(): bool + { + return false; + } + + public function convertUndefinedToNullable(): UnionType + { + return $this; + } + + public function classStringTypes(): UnionType + { + return $this; + } + + public function classStringOrObjectTypes(): UnionType + { + return $this; + } + + /** + * @return Generator no types. + * @suppress PhanImpossibleCondition + * @suppress PhanTypeMismatchGeneratorYieldValue + */ + public function getTypesRecursively(): Generator + { + if (false) { + yield; + } + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/FQSEN.php b/bundled-libs/phan/phan/src/Phan/Language/FQSEN.php new file mode 100644 index 000000000..6eb20570a --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/FQSEN.php @@ -0,0 +1,70 @@ +name = $name; + } + + /** + * @param string $fully_qualified_string + * An FQSEN string like '\Namespace\Class::method' or + * 'Class' or 'Class::method'. + * + * @return static + */ + abstract public static function fromFullyQualifiedString( + string $fully_qualified_string + ); + + /** + * @param string $fqsen_string + * An FQSEN string like '\Namespace\Class::method' or + * 'Class' or 'Class::method'. + * + * @param Context $context + * The context in which the FQSEN string was found + * + * @return static + */ + abstract public static function fromStringInContext( + string $fqsen_string, + Context $context + ); + + /** + * @return string + * The class associated with this FQSEN + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + * The canonical representation of the name of the object. Functions + * and Methods, for instance, lowercase their names. + */ + public static function canonicalName(string $name): string + { + return $name; + } + + /** + * @return string + * The canonical representation of the name of the object, + * for use in array key lookups for singletons, namespace maps, etc. + * This should not be used directly or indirectly in issue output + * If an FQSEN is case-sensitive, this should return $name + */ + public static function canonicalLookupKey(string $name): string + { + return \strtolower($name); + } + + /** + * @return string + * A string representation of this fully-qualified + * structural element name. + */ + abstract public function __toString(): string; + + /** + * @throws Error to prevent accidentally calling this + */ + public function __clone() + { + // We compare and look up FQSENs by their identity + throw new Error("cloning an FQSEN (" . (string)$this . ") is forbidden\n"); + } + + /** + * @throws Error to prevent accidentally calling this + */ + public function serialize(): string + { + // We compare and look up FQSENs by their identity + throw new Error("serializing an FQSEN (" . (string)$this . ") is forbidden\n"); + } + + /** + * @param string $serialized + * @throws Error to prevent accidentally calling this + * @suppress PhanParamSignatureRealMismatchHasNoParamTypeInternal, PhanUnusedSuppression parameter type widening was allowed in php 7.2, signature changed in php 8 + */ + public function unserialize($serialized): void + { + // We compare and look up FQSENs by their identity + throw new Error("unserializing an FQSEN ($serialized) is forbidden\n"); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/FQSEN/Alternatives.php b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/Alternatives.php new file mode 100644 index 000000000..670f3dfde --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/Alternatives.php @@ -0,0 +1,92 @@ +alternate_id; + } + + /** + * @return string + * Get the name of this element with its alternate id + * attached + */ + public function getNameWithAlternateId(): string + { + if ($this->alternate_id) { + return "{$this->getName()},{$this->alternate_id}"; + } + + return $this->getName(); + } + + /** + * @return bool + * True if this is an alternate + */ + public function isAlternate(): bool + { + return (0 !== $this->alternate_id); + } + + /** + * @return static + * A FQSEN with the given alternate_id set + * @suppress PhanTypeMismatchDeclaredReturn (static vs FQSEN) + */ + abstract public function withAlternateId( + int $alternate_id + ); + + /** + * @return static + * Get the canonical (non-alternate) FQSEN associated + * with this FQSEN + * + * @suppress PhanTypeMismatchReturn (Alternatives is a trait, cannot directly implement the FQSEN interface. Related to #800) + */ + public function getCanonicalFQSEN() + { + if ($this->alternate_id === 0) { + return $this; + } + + return $this->withAlternateId(0); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassConstantName.php b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassConstantName.php new file mode 100644 index 000000000..e908bfaf5 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassConstantName.php @@ -0,0 +1,23 @@ +fully_qualified_class_name = + $fully_qualified_class_name; + $this->alternate_id = $alternate_id; + } + + /** + * Construct a fully-qualified class element from the class, + * the element name in the class. + * (and an alternate id to account for duplicate element definitions) + * + * @param FullyQualifiedClassName $fully_qualified_class_name + * The fully qualified class name of the class in which + * this element exists + * + * @param string $name + * A name if one is in scope or the empty string otherwise. + * + * @param int $alternate_id + * An alternate ID for the element for use when + * there are multiple definitions of the element + * + * @return static + * @suppress PhanTypeInstantiateAbstractStatic this error is correct, but this should never be called directly + */ + public static function make( + FullyQualifiedClassName $fully_qualified_class_name, + string $name, + int $alternate_id = 0 + ) { + $name = static::canonicalName($name); + + $key = $fully_qualified_class_name->__toString() . '::' . $name . ',' . $alternate_id . + '|' . static::class; + + static $cache = []; + return $cache[$key] ?? ($cache[$key] = new static( + $fully_qualified_class_name, + $name, + $alternate_id + )); + } + + /** + * @return static + * Get the canonical (non-alternate) FQSEN associated + * with this FQSEN + */ + public function getCanonicalFQSEN() + { + $fully_qualified_class_name = $this->fully_qualified_class_name->getCanonicalFQSEN(); + if (!$this->alternate_id && $fully_qualified_class_name === $this->fully_qualified_class_name) { + return $this; + } + return static::make( + $fully_qualified_class_name, // @phan-suppress-current-line PhanPartialTypeMismatchArgument + $this->name, + 0 + ); + } + + /** + * @param $fully_qualified_string + * An FQSEN string like '\Namespace\Class::methodName' + * + * @return static + * + * @throws InvalidArgumentException if the $fully_qualified_string doesn't have a '::' delimiter + * + * @throws FQSENException if the class or element FQSEN is invalid + */ + public static function fromFullyQualifiedString( + string $fully_qualified_string + ) { + $parts = \explode('::', $fully_qualified_string); + if (\count($parts) !== 2) { + throw new InvalidArgumentException("Fully qualified class element lacks '::' delimiter"); + } + + [ + $fully_qualified_class_name_string, + $name_string + ] = $parts; + + $fully_qualified_class_name = + FullyQualifiedClassName::fromFullyQualifiedString( + $fully_qualified_class_name_string + ); + + // Split off the alternate ID + $parts = \explode(',', $name_string); + $name = $parts[0]; + $alternate_id = (int)($parts[1] ?? 0); + + return static::make( + $fully_qualified_class_name, + $name, + $alternate_id + ); + } + + /** + * @param string $fqsen_string + * An FQSEN string like '\Namespace\Class::methodName' + * + * @param Context $context + * The context in which the FQSEN string was found + * + * @return static + * + * @throws InvalidArgumentException if $fqsen_string is invalid in $context + * + * @throws FQSENException if $fqsen_string is invalid + */ + public static function fromStringInContext( + string $fqsen_string, + Context $context + ) { + $parts = \explode('::', $fqsen_string); + + // Test to see if we have a class defined + if (\count($parts) === 1) { + if (!$context->isInClassScope()) { + throw new InvalidArgumentException("Cannot reference class element without class name when not in class scope."); + } + + $fully_qualified_class_name = $context->getClassFQSEN(); + } else { + if (\count($parts) > 2) { + throw new InvalidArgumentException("Too many '::' in $fqsen_string"); + } + [ + $class_name_string, + $fqsen_string + ] = $parts; + + $fully_qualified_class_name = + FullyQualifiedClassName::fromStringInContext( + $class_name_string, + $context + ); + } + + // Split off the alternate ID + $parts = \explode(',', $fqsen_string); + $name = $parts[0]; + $alternate_id = (int)($parts[1] ?? 0); + + return static::make( + $fully_qualified_class_name, + $name, + $alternate_id + ); + } + + /** + * @return FullyQualifiedClassName + * The fully qualified class name associated with this + * class element. + */ + public function getFullyQualifiedClassName(): FullyQualifiedClassName + { + return $this->fully_qualified_class_name; + } + + /** + * @return static + * A FQSEN with the given alternate_id set + */ + public function withAlternateId( + int $alternate_id + ) { + + if ($alternate_id >= 1000) { + throw new AssertionError("Your alternate IDs have run away"); + } + + return static::make( + $this->fully_qualified_class_name, + $this->name, + $alternate_id + ); + } + + /** + * @return string + * A string representation of the given values + */ + public static function toString( + FullyQualifiedClassName $fqsen, + string $name, + int $alternate_id + ): string { + $fqsen_string = $fqsen->__toString() . '::' . $name; + + if ($alternate_id) { + $fqsen_string .= ",$alternate_id"; + } + + return $fqsen_string; + } + + /** + * @return string + * A string representation of this fully-qualified + * structural element name. + */ + public function __toString(): string + { + return $this->memoize(__METHOD__, function (): string { + return self::toString( + $this->fully_qualified_class_name, + $this->name, + $this->alternate_id + ); + }); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassName.php b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassName.php new file mode 100644 index 000000000..41751371b --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedClassName.php @@ -0,0 +1,107 @@ +asFQSENString() + ); + } + + public const VALID_CLASS_REGEX = '/^\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*$/D'; + + /** + * Asserts that something is a valid class FQSEN in PHPDoc. + * Use this for FQSENs passed in from the analyzed code. + * @suppress PhanUnreferencedPublicMethod + */ + public static function isValidClassFQSEN(string $type): bool + { + return preg_match(self::VALID_CLASS_REGEX, $type) > 0; + } + + /** + * @return Type + * The type of this class + */ + public function asType(): Type + { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall the creation of FullyQualifiedClassName checks for FQSENException. + return Type::fromFullyQualifiedString( + $this->__toString() + ); + } + + /** + * @return UnionType + * The union type of just this class type, as a phpdoc union type + * @suppress PhanUnreferencedPublicMethod + */ + public function asPHPDocUnionType(): UnionType + { + return $this->asType()->asPHPDocUnionType(); + } + + /** + * @return UnionType + * The union type of just this class type, as a real union type + */ + public function asRealUnionType(): UnionType + { + return $this->asType()->asRealUnionType(); + } + + /** + * @return FullyQualifiedClassName + * The FQSEN for \stdClass. + */ + public static function getStdClassFQSEN(): FullyQualifiedClassName + { + return self::memoizeStatic(__METHOD__, static function (): FullyQualifiedClassName { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + return self::fromFullyQualifiedString("\stdClass"); + }); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedConstantName.php b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedConstantName.php new file mode 100644 index 000000000..6db79376c --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedConstantName.php @@ -0,0 +1,14 @@ +hasNamespaceMapFor(static::getNamespaceMapType(), $fqsen_string)) { + return $context->getNamespaceMapFor( + static::getNamespaceMapType(), + $fqsen_string + ); + } + + // For functions we don't use the context's namespace if + // there is no NS on the call. + $namespace = \implode('\\', \array_filter($parts)); + + return static::make( + $namespace, + $name, + $alternate_id + ); + } + + /** + * Generates a deterministic FQSEN for the closure of the passed in node. + * @param Node $node a Node type AST_CLOSURE, within the file $context->getFile() + */ + public static function fromClosureInContext( + Context $context, + Node $node + ): FullyQualifiedFunctionName { + $hash_material = + $context->getFile() . '|' . + $node->lineno . '|' . + $node->children['__declId']; + + $name = 'closure_' . \substr(\md5($hash_material), 0, 12); + + // @phan-suppress-next-line PhanThrowTypeAbsentForCall this is valid + return static::fromStringInContext( + $name, + $context + ); + } + + /** + * @return bool + * True if this FQSEN represents a closure + */ + public function isClosure(): bool + { + return \strncmp('closure_', $this->name, 8) === 0; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedGlobalConstantName.php b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedGlobalConstantName.php new file mode 100644 index 000000000..11a269025 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedGlobalConstantName.php @@ -0,0 +1,32 @@ +namespace = $namespace; + $this->alternate_id = $alternate_id; + } + + /** @internal */ + public const VALID_STRUCTURAL_ELEMENT_REGEX = '/^\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*$/D'; + /** @internal */ + public const VALID_STRUCTURAL_ELEMENT_REGEX_PART = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/D'; + + /** + * Construct a fully-qualified global structural element from a namespace and name. + * + * @param string $namespace + * The namespace in this element's scope + * + * @param string $name + * The name of this structural element (additional namespace prefixes here are properly handled) + * + * @param int $alternate_id + * An alternate ID for the element for use when + * there are multiple definitions of the element + * + * @return static + * + * @throws FQSENException on invalid/empty FQSEN + */ + public static function make( + string $namespace, + string $name, + int $alternate_id = 0 + ) { + // Transfer any relative namespace stuff from the + // name to the namespace. + $name_parts = \explode('\\', $name); + $name = (string)\array_pop($name_parts); + if ($name === '') { + throw new EmptyFQSENException( + "Empty name of fqsen", + \rtrim($namespace, "\\") . "\\" . \implode("\\", \array_merge($name_parts, [$name])) + ); + } + foreach ($name_parts as $i => $part) { + if ($part === '') { + if ($i > 0) { + throw new InvalidFQSENException( + "Invalid part '' of fqsen", + \rtrim($namespace, "\\") . "\\" . \implode('\\', \array_merge(array_slice($name_parts, $i), [$name])) + ); + } + continue; + } + if (!\preg_match(self::VALID_STRUCTURAL_ELEMENT_REGEX_PART, $part)) { + throw new InvalidFQSENException( + "Invalid part '$part' of fqsen", + \rtrim($namespace, "\\") . "\\$part\\" . \implode('\\', \array_merge(array_slice($name_parts, $i), [$name])) + ); + } + if ($namespace === '\\') { + $namespace = '\\' . $part; + } else { + $namespace .= '\\' . $part; + } + } + $namespace = self::cleanNamespace($namespace); + if (!\preg_match(self::VALID_STRUCTURAL_ELEMENT_REGEX, \rtrim($namespace, '\\') . '\\' . $name)) { + throw new InvalidFQSENException("Invalid namespaced name", \rtrim($namespace, '\\') . '\\' . $name); + } + + // use the canonicalName for $name instead of strtolower - Some subclasses(constants) are case-sensitive. + $key = static::class . '|' . + static::toString(\strtolower($namespace), static::canonicalLookupKey($name), $alternate_id); + + $fqsen = self::memoizeStatic($key, static function () use ($namespace, $name, $alternate_id): FullyQualifiedGlobalStructuralElement { + return new static( + $namespace, + $name, + $alternate_id + ); + }); + + return $fqsen; + } + + /** + * @param $fully_qualified_string + * An fully qualified string like '\Namespace\Class' + * + * @return static + * + * @throws FQSENException on failure. + */ + public static function fromFullyQualifiedString( + string $fully_qualified_string + ) { + + $key = static::class . '|' . $fully_qualified_string; + + return self::memoizeStatic( + $key, + /** + * @throws FQSENException + */ + static function () use ($fully_qualified_string): FullyQualifiedGlobalStructuralElement { + // Split off the alternate_id + $parts = \explode(',', $fully_qualified_string); + $fqsen_string = $parts[0]; + $alternate_id = (int)($parts[1] ?? 0); + + $parts = \explode('\\', $fqsen_string); + if ($parts[0] === '') { + \array_shift($parts); + if (\count($parts) === 0) { + throw new EmptyFQSENException("The name cannot be empty", $fqsen_string); + } + } + $name = (string)\array_pop($parts); + + if ($name === '') { + throw new EmptyFQSENException("The name cannot be empty", $fqsen_string); + } + + $namespace = '\\' . \implode('\\', $parts); + if ($namespace !== '\\') { + if (!\preg_match(self::VALID_STRUCTURAL_ELEMENT_REGEX, $namespace)) { + throw new InvalidFQSENException("The namespace $namespace is invalid", $fqsen_string); + } + } elseif (\count($parts) > 0) { + // E.g. from `\\stdClass` with two backslashes + throw new InvalidFQSENException("The namespace cannot have empty parts", $fqsen_string); + } + + return static::make( + $namespace, + $name, + $alternate_id + ); + } + ); + } + + /** + * Construct a fully-qualified global structural element from a namespace and name, + * if it was already constructed. + * + * @param string $namespace + * The namespace in this element's scope + * + * @param string $name + * The name of this structural element (additional namespace prefixes here are properly handled) + * + * @return ?static the FQSEN, if it was loaded + * + * @throws FQSENException on failure. + */ + public static function makeIfLoaded(string $namespace, string $name) + { + $name_parts = \explode('\\', $name); + $name = (string)\array_pop($name_parts); + if ($name === '') { + throw new EmptyFQSENException( + "Empty name of fqsen", + \rtrim($namespace, "\\") . "\\" . \implode("\\", \array_merge($name_parts, [$name])) + ); + } + foreach ($name_parts as $i => $part) { + if ($part === '') { + if ($i > 0) { + throw new InvalidFQSENException( + "Invalid part '' of fqsen", + \rtrim($namespace, "\\") . "\\" . \implode('\\', \array_merge(array_slice($name_parts, $i), [$name])) + ); + } + continue; + } + if (!\preg_match(self::VALID_STRUCTURAL_ELEMENT_REGEX_PART, $part)) { + throw new InvalidFQSENException( + "Invalid part '$part' of fqsen", + \rtrim($namespace, "\\") . "\\$part\\" . \implode('\\', \array_merge(array_slice($name_parts, $i), [$name])) + ); + } + if ($namespace === '\\') { + $namespace = '\\' . $part; + } else { + $namespace .= '\\' . $part; + } + } + $namespace = self::cleanNamespace($namespace); + if (!\preg_match(self::VALID_STRUCTURAL_ELEMENT_REGEX, \rtrim($namespace, '\\') . '\\' . $name)) { + throw new InvalidFQSENException("Invalid namespaced name", \rtrim($namespace, '\\') . '\\' . $name); + } + $key = static::class . '|' . + static::toString(\strtolower($namespace), static::canonicalLookupKey($name), 0); + + try { + return self::memoizeStatic( + $key, + /** + * @throws FQSENException + */ + static function (): self { + // Reuse the exception to save time generating an unused stack trace. + static $exception; + $exception = ($exception ?? new Exception()); + throw $exception; + } + ); + } catch (\Exception $_) { + return null; + } + } + + /** + * @param $fqsen_string + * An FQSEN string like '\Namespace\Class' + * + * @param Context $context + * The context in which the FQSEN string was found + * + * @return static + * + * @throws FQSENException if the $fqsen_string is invalid or empty + */ + public static function fromStringInContext( + string $fqsen_string, + Context $context + ) { + // Check to see if we're fully qualified + if (($fqsen_string[0] ?? '') === '\\') { + return static::fromFullyQualifiedString($fqsen_string); + } + // @phan-suppress-next-line PhanAbstractStaticMethodCallInStatic Do not call fromStringInContext directly on this abstract class + $namespace_map_type = static::getNamespaceMapType(); + if ($namespace_map_type === \ast\AST_CONST && \in_array(\strtolower($fqsen_string), ['true', 'false', 'null'], true)) { + return static::fromFullyQualifiedString($fqsen_string); + } + + // Split off the alternate ID + $parts = \explode(',', $fqsen_string); + $fqsen_string = $parts[0]; + $alternate_id = (int)($parts[1] ?? 0); + + $parts = \explode('\\', $fqsen_string); + // Split the parts into the namespace(0 or more components) and the last name. + $name = \array_pop($parts); + + // @phan-suppress-next-line PhanSuspiciousTruthyString + if (!$name) { + throw new InvalidFQSENException("The name cannot be empty", $fqsen_string); + } + + // Check for a name map + if ($context->hasNamespaceMapFor($namespace_map_type, $fqsen_string)) { + return $context->getNamespaceMapFor( + $namespace_map_type, + $fqsen_string + ); + } + + $namespace = \implode('\\', \array_filter($parts)); + + // n.b.: Functions must override this method because + // they don't prefix the namespace for naked + // calls + if ($namespace === '') { + $namespace = $context->getNamespace(); + } + + return static::make( + $namespace, + $name, + $alternate_id + ); + } + + /** + * @return int + * The namespace map type such as \ast\flags\USE_NORMAL or \ast\flags\USE_FUNCTION + */ + abstract protected static function getNamespaceMapType(): int; + + /** + * @return string + * The namespace associated with this FQSEN + */ + public function getNamespace(): string + { + return $this->namespace; + } + + /** + * @return string + * The namespace+name associated with this FQSEN. + * (e.g. 'ast\parse_code') + */ + public function getNamespacedName(): string + { + if ($this->namespace === '\\') { + return $this->name; + } + return \ltrim($this->namespace, '\\') . '\\' . $this->name; + } + + /** + * @return static a copy of this global structural element with a different namespace + * @suppress PhanUnreferencedPublicMethod + */ + public function withNamespace( + string $namespace + ) { + // @phan-suppress-next-line PhanThrowTypeAbsentForCall the class name was already validated + return static::make( + self::cleanNamespace($namespace), + $this->name, + $this->getAlternateId() + ); + } + + /** + * @return static + * A FQSEN with the given alternate_id set + */ + public function withAlternateId( + int $alternate_id + ) { + if ($this->getAlternateId() === $alternate_id) { + return $this; + } + + if ($alternate_id >= 1000) { + throw new AssertionError("Your alternate IDs have run away"); + } + + // @phan-suppress-next-line PhanThrowTypeAbsentForCall the class name was already validated + return static::make( + $this->namespace, + $this->name, + $alternate_id + ); + } + + /** + * @param string $namespace (can be empty) + * + * @return string + * A cleaned version of the given namespace such that + * its always prefixed with a '\' and never ends in a + * '\', and is the string "\" if there is no namespace. + */ + protected static function cleanNamespace(string $namespace): string + { + if ($namespace === '' + || $namespace === '\\' + ) { + return '\\'; + } + + // Ensure that the first character of the namespace + // is always a '\' + if ($namespace[0] !== '\\') { + $namespace = '\\' . $namespace; + } + + // Ensure that we don't have a trailing '\' on the + // namespace + return \rtrim($namespace, '\\'); + } + + /** + * @return string + * A string representation of this fully-qualified + * structural element name. + */ + public static function toString( + string $namespace, + string $name, + int $alternate_id + ): string { + $fqsen_string = $namespace; + + if ($fqsen_string !== '' && $fqsen_string !== '\\') { + $fqsen_string .= '\\'; + } + + $fqsen_string .= static::canonicalName($name); + + // Append an alternate ID if we need to disambiguate + // multiple definitions + if ($alternate_id) { + $fqsen_string .= ',' . $alternate_id; + } + + return $fqsen_string; + } + + /** @var string|null caches the value of $this->__toString() */ + private $as_string = null; + + /** + * @return string + * A string representation of this fully-qualified + * structural element name. + */ + public function __toString(): string + { + return $this->as_string ?? $this->as_string = static::toString( + $this->namespace, + $this->name, + $this->getAlternateId() + ); + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedMethodName.php b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedMethodName.php new file mode 100644 index 000000000..cd71285e6 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedMethodName.php @@ -0,0 +1,107 @@ + '__call', + '__callstatic' => '__callStatic', + '__clone' => '__clone', + '__construct' => '__construct', + '__debuginfo' => '__debugInfo', + '__destruct' => '__destruct', + '__get' => '__get', + '__invoke' => '__invoke', + '__isset' => '__isset', + '__set' => '__set', + '__set_state' => '__set_state', + '__serialize' => '__serialize', + '__sleep' => '__sleep', + '__tostring' => '__toString', + '__unserialize' => '__unserialize', + '__unset' => '__unset', + '__wakeup' => '__wakeup', + ]; + + public const MAGIC_METHOD_NAME_SET = [ + '__call' => true, + '__callStatic' => true, + '__clone' => true, + '__construct' => true, + '__debugInfo' => true, + '__destruct' => true, + '__get' => true, + '__invoke' => true, + '__isset' => true, + '__serialize' => true, + '__set' => true, + '__set_state' => true, + '__sleep' => true, + '__toString' => true, + '__unserialize' => true, + '__unset' => true, + '__wakeup' => true, + ]; + + /** + * Maps magic method names with known types to those types. + * Excludes values with mixed types. + */ + public const MAGIC_METHOD_TYPE_MAP = [ + '__clone' => 'void', + '__construct' => 'void', + '__debugInfo' => 'array', + '__destruct' => 'void', + '__isset' => 'bool', + '__serialize' => 'array', + '__set' => 'void', + '__set_state' => 'object', + '__sleep' => 'string[]', + '__toString' => 'string', + '__unserialize' => 'void', + '__unset' => 'void', + '__wakeup' => 'void', + ]; + + /** + * A list of magic methods which should have a return type of void + */ + public const MAGIC_VOID_METHOD_NAME_SET = [ + '__clone' => true, + '__construct' => true, + '__destruct' => true, + '__set' => true, + '__unset' => true, + '__unserialize' => true, + '__wakeup' => true, + ]; + + /** + * @return string + * The canonical representation of the name of the object. Functions + * and Methods, for instance, lowercase their names. + */ + public static function canonicalName(string $name): string + { + return self::CANONICAL_NAMES[\strtolower($name)] ?? $name; + } + + /** + * @return bool + * True if this FQSEN represents a closure + */ + public function isClosure(): bool + { + return false; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedPropertyName.php b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedPropertyName.php new file mode 100644 index 000000000..f69f9c97f --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/FQSEN/FullyQualifiedPropertyName.php @@ -0,0 +1,12 @@ +file = $file; + return $context; + } + + /** + * @return string + * The path to the file in which the element is defined + */ + public function getFile(): string + { + return $this->file; + } + + /** + * @return string + * The path of this FileRef's file relative to the project + * root directory + */ + public function getProjectRelativePath(): string + { + return self::getProjectRelativePathForPath($this->file); + } + + /** + * @param string $cwd_relative_path (relative or absolute path) + * @return string + * The path of the file relative to the project root directory for the provided path + * + * @see Config::getProjectRootDirectory() for converting paths to absolute paths + */ + public static function getProjectRelativePathForPath(string $cwd_relative_path): string + { + if ($cwd_relative_path === '') { + return ''; + } + // Get a path relative to the project root + // e.g. if the path is /my-project, then strip the beginning of "/my-project/src/a.php" to "src/a.php" but should not change /my-project-unrelated-src/a.php + // And don't strip subdirectories of the same name, e.g. should convert "/my-project/subdir/my-project/file.php" to "subdir/my-project/file.php" + // And convert "/my-project/.//src/a.php" to "src/a.php" + $path = \realpath($cwd_relative_path) ?: $cwd_relative_path; + $root_directory = Config::getProjectRootDirectory(); + $n = \strlen($root_directory); + if (\strncmp($path, $root_directory, $n) === 0) { + if (\in_array($path[$n] ?? '', [\DIRECTORY_SEPARATOR, '/'], true)) { + $path = (string)\substr($path, $n + 1); + // Strip any extra beginning directory separators + $path = \ltrim($path, '/' . \DIRECTORY_SEPARATOR); + return $path; + } + } + + // Deal with a wide variety of cases + // E.g. the project in question is a symlink, + // or uses directory separators that were converted to Windows directory by the call to realpath. + // (On Windows, 'c:/Project/Xyz/./other' gets normalized to 'C:\Project\Xyz\other' (uppercase drive letter)) + $root_directory_realpath = (string)\realpath($root_directory); + if ($root_directory_realpath !== '' && $root_directory_realpath !== $root_directory) { + $n = \strlen($root_directory_realpath); + if (\strncmp($path, $root_directory_realpath, $n) === 0) { + if (\in_array($path[$n] ?? '', [\DIRECTORY_SEPARATOR, '/'], true)) { + $path = (string)\substr($path, $n + 1); + // Strip any extra beginning directory separators + $path = \ltrim($path, '/' . \DIRECTORY_SEPARATOR); + return $path; + } + } + } + + return $path; + } + + /** + * @return bool + * True if this object is internal to PHP + */ + public function isPHPInternal(): bool + { + return 'internal' === $this->file; + } + + /** + * @return bool + * True if this object refers to the same file and line number. + */ + public function equals(FileRef $other): bool + { + return $this->line_number_start === $other->line_number_start && $this->file === $other->file; + } + + /** + * @param int $line_number + * The starting line number of the element within the file + * + * @return static + * This context with the given line number is returned + */ + public function withLineNumberStart(int $line_number) + { + $this->line_number_start = $line_number; + return $this; + } + + /** + * @param int $line_number + * The starting line number of the element within the file + * + * @return void + * Both this and withLineNumberStart modify the original context. + */ + public function setLineNumberStart(int $line_number): void + { + $this->line_number_start = $line_number; + } + + /** + * @return int + * The starting line number of the element within the file + */ + public function getLineNumberStart(): int + { + return $this->line_number_start; + } + + /** + * @param int $line_number + * The ending line number of the element within the $file + * + * @return static + * This context with the given end line number is returned + */ + public function withLineNumberEnd(int $line_number) + { + $this->line_number_end = $line_number; + return $this; + } + + /** + * Get a string representation of the context + * + * @return string + */ + public function __toString(): string + { + return $this->file . ':' . $this->line_number_start; + } + + public function serialize(): string + { + return $this->__toString(); + } + + /** + * @param string $serialized + * @suppress PhanParamSignatureRealMismatchHasNoParamTypeInternal, PhanUnusedSuppression parameter type widening was allowed in php 7.2, signature changed in php 8 + */ + public function unserialize($serialized): void + { + $map = \explode(':', $serialized); + $this->file = $map[0]; + $this->line_number_start = (int)$map[1]; + $this->line_number_end = (int)($map[2] ?? 0); + } + + /** + * @param FileRef $other - An instance of FileRef or a subclass such as Context + * @return FileRef - A plain file ref, with no other information + */ + public static function copyFileRef(FileRef $other): FileRef + { + $file_ref = new FileRef(); + $file_ref->file = $other->file; + $file_ref->line_number_start = $other->line_number_start; + $file_ref->line_number_end = $other->line_number_end; + return $file_ref; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/FutureUnionType.php b/bundled-libs/phan/phan/src/Phan/Language/FutureUnionType.php new file mode 100644 index 000000000..478f07e9e --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/FutureUnionType.php @@ -0,0 +1,94 @@ +node */ + private $context; + + /** @var Node|string|int|bool|float the node which we will be fetching the type of. */ + private $node; + + /** + * @param CodeBase $code_base + * @param Context $context + * @param Node|string|int|bool|float $node + */ + public function __construct( + CodeBase $code_base, + Context $context, + $node + ) { + $this->code_base = $code_base; + $this->context = $context; + $this->node = $node; + } + + /** + * Force the future to figure out the type of the + * given object or throw an IssueException if it + * is unable to do so + * + * @return UnionType + * The type of the future + * + * @throws IssueException + * An exception is thrown if we are unable to determine + * the type at the time this method is called + */ + public function get(): UnionType + { + $this->context->clearCachedUnionTypes(); + return UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $this->node, + false + ); + } + + /** + * Gets the codebase singleton which created this FutureUnionType. + * (used to resolve class references, constants, etc.) + * @internal (May rethink exposing the codebase in the future) + */ + public function getCodebase(): CodeBase + { + return $this->code_base; + } + + /** + * Gets the context in which this FutureUnionType was created + * (used to resolve class references, constants, etc.) + * @internal (May rethink exposing the codebase in the future) + */ + public function getContext(): Context + { + return $this->context; + } + + /** + * Gets the node which this is based on + * @return Node|string|int|bool|float + */ + public function getNode() + { + return $this->node; + } +} diff --git a/bundled-libs/phan/phan/src/Phan/Language/Internal/ClassDocumentationMap.php b/bundled-libs/phan/phan/src/Phan/Language/Internal/ClassDocumentationMap.php new file mode 100644 index 000000000..d942b8dc8 --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Internal/ClassDocumentationMap.php @@ -0,0 +1,1490 @@ +' => 'documentation', + * + * NOTE: This format will very likely change as information is added and should not be used directly. + * + * Sources of function/method summary info: + * + * 1. docs.php.net's SVN repo or website, and examples (See internal/internalsignatures.php) + * + * See https://secure.php.net/manual/en/copyright.php + * + * The PHP manual text and comments are covered by the [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/legalcode), + * copyright (c) the PHP Documentation Group + * 2. Various websites documenting individual extensions (e.g. php-ast) + * 3. PHPStorm stubs (for anything missing from the above sources) + * + * Available from https://github.com/JetBrains/phpstorm-stubs under the [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0) + * + * + * CONTRIBUTING: + * + * Running `internal/internalstubs.php` can be used to update signature maps + * + * There are no plans for these signatures to diverge from what the above upstream sources contain. + * + * - If the descriptions cause Phan to crash, bug reports are welcome + * - If Phan improperly extracted text from a summary (and this affects multiple signatures), patches fixing the extraction will be accepted. + * - Otherwise, fixes for typos/grammar/inaccuracies in the summary will only be accepted once they are contributed upstream and can be regenerated (e.g. to the svn repo for docs.php.net). + * + * Note that the summaries are used in a wide variety of contexts (e.g. PHP's online documentation), + * and what makes sense for Phan may not make sense for those projects, and vice versa. + */ +return [ +'__PhanMissingTestClass' => 'Missing class name used by Phan for unit tests', +'AMQPBasicProperties' => 'stub class representing AMQPBasicProperties from pecl-amqp', +'AMQPChannel' => 'stub class representing AMQPChannel from pecl-amqp', +'AMQPChannelException' => 'stub class representing AMQPChannelException from pecl-amqp', +'AMQPConnection' => 'stub class representing AMQPConnection from pecl-amqp', +'AMQPConnectionException' => 'stub class representing AMQPConnectionException from pecl-amqp', +'AMQPDecimal' => 'stub class representing AMQPDecimal from pecl-amqp', +'AMQPEnvelope' => 'stub class representing AMQPEnvelope from pecl-amqp', +'AMQPEnvelopeException' => 'stub class representing AMQPEnvelopeException from pecl-amqp', +'AMQPException' => 'stub class representing AMQPException from pecl-amqp', +'AMQPExchange' => 'stub class representing AMQPExchange from pecl-amqp', +'AMQPExchangeException' => 'stub class representing AMQPExchangeException from pecl-amqp', +'AMQPExchangeValue' => 'stub class representing AMQPExchangeValue from pecl-amqp', +'AMQPQueue' => 'stub class representing AMQPQueue from pecl-amqp', +'AMQPQueueException' => 'stub class representing AMQPQueueException from pecl-amqp', +'AMQPTimestamp' => 'stub class representing AMQPTimestamp from pecl-amqp', +'AMQPValueException' => 'stub class representing AMQPExchangeValue from pecl-amqp', +'APCIterator' => 'The `APCIterator` class makes it easier to iterate over large APC caches. This is helpful as it allows iterating over large caches in steps, while grabbing a defined number of entries per lock instance, so it frees the cache locks for other activities rather than hold up the entire cache to grab 100 (the default) entries. Also, using regular expression matching is more efficient as it\'s been moved to the C level.', +'APCUIterator' => 'The `APCUIterator` class makes it easier to iterate over large APCu caches. This is helpful as it allows iterating over large caches in steps, while grabbing a defined number of entries per lock instance, so it frees the cache locks for other activities rather than hold up the entire cache to grab 100 (the default) entries. Also, using regular expression matching is more efficient as it\'s been moved to the C level.', +'AppendIterator' => 'An Iterator that iterates over several iterators one after the other.', +'ArgumentCountError' => '`ArgumentCountError` is thrown when too few arguments are passed to a user-defined function or method.', +'ArithmeticError' => '`ArithmeticError` is thrown when an error occurs while performing mathematical operations. In PHP 7.0, these errors include attempting to perform a bitshift by a negative amount, and any call to `intdiv` that would result in a value outside the possible bounds of an `integer`.', +'ArrayIterator' => 'This iterator allows to unset and modify values and keys while iterating over Arrays and Objects. + +When you want to iterate over the same array multiple times you need to instantiate ArrayObject and let it create ArrayIterator instances that refer to it either by using `foreach` or by calling its getIterator() method manually.', +'ArrayObject' => 'This class allows objects to work as arrays.', +'AssertionError' => '`AssertionError` is thrown when an assertion made via `assert` fails.', +'ast\Metadata' => 'Metadata entry for a single AST kind, as returned by ast\get_metadata().', +'ast\Node' => 'This class describes a single node in a PHP AST.', +'BadFunctionCallException' => 'Exception thrown if a callback refers to an undefined function or if some arguments are missing.', +'BadMethodCallException' => 'Exception thrown if a callback refers to an undefined method or if some arguments are missing.', +'CachingIterator' => 'This object supports cached iteration over another iterator.', +'Cairo' => 'Simple class with some static helper methods.', +'CairoAntialias' => 'Enum class that specifies the type of antialiasing to do when rendering text or shapes.', +'CairoContent' => '`CairoContent` is used to describe the content that a surface will contain, whether color information, alpha information (translucence vs. opacity), or both. + +Note: The large values here are designed to keep `CairoContent` values distinct from `CairoContent` values so that the implementation can detect the error if users confuse the two types.', +'CairoContext' => 'Context is the main object used when drawing with cairo. To draw with cairo, you create a `CairoContext`, set the target `CairoSurface`, and drawing options for the `CairoContext`, create shapes with functions . like `CairoContext::moveTo` and `CairoContext::lineTo`, and then draw shapes with `CairoContext::stroke` or `CairoContext::fill`. Contexts can be pushed to a stack via `CairoContext::save`. They may then safely be changed, without losing the current state. Use `CairoContext::restore` to restore to the saved state.', +'CairoException' => '`Exception` class thrown by `Cairo` functions and methods', +'CairoFillRule' => 'A `CairoFillRule` is used to select how paths are filled. For both fill rules, whether or not a point is included in the fill is determined by taking a ray from that point to infinity and looking at intersections with the path. The ray can be in any direction, as long as it doesn\'t pass through the end point of a segment or have a tricky intersection such as intersecting tangent to the path. (Note that filling is not actually implemented in this way. This is just a description of the rule that is applied.) + +The default fill rule is `CairoFillRule::WINDING`.', +'CairoFilter' => 'A `CairoFilter` is used to indicate what filtering should be applied when reading pixel values from patterns. See `CairoPattern::setSource` or `cairo_pattern_set_source` for indicating the desired filter to be used with a particular pattern.', +'CairoFontFace' => 'CairoFontFace abstract class represents a particular font at a particular weight, slant, and other characteristic but no transformation or size. + +Note: This class can not be instantiated directly, it is created by `CairoContext::getFontFace` or `cairo_scaled_font_get_font_face`.', +'CairoFontOptions' => 'An opaque structure holding all options that are used when rendering fonts. + +Individual features of a cairo_font_options_t can be set or accessed using functions named cairo_font_options_set_feature_name and cairo_font_options_get_feature_name, like cairo_font_options_set_antialias() and cairo_font_options_get_antialias(). + +New features may be added to `CairoFontOptions` in the future. For this reason `CairoFontOptions::copy`, `CairoFontOptions::equal`, `CairoFontOptions::merge`, `CairoFontOptions::hash` (cairo_font_options_copy(), cairo_font_options_equal(), cairo_font_options_merge(), and cairo_font_options_hash() in procedural way) should be used to copy, check for equality, merge, or compute a hash value of `CairoFontOptions` objects.', +'CairoFontSlant' => 'Specifies variants of a font face based on their slant.', +'CairoFontType' => 'CairoFontType class is an abstract final class that contains constants used to describe the type of a given `CairoFontFace` or `CairoScaledFont`. The font types are also known as "font backends" within cairo. + +The type of a CairoFontFace is determined by the how it is created, an example would be the `CairoToyFontFace::__construct`. The `CairoFontFace` type can be queried with `CairoFontFace::getType` or `cairo_font_face_get_type` + +The various `CairoFontFace` functions can be used with a font face of any type. + +The type of a `CairoScaledFont` is determined by the type of the `CairoFontFace` passed to `CairoScaledFont::__construct` or `cairo_scaled_font_create`. The scaled font type can be queried with `CairoScaledFont::getType` or `cairo_scaled_font_get_type`.', +'CairoFontWeight' => 'Specifies variants of a font face based on their weight.', +'CairoFormat' => 'CairoFormat enums are used to identify the memory format of the image data.', +'CairoGradientPattern' => '`CairoGradientPattern` is an abstract base class from which other Pattern classes derive. It cannot be instantiated directly.', +'CairoHintMetrics' => 'Specifies whether to hint font metrics; hinting font metrics means quantizing them so that they are integer values in device space. Doing this improves the consistency of letter and line spacing, however it also means that text will be laid out differently at different zoom factors.', +'CairoHintStyle' => 'Specifies the type of hinting to do on font outlines. Hinting is the process of fitting outlines to the pixel grid in order to improve the appearance of the result. Since hinting outlines involves distorting them, it also reduces the faithfulness to the original outline shapes. Not all of the outline hinting styles are supported by all font backends.', +'CairoImageSurface' => 'CairoImageSurface provide the ability to render to memory buffers either allocated by cairo or by the calling code. The supported image formats are those defined in `CairoFormat`.', +'CairoLinearGradient' => 'Create a new CairoLinearGradient along the line defined', +'CairoLineCap' => 'Specifies how to render the endpoints of the path when stroking. + +The default line cap style is `CairoLineCap::BUTT`.', +'CairoMatrix' => 'Matrices are used throughout cairo to convert between different coordinate spaces.', +'CairoOperator' => 'This is used to set the compositing operator for all cairo drawing operations. + +The default operator is `CairoOperator::OVER` + +The operators marked as unbounded modify their destination even outside of the mask layer (that is, their effect is not bound by the mask layer). However, their effect can still be limited by way of clipping. + +To keep things simple, the operator descriptions here document the behavior for when both source and destination are either fully transparent or fully opaque. The actual implementation works for translucent layers too. For a more detailed explanation of the effects of each operator, including the mathematical definitions, see http://cairographics.org/operators/.', +'CairoPath' => 'Note: CairoPath class cannot be instantiated directly, doing so will result in Fatal Error if used or passed', +'CairoPattern' => '`CairoPattern` is the abstract base class from which all the other pattern classes derive. It cannot be instantiated directly', +'CairoPatternType' => '`CairoPatternType` is used to describe the type of a given pattern. + +The type of a pattern is determined by the function used to create it. The `cairo_pattern_create_rgb` and `cairo_pattern_create_rgba` functions create `CairoPatternType::SOLID` patterns. The remaining cairo_pattern_create_* functions map to pattern types in obvious ways.', +'CairoStatus' => '`CairoStatus` is used to indicate errors that can occur when using Cairo. In some cases it is returned directly by functions. but when using `CairoContext`, the last error, if any, is stored in the object and can be retrieved with `CairoContext::status` or `cairo_status`. New entries may be added in future versions. + +Use `Cairo::statusToString` or `cairo_status_to_string` to get a human-readable representation of an error message.', +'CairoSurface' => 'This is the base-class for all other Surface types. CairoSurface is the abstract type representing all different drawing targets that cairo can render to. The actual drawings are performed using a CairoContext.', +'CairoSvgSurface' => 'Svg specific surface class, uses the SVG (standard vector graphics) surface backend.', +'CairoToyFontFace' => 'The `CairoToyFontFace` class can be used instead of `CairoContext::selectFontFace` to create a toy font independently of a context.', +'Cassandra' => 'The main entry point to the PHP Driver for Apache Cassandra. + +Use Cassandra::cluster() to build a cluster instance. +Use Cassandra::ssl() to build SSL options instance.', +'Cassandra\Aggregate' => 'A PHP representation of an aggregate', +'Cassandra\BatchStatement' => 'Batch statements are used to execute a series of simple or prepared +statements. + +There are 3 types of batch statements: + * `Cassandra::BATCH_LOGGED` - this is the default batch type. This batch + guarantees that either all or none of its statements will be executed. + This behavior is achieved by writing a batch log on the coordinator, + which slows down the execution somewhat. + * `Cassandra::BATCH_UNLOGGED` - this batch will not be verified when + executed, which makes it faster than a `LOGGED` batch, but means that + some of its statements might fail, while others - succeed. + * `Cassandra::BATCH_COUNTER` - this batch is used for counter updates, + which are, unlike other writes, not idempotent.', +'Cassandra\Bigint' => 'A PHP representation of the CQL `bigint` datatype', +'Cassandra\Blob' => 'A PHP representation of the CQL `blob` datatype', +'Cassandra\Cluster' => 'Cluster object is used to create Sessions.', +'Cassandra\Cluster\Builder' => 'Cluster builder allows fluent configuration of the cluster instance.', +'Cassandra\Collection' => 'A PHP representation of the CQL `list` datatype', +'Cassandra\Column' => 'A PHP representation of a column', +'Cassandra\Custom' => 'A class for representing custom values.', +'Cassandra\Date' => 'A PHP representation of the CQL `date` type.', +'Cassandra\Decimal' => 'A PHP representation of the CQL `decimal` datatype + +The actual value of a decimal is `$value * pow(10, $scale * -1)`', +'Cassandra\DefaultAggregate' => 'A PHP representation of an aggregate', +'Cassandra\DefaultCluster' => 'Default cluster implementation.', +'Cassandra\DefaultColumn' => 'A PHP representation of a column', +'Cassandra\DefaultFunction' => 'A PHP representation of a public function', +'Cassandra\DefaultIndex' => 'A PHP representation of an index', +'Cassandra\DefaultKeyspace' => 'A PHP representation of a keyspace', +'Cassandra\DefaultMaterializedView' => 'A PHP representation of a materialized view', +'Cassandra\DefaultSchema' => 'A PHP representation of a schema', +'Cassandra\DefaultSession' => 'A session is used to prepare and execute statements.', +'Cassandra\DefaultTable' => 'A PHP representation of a table', +'Cassandra\Duration' => 'A PHP representation of the CQL `duration` datatype', +'Cassandra\Exception' => 'An interface implemented by all exceptions thrown by the PHP Driver. +Makes it easy to catch all driver-related exceptions using +`catch (Exception $e)`.', +'Cassandra\Exception\AlreadyExistsException' => 'AlreadyExistsException is raised when attempting to re-create existing keyspace.', +'Cassandra\Exception\AuthenticationException' => 'AuthenticationException is raised when client was not configured with valid +authentication credentials.', +'Cassandra\Exception\ConfigurationException' => 'ConfigurationException is raised when query is syntactically correct but +invalid because of some configuration issue. +For example when attempting to drop a non-existent keyspace.', +'Cassandra\Exception\DivideByZeroException' => 'Cassandra domain exception.', +'Cassandra\Exception\DomainException' => 'Cassandra domain exception.', +'Cassandra\Exception\ExecutionException' => 'ExecutionException is raised when something went wrong during request execution.', +'Cassandra\Exception\InvalidArgumentException' => 'Cassandra invalid argument exception.', +'Cassandra\Exception\InvalidQueryException' => 'InvalidQueryException is raised when query is syntactically correct but invalid. +For example when attempting to create a table without specifying a keyspace.', +'Cassandra\Exception\InvalidSyntaxException' => 'InvalidSyntaxException is raised when CQL in the request is syntactically incorrect.', +'Cassandra\Exception\IsBootstrappingException' => 'IsBootstrappingException is raised when a node is bootstrapping.', +'Cassandra\Exception\LogicException' => 'Cassandra logic exception.', +'Cassandra\Exception\OverloadedException' => 'OverloadedException is raised when a node is overloaded.', +'Cassandra\Exception\ProtocolException' => 'ProtocolException is raised when a client did not follow server\'s protocol, +e.g. sending a QUERY message before STARTUP. Seeing this error can be +considered a bug.', +'Cassandra\Exception\RangeException' => 'Cassandra domain exception.', +'Cassandra\Exception\ReadTimeoutException' => 'ReadTimeoutException is raised when a coordinator failed to receive acks +from the required number of replica nodes in time during a read.', +'Cassandra\Exception\RuntimeException' => 'Cassandra runtime exception.', +'Cassandra\Exception\ServerException' => 'ServerException is raised when something unexpected happened on the server. +This exception is most likely due to a server-side bug. +**NOTE** This exception and all its children are generated on the server.', +'Cassandra\Exception\TimeoutException' => 'TimeoutException is generally raised when a future did not resolve +within a given time interval.', +'Cassandra\Exception\TruncateException' => 'TruncateException is raised when something went wrong during table +truncation.', +'Cassandra\Exception\UnauthorizedException' => 'UnauthorizedException is raised when the current user doesn\'t have +sufficient permissions to access data.', +'Cassandra\Exception\UnavailableException' => 'UnavailableException is raised when a coordinator detected that there aren\'t +enough replica nodes available to fulfill the request. + +NOTE: Request has not even been forwarded to the replica nodes in this case.', +'Cassandra\Exception\UnpreparedException' => 'UnpreparedException is raised when a given prepared statement id does not +exist on the server. The driver should be automatically re-preparing the +statement in this case. Seeing this error could be considered a bug.', +'Cassandra\Exception\ValidationException' => 'ValidationException is raised on invalid request, before even attempting to +execute it.', +'Cassandra\Exception\WriteTimeoutException' => 'WriteTimeoutException is raised when a coordinator failed to receive acks +from the required number of replica nodes in time during a write.', +'Cassandra\ExecutionOptions' => 'Request execution options.', +'Cassandra\Float_' => 'A PHP representation of the CQL `float` datatype', +'Cassandra\Function_' => 'A PHP representation of a function', +'Cassandra\Future' => 'Futures are returns from asynchronous methods.', +'Cassandra\FutureClose' => 'A future returned from Session::closeAsync().', +'Cassandra\FuturePreparedStatement' => 'A future returned from `Session::prepareAsync()` +This future will resolve with a PreparedStatement or an exception.', +'Cassandra\FutureRows' => 'This future results is resolved with Rows.', +'Cassandra\FutureSession' => 'A future that resolves with Session.', +'Cassandra\FutureValue' => 'A future that always resolves in a value.', +'Cassandra\Index' => 'A PHP representation of an index', +'Cassandra\Inet' => 'A PHP representation of the CQL `inet` datatype', +'Cassandra\Keyspace' => 'A PHP representation of a keyspace', +'Cassandra\Map' => 'A PHP representation of the CQL `map` datatype', +'Cassandra\MaterializedView' => 'A PHP representation of a materialized view', +'Cassandra\Numeric' => 'Common interface implemented by all numeric types, providing basic +arithmetic functions.', +'Cassandra\PreparedStatement' => 'Prepared statements are faster to execute because the server doesn\'t need +to process a statement\'s CQL during the execution. + +With token-awareness enabled in the driver, prepared statements are even +faster, because they are sent directly to replica nodes and avoid the extra +network hop.', +'Cassandra\RetryPolicy' => 'Interface for retry policies.', +'Cassandra\RetryPolicy\DefaultPolicy' => 'The default retry policy. This policy retries a query, using the +request\'s original consistency level, in the following cases: + +* On a read timeout, if enough replicas replied but the data was not received. +* On a write timeout, if a timeout occurs while writing a distributed batch log. +* On unavailable, it will move to the next host. + +In all other cases the error will be returned.', +'Cassandra\RetryPolicy\DowngradingConsistency' => 'A retry policy that will downgrade the consistency of a request in +an attempt to save a request in cases where there is any chance of success. A +write request will succeed if there is at least a single copy persisted and a +read request will succeed if there is some data available even if it increases +the risk of reading stale data. This policy will retry in the same scenarios as +the default policy, and it will also retry in the following case: + +* On a read timeout, if some replicas responded but is lower than + required by the current consistency level then retry with a lower + consistency level +* On a write timeout, Retry unlogged batches at a lower consistency level + if at least one replica responded. For single queries and batch if any + replicas responded then consider the request successful and swallow the + error. +* On unavailable, retry at a lower consistency if at lease one replica + responded. + +Important: This policy may attempt to retry requests with a lower +consistency level. Using this policy can break consistency guarantees.', +'Cassandra\RetryPolicy\Fallthrough' => 'A retry policy that never retries and allows all errors to fallthrough.', +'Cassandra\RetryPolicy\Logging' => 'A retry policy that logs the decisions of its child policy.', +'Cassandra\Rows' => 'Rows represent a result of statement execution.', +'Cassandra\Schema' => 'A PHP representation of a schema', +'Cassandra\Session' => 'A session is used to prepare and execute statements.', +'Cassandra\Set' => 'A PHP representation of the CQL `set` datatype', +'Cassandra\SimpleStatement' => 'Simple statements can be executed using a Session instance. +They are constructed with a CQL string that can contain positional +argument markers `?`. + +NOTE: Positional argument are only valid for native protocol v2+.', +'Cassandra\Smallint' => 'A PHP representation of the CQL `smallint` datatype.', +'Cassandra\SSLOptions' => 'SSL options for Cluster.', +'Cassandra\SSLOptions\Builder' => 'SSLOptions builder allows fluent configuration of ssl options.', +'Cassandra\Statement' => 'All statements implement this common interface.', +'Cassandra\Table' => 'A PHP representation of a table', +'Cassandra\Time' => 'A PHP representation of the CQL `time` type.', +'Cassandra\Timestamp' => 'A PHP representation of the CQL `timestamp` datatype', +'Cassandra\TimestampGenerator' => 'Interface for timestamp generators.', +'Cassandra\TimestampGenerator\Monotonic' => 'A timestamp generator that generates monotonically increasing timestamps +client-side. The timestamps generated have a microsecond granularity with +the sub-millisecond part generated using a counter. The implementation +guarantees that no more than 1000 timestamps will be generated for a given +clock tick even if shared by multiple session objects. If that rate is +exceeded then a warning is logged and timestamps stop incrementing until +the next clock tick.', +'Cassandra\TimestampGenerator\ServerSide' => 'A timestamp generator that allows the server-side to assign timestamps.', +'Cassandra\Timeuuid' => 'A PHP representation of the CQL `timeuuid` datatype', +'Cassandra\Tinyint' => 'A PHP representation of the CQL `tinyint` datatype.', +'Cassandra\Tuple' => 'A PHP representation of the CQL `tuple` datatype', +'Cassandra\Type' => 'Cluster object is used to create Sessions.', +'Cassandra\Type\Collection' => 'A class that represents the list type. The list type contains the type of the +elements contain in the list.', +'Cassandra\Type\Custom' => 'A class that represents a custom type.', +'Cassandra\Type\Map' => 'A class that represents the map type. The map type contains two types that +represents the types of the key and value contained in the map.', +'Cassandra\Type\Scalar' => 'A class that represents a primitive type (e.g. `varchar` or `bigint`)', +'Cassandra\Type\Set' => 'A class that represents the set type. The set type contains the type of the +elements contain in the set.', +'Cassandra\Type\Tuple' => 'A class that represents the tuple type. The tuple type is able to represent +a composite type of one or more types accessed by index.', +'Cassandra\Type\UserType' => 'A class that represents a user type. The user type is able to represent a +composite type of one or more types accessed by name.', +'Cassandra\UserTypeValue' => 'A PHP representation of the CQL UDT datatype', +'Cassandra\Uuid' => 'A PHP representation of the CQL `uuid` datatype', +'Cassandra\UuidInterface' => 'A PHP representation of the CQL `uuid` datatype', +'Cassandra\Value' => 'Common interface implemented by all Cassandra value types.', +'Cassandra\Varint' => 'A PHP representation of the CQL `varint` datatype', +'chdb' => 'Represents a loaded chdb file.', +'classObj' => 'Class Objects can be returned by the `layerObj`_ class, or can be +created using:', +'clusterObj' => 'Instance of clusterObj is always embedded inside the `layerObj`_.', +'Collator' => 'Provides string comparison capability with support for appropriate locale-sensitive sort orderings.', +'Collectable' => 'Represents a garbage-collectable object.', +'colorObj' => 'Instances of colorObj are always embedded inside other classes.', +'COM' => 'The COM class allows you to instantiate an OLE compatible COM object and call its methods and access its properties.', +'com_exception' => 'This extension will throw instances of the class com_exception whenever there is a potentially fatal error reported by COM. All COM exceptions have a well-defined code property that corresponds to the HRESULT return value from the various COM operations. You may use this code to make programmatic decisions on how to handle the exception.', +'CommonMark\CQL' => 'CommonMark Query Language is a DSL for describing how to travel through a CommonMark Node tree implemented as a parser and compiler for a small set of instructions, and a virtual machine for executing those instructions.', +'CommonMark\Node' => 'Represents an Abstract Node, this final abstract is not for direct use by the programmer.', +'CommonMark\Parser' => 'Provides an incremental parser as an alternative to the simple Parsing API function', +'COMPersistHelper' => '`COMPersistHelper` improves the interoperability of COM and PHP with regard to the `php.ini` directive open_basedir, and stream `resource`s.', +'CompileError' => '`CompileError` is thrown for some compilation errors, which formerly issued a fatal error.', +'Componere\Abstract\Definition' => 'This final abstract represents a class entry, and should not be used by the programmer.', +'Componere\Definition' => 'The Definition class allows the programmer to build and register a type at runtime. + +Should a Definition replace an existing class, the existing class will be restored when the Definition is destroyed.', +'Componere\Method' => 'A Method represents a function with modifiable accessibility flags', +'Componere\Patch' => 'The Patch class allows the programmer to change the type of an instance at runtime without registering a new Definition + +When a Patch is destroyed it is reverted, so that instances that were patched during the lifetime of the Patch are restored to their formal type.', +'Componere\Value' => 'A Value represents a PHP variable of all types, including undefined', +'Cond' => 'The static methods contained in the Cond class provide direct access to Posix Condition Variables.', +'Couchbase\AnalyticsQuery' => 'Represents a Analytics query (currently experimental support).', +'Couchbase\Authenticator' => 'Interface of authentication containers.', +'Couchbase\BooleanFieldSearchQuery' => 'A FTS query that queries fields explicitly indexed as boolean.', +'Couchbase\BooleanSearchQuery' => 'A compound FTS query that allows various combinations of sub-queries.', +'Couchbase\Bucket' => 'Represents connection to the Couchbase Server', +'Couchbase\BucketManager' => 'Provides management capabilities for the Couchbase Bucket', +'Couchbase\ClassicAuthenticator' => 'Authenticator based on login/password credentials. + +This authenticator uses separate credentials for Cluster management interface +as well as for each bucket.', +'Couchbase\Cluster' => 'Represents a Couchbase Server Cluster. + +It is an entry point to the library, and in charge of opening connections to the Buckets. +In addition it can instantiate \Couchbase\ClusterManager to perform cluster-wide operations.', +'Couchbase\ClusterManager' => 'Provides management capabilities for a Couchbase Server Cluster', +'Couchbase\ConjunctionSearchQuery' => 'A compound FTS query that performs a logical AND between all its sub-queries (conjunction).', +'Couchbase\DateRangeSearchFacet' => 'A facet that categorizes hits inside date ranges (or buckets) provided by the user.', +'Couchbase\DateRangeSearchQuery' => 'A FTS query that matches documents on a range of values. At least one bound is required, and the +inclusiveness of each bound can be configured.', +'Couchbase\DisjunctionSearchQuery' => 'A compound FTS query that performs a logical OR between all its sub-queries (disjunction). It requires that a +minimum of the queries match. The minimum is configurable (default 1).', +'Couchbase\DocIdSearchQuery' => 'A FTS query that matches on Couchbase document IDs. Useful to restrict the search space to a list of keys (by using +this in a compound query).', +'Couchbase\Document' => 'Represents Couchbase Document, which stores metadata and the value. + +The instances of this class returned by K/V commands of the \Couchbase\Bucket', +'Couchbase\DocumentFragment' => 'A fragment of a JSON Document returned by the sub-document API.', +'Couchbase\Exception' => 'Exception represeting all errors generated by the extension', +'Couchbase\GeoBoundingBoxSearchQuery' => 'A FTS query which allows to match geo bounding boxes.', +'Couchbase\GeoDistanceSearchQuery' => 'A FTS query that finds all matches from a given location (point) within the given distance. + +Both the point and the distance are required.', +'Couchbase\LookupInBuilder' => 'A builder for subdocument lookups. In order to perform the final set of operations, use the +execute() method. + +Instances of this builder should be obtained through \Couchbase\Bucket->lookupIn()', +'Couchbase\MatchAllSearchQuery' => 'A FTS query that matches all indexed documents (usually for debugging purposes).', +'Couchbase\MatchNoneSearchQuery' => 'A FTS query that matches 0 document (usually for debugging purposes).', +'Couchbase\MatchPhraseSearchQuery' => 'A FTS query that matches several given terms (a "phrase"), applying further processing +like analyzers to them.', +'Couchbase\MatchSearchQuery' => 'A FTS query that matches a given term, applying further processing to it +like analyzers, stemming and even #fuzziness(int).', +'Couchbase\MutateInBuilder' => 'A builder for subdocument mutations. In order to perform the final set of operations, use the +execute() method. + +Instances of this builder should be obtained through \Couchbase\Bucket->mutateIn()', +'Couchbase\MutationState' => 'Container for mutation tokens.', +'Couchbase\MutationToken' => 'An object which contains meta information of the document needed to enforce query consistency.', +'Couchbase\N1qlIndex' => 'Represents N1QL index definition', +'Couchbase\N1qlQuery' => 'Represents a N1QL query', +'Couchbase\NumericRangeSearchFacet' => 'A facet that categorizes hits into numerical ranges (or buckets) provided by the user.', +'Couchbase\NumericRangeSearchQuery' => 'A FTS query that matches documents on a range of values. At least one bound is required, and the +inclusiveness of each bound can be configured.', +'Couchbase\PasswordAuthenticator' => 'Authenticator based on RBAC feature of Couchbase Server 5+. + +This authenticator uses single credentials for all operations (data and management).', +'Couchbase\PhraseSearchQuery' => 'A FTS query that matches several terms (a "phrase") as is. The order of the terms matter and no further processing is +applied to them, so they must appear in the index exactly as provided. Usually for debugging purposes, prefer +MatchPhraseQuery.', +'Couchbase\PrefixSearchQuery' => 'A FTS query that allows for simple matching on a given prefix.', +'Couchbase\QueryStringSearchQuery' => 'A FTS query that performs a search according to the "string query" syntax.', +'Couchbase\RegexpSearchQuery' => 'A FTS query that allows for simple matching of regular expressions.', +'Couchbase\SearchFacet' => 'Common interface for all search facets', +'Couchbase\SearchQuery' => 'Represents full text search query', +'Couchbase\SearchQueryPart' => 'Common interface for all classes, which could be used as a body of SearchQuery', +'Couchbase\SearchSort' => 'Base class for all FTS sort options in querying.', +'Couchbase\SearchSortField' => 'Sort by a field in the hits.', +'Couchbase\SearchSortGeoDistance' => 'Sort by a location and unit in the hits.', +'Couchbase\SearchSortId' => 'Sort by the document identifier.', +'Couchbase\SearchSortScore' => 'Sort by the hit score.', +'Couchbase\SpatialViewQuery' => 'Represents spatial Couchbase Map/Reduce View query', +'Couchbase\TermRangeSearchQuery' => 'A FTS query that matches documents on a range of values. At least one bound is required, and the +inclusiveness of each bound can be configured.', +'Couchbase\TermSearchFacet' => 'A facet that gives the number of occurrences of the most recurring terms in all hits.', +'Couchbase\TermSearchQuery' => 'A facet that gives the number of occurrences of the most recurring terms in all hits.', +'Couchbase\UserSettings' => 'Represents settings for new/updated user.', +'Couchbase\ViewQuery' => 'Represents regular Couchbase Map/Reduce View query', +'Couchbase\ViewQueryEncodable' => 'Common interface for all View queries', +'Couchbase\WildcardSearchQuery' => 'A FTS query that allows for simple matching using wildcard characters (* and ?).', +'Countable' => 'Classes implementing `Countable` can be used with the `count` function.', +'Crypto\Base64' => 'Class for base64 encoding and docoding', +'Crypto\Base64Exception' => 'Exception class for base64 errors', +'Crypto\Cipher' => 'Class providing cipher algorithms', +'Crypto\CipherException' => 'Exception class for cipher errors', +'Crypto\CMAC' => 'Class providing CMAC functionality', +'Crypto\Hash' => 'Class providing hash algorithms', +'Crypto\HashException' => 'Exception class for hash errors', +'Crypto\HMAC' => 'Class providing HMAC functionality', +'Crypto\KDF' => 'Abstract class for KDF subclasses', +'Crypto\KDFException' => 'Exception class for KDF errors', +'Crypto\MAC' => 'Abstract class for MAC subclasses', +'Crypto\MACException' => 'Exception class for MAC errors', +'Crypto\PBKDF2' => 'Class providing PBKDF2 functionality', +'Crypto\PBKDF2Exception' => 'Exception class for PBKDF2 errors', +'Crypto\Rand' => 'Class for generating random numbers', +'Crypto\RandException' => 'Exception class for rand errors', +'CURLFile' => '`CURLFile` should be used to upload a file with `CURLOPT_POSTFIELDS`.', +'DateInterval' => 'Represents a date interval. + +A date interval stores either a fixed amount of time (in years, months, days, hours etc) or a relative time string in the format that `DateTime`\'s constructor supports.', +'DatePeriod' => 'Represents a date period. + +A date period allows iteration over a set of dates and times, recurring at regular intervals, over a given period.', +'DateTime' => 'Representation of date and time.', +'DateTimeImmutable' => 'This class behaves the same as `DateTime` except it never modifies itself but returns a new object instead.', +'DateTimeInterface' => 'DateTimeInterface is meant so that both DateTime and DateTimeImmutable can be type hinted for. It is not possible to implement this interface with userland classes.', +'DateTimeZone' => 'Representation of time zone.', +'Directory' => 'Instances of `Directory` are created by calling the `dir` function, not by the new operator.', +'DirectoryIterator' => 'The DirectoryIterator class provides a simple interface for viewing the contents of filesystem directories.', +'DivisionByZeroError' => '`DivisionByZeroError` is thrown when an attempt is made to divide a number by zero.', +'DomainException' => 'Exception thrown if a value does not adhere to a defined valid data domain.', +'DOMAttr' => '`DOMAttr` represents an attribute in the `DOMElement` object.', +'DOMCdataSection' => 'The `DOMCdataSection` inherits from `DOMText` for textural representation of CData constructs.', +'DOMCharacterData' => 'Represents nodes with character data. No nodes directly correspond to this class, but other nodes do inherit from it.', +'DOMComment' => 'Represents comment nodes, characters delimited by `<!--` and `-->`.', +'DOMDocument' => 'Represents an entire HTML or XML document; serves as the root of the document tree.', +'DOMDocumentType' => 'Each `DOMDocument` has a `doctype` attribute whose value is either `null` or a `DOMDocumentType` object.', +'DOMEntity' => 'This interface represents a known entity, either parsed or unparsed, in an XML document.', +'DOMException' => 'DOM operations raise exceptions under particular circumstances, i.e., when an operation is impossible to perform for logical reasons. + +See also the PHP manual\'s section on language.exceptions.', +'DOMImplementation' => 'The `DOMImplementation` interface provides a number of methods for performing operations that are independent of any particular instance of the document object model.', +'DOMText' => 'The `DOMText` class inherits from `DOMCharacterData` and represents the textual content of a `DOMElement` or `DOMAttr`.', +'DOMXPath' => 'Supports XPath 1.0', +'DOTNET' => 'The DOTNET class allows you to instantiate a class from a .Net assembly and call its methods and access its properties.', +'Ds\Collection' => '`Collection` is the base interface which covers functionality common to all the data structures in this library. It guarantees that all structures are traversable, countable, and can be converted to json using `json_encode`.', +'Ds\Deque' => 'A Deque (pronounced “deck”) is a sequence of values in a contiguous buffer that grows and shrinks automatically. The name is a common abbreviation of “double-ended queue” and is used internally by `Ds\Queue`. + +Two pointers are used to keep track of a head and a tail. The pointers can “wrap around” the end of the buffer, which avoids the need to move other values around to make room. This makes shift and unshift very fast —  something a `Ds\Vector` can’t compete with. + +Accessing a value by index requires a translation between the index and its corresponding position in the buffer: `((head + position) % capacity)`.', +'Ds\Hashable' => 'Hashable is an interface which allows objects to be used as keys. It’s an alternative to `spl_object_hash`, which determines an object’s hash based on its handle: this means that two objects that are considered equal by an implicit definition would not treated as equal because they are not the same instance. + +`hash` is used to return a scalar value to be used as the object\'s hash value, which determines where it goes in the hash table. While this value does not have to be unique, objects which are equal must have the same hash value. + +`equals` is used to determine if two objects are equal. It\'s guaranteed that the comparing object will be an instance of the same class as the subject.', +'Ds\Map' => 'A Map is a sequential collection of key-value pairs, almost identical to an `array` used in a similar context. Keys can be any type, but must be unique. Values are replaced if added to the map using the same key.', +'Ds\Pair' => 'A pair is used by `Ds\Map` to pair keys with values.', +'Ds\PriorityQueue' => 'A PriorityQueue is very similar to a Queue. Values are pushed into the queue with an assigned priority, and the value with the highest priority will always be at the front of the queue. + +Implemented using a max heap.', +'Ds\Queue' => 'A Queue is a “first in, first out” or “FIFO” collection that only allows access to the value at the front of the queue and iterates in that order, destructively.', +'Ds\Sequence' => 'A Sequence describes the behaviour of values arranged in a single, linear dimension. Some languages refer to this as a "List". It’s similar to an array that uses incremental integer keys, with the exception of a few characteristics: Values will always be indexed as [0, 1, 2, …, size - 1]. Only allowed to access values by index in the range [0, size - 1]. + +Use cases: Wherever you would use an array as a list (not concerned with keys). A more efficient alternative to `SplDoublyLinkedList` and `SplFixedArray`.', +'Ds\Set' => 'A Set is a sequence of unique values. This implementation uses the same hash table as `Ds\Map`, where values are used as keys and the mapped value is ignored.', +'Ds\Stack' => 'A Stack is a “last in, first out” or “LIFO” collection that only allows access to the value at the top of the structure and iterates in that order, destructively. + +Uses a `Ds\Vector` internally.', +'Ds\Vector' => 'A Vector is a sequence of values in a contiguous buffer that grows and shrinks automatically. It’s the most efficient sequential structure because a value’s index is a direct mapping to its index in the buffer, and the growth factor isn\'t bound to a specific multiple or exponent.', +'EmptyIterator' => 'The EmptyIterator class for an empty iterator.', +'Ev' => 'Ev is a static class providing access to the default loop and to some common operations.', +'EvCheck' => '`EvPrepare` and `EvCheck` watchers are usually used in pairs. `EvPrepare` watchers get invoked before the process blocks, `EvCheck` afterwards. + +It is not allowed to call `EvLoop::run` or similar methods or functions that enter the current event loop from either `EvPrepare` or `EvCheck` watchers. Other loops than the current one are fine, however. The rationale behind this is that one don\'t need to check for recursion in those watchers, i.e. the sequence will always be: `EvPrepare` -> blocking -> `EvCheck` , so having a watcher of each kind they will always be called in pairs bracketing the blocking call. + +The main purpose is to integrate other event mechanisms into *libev* and their use is somewhat advanced. They could be used, for example, to track variable changes, implement custom watchers, integrate net-snmp or a coroutine library and lots more. They are also occasionally useful to cache some data and want to flush it before blocking. + +It is recommended to give `EvCheck` watchers highest( `Ev::MAXPRI` ) priority, to ensure that they are being run before any other watchers after the poll (this doesn’t matter for `EvPrepare` watchers). + +Also, `EvCheck` watchers should not activate/feed events. While *libev* fully supports this, they might get executed before other `EvCheck` watchers did their job.', +'EvChild' => '`EvChild` watchers trigger when the process receives a `SIGCHLD` in response to some child status changes (most typically when a child dies or exits). It is permissible to install an `EvChild` watcher after the child has been forked(which implies it might have already exited), as long as the event loop isn\'t entered(or is continued from a watcher), i.e. forking and then immediately registering a watcher for the child is fine, but forking and registering a watcher a few event loop iterations later or in the next callback invocation is not. + +It is allowed to register `EvChild` watchers in the *default loop* only.', +'EvEmbed' => 'Used to embed one event loop into another.', +'Event' => '`Event` class represents and event firing on a file descriptor being ready to read from or write to; a file descriptor becoming ready to read from or write to(edge-triggered I/O only); a timeout expiring; a signal occurring; a user-triggered event. + +Every event is associated with `EventBase` . However, event will never fire until it is *added* (via `Event::add` ). An added event remains in *pending* state until the registered event occurs, thus turning it to *active* state. To handle events user may register a callback which is called when event becomes active. If event is configured *persistent* , it remains pending. If it is not persistent, it stops being pending when it\'s callback runs. `Event::del` method *deletes* event, thus making it non-pending. By means of `Event::add` method it could be added again.', +'EventBase' => '`EventBase` class represents libevent\'s event base structure. It holds a set of events and can poll to determine which events are active. + +Each event base has a *method* , or a *backend* that it uses to determine which events are ready. The recognized methods are: `select` , `poll` , `epoll` , `kqueue` , `devpoll` , `evport` and `win32` . + +To configure event base to use, or avoid specific backend `EventConfig` class can be used.', +'EventBuffer' => '`EventBuffer` represents Libevent\'s "evbuffer", an utility functionality for buffered I/O. + +Event buffers are meant to be generally useful for doing the "buffer" part of buffered network I/O.', +'EventBufferEvent' => 'Represents Libevent\'s buffer event. + +Usually an application wants to perform some amount of data buffering in addition to just responding to events. When we want to write data, for example, the usual pattern looks like: + +This buffered I/O pattern is common enough that Libevent provides a generic mechanism for it. A "buffer event" consists of an underlying transport (like a socket), a read buffer, and a write buffer. Instead of regular events, which give callbacks when the underlying transport is ready to be read or written, a buffer event invokes its user-supplied callbacks when it has read or written enough data.', +'EventConfig' => 'Represents configuration structure which could be used in construction of the `EventBase` .', +'EventDnsBase' => 'Represents Libevent\'s DNS base structure. Used to resolve DNS asynchronously, parse configuration files like resolv.conf etc.', +'EventHttp' => 'Represents HTTP server.', +'EventHttpConnection' => 'Represents an HTTP connection.', +'EventHttpRequest' => 'Represents an HTTP request.', +'EventListener' => 'Represents a connection listener.', +'EventSslContext' => 'Represents `SSL_CTX` structure. Provides methods and properties to configure the SSL context.', +'EventUtil' => '`EventUtil` is a singleton with supplimentary methods and constants.', +'EvFork' => 'Fork watchers are called when a `fork()` was detected (usually because whoever signalled *libev* about it by calling `EvLoop::fork` ). The invocation is done before the event loop blocks next and before `EvCheck` watchers are being called, and only in the child after the fork. Note, that if whoever calling `EvLoop::fork` calls it in the wrong process, the fork handlers will be invoked, too.', +'EvIdle' => '`EvIdle` watchers trigger events when no other events of the same or higher priority are pending ( `EvPrepare` , `EvCheck` and other `EvIdle` watchers do not count as receiving *events* ). + +Thus, as long as the process is busy handling sockets or timeouts(or even signals) of the same or higher priority it will not be triggered. But when the process is in idle(or only lower-priority watchers are pending), the `EvIdle` watchers are being called once per event loop iteration - until stopped, that is, or the process receives more events and becomes busy again with higher priority stuff. + +Apart from keeping the process non-blocking(which is a useful on its own sometimes), `EvIdle` watchers are a good place to do *"pseudo-background processing"* , or delay processing stuff to after the event loop has handled all outstanding events. + +The most noticeable effect is that as long as any *idle* watchers are active, the process will *not* block when waiting for new events.', +'EvIo' => '`EvIo` watchers check whether a file descriptor(or socket, or a stream castable to numeric file descriptor) is readable or writable in each iteration of the event loop, or, more precisely, when reading would not block the process and writing would at least be able to write some data. This behaviour is called *level-triggering* because events are kept receiving as long as the condition persists. To stop receiving events just stop the watcher. + +The number of read and/or write event watchers per fd is unlimited. Setting all file descriptors to non-blocking mode is also usually a good idea(but not required). + +Another thing to watch out for is that it is quite easy to receive false readiness notifications, i.e. the callback might be called with `Ev::READ` but a subsequent *read()* will actually block because there is no data. It is very easy to get into this situation. Thus it is best to always use non-blocking I/O: An extra *read()* returning `EAGAIN` (or similar) is far preferable to a program hanging until some data arrives. + +If for some reason it is impossible to run the fd in non-blocking mode, then separately re-test whether a file descriptor is really ready. Some people additionally use `SIGALRM` and an interval timer, just to be sure thry won\'t block infinitely. + +Always consider using non-blocking mode.', +'EvLoop' => 'Represents an event loop that is always distinct from the *default loop* . Unlike the *default loop* , it cannot handle `EvChild` watchers. + +Having threads we have to create a loop per thread, and use the *default loop* in the parent thread. + +The *default event loop* is initialized automatically by *Ev* . It is accessible via methods of the `Ev` class, or via `EvLoop::defaultLoop` method.', +'EvPeriodic' => 'Periodic watchers are also timers of a kind, but they are very versatile. + +Unlike `EvTimer` , `EvPeriodic` watchers are not based on real time(or relative time, the physical time that passes) but on wall clock time(absolute time, calendar or clock). The difference is that wall clock time can run faster or slower than real time, and time jumps are not uncommon(e.g. when adjusting it). + +`EvPeriodic` watcher can be configured to trigger after some specific point in time. For example, if an `EvPeriodic` watcher is configured to trigger *"in 10 seconds"* (e.g. `EvLoop::now` + `10.0` , i.e. an absolute time, not a delay), and the system clock is reset to *January of the previous year* , then it will take a year or more to trigger the event (unlike an `EvTimer` , which would still trigger roughly `10` seconds after starting it as it uses a relative timeout). + +As with timers, the callback is guaranteed to be invoked only when the point in time where it is supposed to trigger has passed. If multiple timers become ready during the same loop iteration then the ones with earlier time-out values are invoked before ones with later time-out values (but this is no longer true when a callback calls `EvLoop::run` recursively).', +'EvPrepare' => 'Class EvPrepare + +EvPrepare and EvCheck watchers are usually used in pairs. EvPrepare watchers get invoked before the process blocks, +EvCheck afterwards. + +It is not allowed to call EvLoop::run() or similar methods or functions that enter the current event loop from either +EvPrepare or EvCheck watchers. Other loops than the current one are fine, however. The rationale behind this is that +one don\'t need to check for recursion in those watchers, i.e. the sequence will always be: EvPrepare -> blocking -> +EvCheck, so having a watcher of each kind they will always be called in pairs bracketing the blocking call. + +The main purpose is to integrate other event mechanisms into libev and their use is somewhat advanced. They could be +used, for example, to track variable changes, implement custom watchers, integrate net-snmp or a coroutine library +and lots more. They are also occasionally useful to cache some data and want to flush it before blocking. + +It is recommended to give EvCheck watchers highest (Ev::MAXPRI) priority, to ensure that they are being run before +any other watchers after the poll (this doesn’t matter for EvPrepare watchers). + +Also, EvCheck watchers should not activate/feed events. While libev fully supports this, they might get executed +before other EvCheck watchers did their job.', +'EvSignal' => '`EvSignal` watchers will trigger an event when the process receives a specific signal one or more times. Even though signals are very asynchronous, *libev* will try its best to deliver signals synchronously, i.e. as part of the normal event processing, like any other event. + +There is no limit for the number of watchers for the same signal, but only within the same loop, i.e. one can watch for `SIGINT` in the default loop and for `SIGIO` in another loop, but it is not allowed to watch for `SIGINT` in both the default loop and another loop at the same time. At the moment, `SIGCHLD` is permanently tied to the default loop. + +If possible and supported, *libev* will install its handlers with `SA_RESTART` (or equivalent) behaviour enabled, so system calls should not be unduly interrupted. In case of a problem with system calls getting interrupted by signals, all the signals can be blocked in an `EvCheck` watcher and unblocked in a `EvPrepare` watcher.', +'EvStat' => '`EvStat` monitors a file system path for attribute changes. It calls *stat()* on that path in regular intervals(or when the OS signals it changed) and sees if it changed compared to the last time, invoking the callback if it did. + +The path does not need to exist: changing from "path exists" to "path does not exist" is a status change like any other. The condition "path does not exist" is signified by the `\'nlink\'` item being 0(returned by `EvStat::attr` method). + +The path must not end in a slash or contain special components such as `\'.\'` or `..` . The path should be absolute: if it is relative and the working directory changes, then the behaviour is undefined. + +Since there is no portable change notification interface available, the portable implementation simply calls *stat()* regularly on the path to see if it changed somehow. For this case a recommended polling interval can be specified. If one specifies a polling interval of `0.0 ` (highly recommended) then a suitable, unspecified default value will be used(which could be expected to be around 5 seconds, although this might change dynamically). *libev* will also impose a minimum interval which is currently around `0.1` , but that’s usually overkill. + +This watcher type is not meant for massive numbers of `EvStat` watchers, as even with OS-supported change notifications, this can be resource-intensive.', +'EvTimer' => '`EvTimer` watchers are simple relative timers that generate an event after a given time, and optionally repeating in regular intervals after that. + +The timers are based on real time, that is, if one registers an event that times out after an hour and resets the system clock to *January last year* , it will still time out after(roughly) one hour. "Roughly" because detecting time jumps is hard, and some inaccuracies are unavoidable. + +The callback is guaranteed to be invoked only after its timeout has passed (not at, so on systems with very low-resolution clocks this might introduce a small delay). If multiple timers become ready during the same loop iteration then the ones with earlier time-out values are invoked before ones of the same priority with later time-out values (but this is no longer true when a callback calls `EvLoop::run` recursively). + +The timer itself will do a best-effort at avoiding drift, that is, if a timer is configured to trigger every `10` seconds, then it will normally trigger at exactly `10` second intervals. If, however, the script cannot keep up with the timer because it takes longer than those `10` seconds to do) the timer will not fire more than once per event loop iteration.', +'EvWatcher' => '`EvWatcher` is a base class for all watchers( `EvCheck` , `EvChild` etc.). Since `EvWatcher` \'s constructor is abstract , one can\'t(and don\'t need to) create EvWatcher objects directly.', +'FANNConnection' => '`FANNConnection` is used for the neural network connection. The objects of this class are used in `fann_get_connection_array` and `fann_set_weight_array`.', +'FFI' => 'FFI class provides access to a simple way to call native functions, +access native variables and create/access data structures defined +in C language.', +'FFI\CData' => 'Proxy object that provides access to compiled structures.', +'FFI\CType' => 'Class containing C type information.', +'FFI\Exception' => 'Class Exception', +'FFI\ParserException' => 'Class ParserException', +'FilesystemIterator' => 'The Filesystem iterator', +'FilterIterator' => 'This abstract iterator filters out unwanted values. This class should be extended to implement custom iterator filters. The `FilterIterator::accept` must be implemented in the subclass.', +'finfo' => 'This class provides an object oriented interface into the fileinfo functions.', +'GearmanClient' => 'Represents a class for connecting to a Gearman job server and making requests to perform some function on provided data. The function performed must be one registered by a Gearman worker and the data passed is opaque to the job server.', +'GearmanException' => 'Class: GearmanException', +'GearmanJob' => 'Class: GearmanJob', +'GearmanTask' => 'Class: GearmanTask', +'GearmanWorker' => 'Class: GearmanWorker', +'GEOSGeometry' => 'Class GEOSGeometry', +'GEOSWKBReader' => 'Class GEOSWKBReader', +'GEOSWKBWriter' => 'Class GEOSWKBWriter', +'GEOSWKTReader' => 'Class GEOSWKTReader', +'GEOSWKTWriter' => 'Class GEOSWKTWriter', +'GlobIterator' => 'Iterates through a file system in a similar fashion to `glob`.', +'GmagickException' => 'GmagickException class', +'GMP' => 'A GMP number. These objects support overloaded arithmetic, bitwise and comparison operators.', +'gnupg' => 'GNUPG Encryption Class', +'gridObj' => 'The grid is always embedded inside a layer object defined as +a grid (layer->connectiontype = MS_GRATICULE) +(for more docs : https://github.com/mapserver/mapserver/wiki/MapServerGrid) +A layer can become a grid layer by adding a grid object to it using : +ms_newGridObj(layerObj layer) +$oLayer = ms_newlayerobj($oMap); +$oLayer->set("name", "GRID"); +ms_newgridobj($oLayer); +$oLayer->grid->set("labelformat", "DDMMSS");', +'Grpc\Call' => 'Class Call', +'Grpc\CallCredentials' => 'Class CallCredentials', +'Grpc\Channel' => 'Class Channel', +'Grpc\ChannelCredentials' => 'Class ChannelCredentials', +'Grpc\Server' => 'Class Server', +'Grpc\ServerCredentials' => 'Class ServerCredentials', +'Grpc\Timeval' => 'Class Timeval', +'HaruAnnotation' => 'Haru PDF Annotation Class.', +'HaruDestination' => 'Haru PDF Destination Class.', +'HaruDoc' => 'Haru PDF Document Class.', +'HaruEncoder' => 'Haru PDF Encoder Class.', +'HaruException' => 'Haru PDF Exception Class.', +'HaruFont' => 'Haru PDF Font Class.', +'HaruImage' => 'Haru PDF Image Class.', +'HaruOutline' => 'Haru PDF Outline Class.', +'HaruPage' => 'Haru PDF Page Class.', +'hashTableObj' => 'Instance of hashTableObj is always embedded inside the `classObj`_, +`layerObj`_, `mapObj`_ and `webObj`_. It is uses a read only. +$hashTable = $oLayer->metadata; +$key = null; +while ($key = $hashTable->nextkey($key)) +echo "Key: ".$key." value: ".$hashTable->get($key)."
";', +'http\Client' => 'The HTTP client. See http\Client\Curl’s options which is the only driver currently supported.', +'http\Client\Curl\User' => 'Interface to an user event loop implementation for http\Client::configure()\'s $use_eventloop option.', +'http\Client\Request' => 'The http\Client\Request class provides an HTTP message implementation tailored to represent a request message to be sent by the client. + +See http\Client::enqueue().', +'http\Client\Response' => 'The http\Client\Response class represents an HTTP message the client returns as answer from a server to an http\Client\Request.', +'http\Cookie' => 'A class representing a list of cookies with specific attributes.', +'http\Encoding\Stream' => 'Base class for encoding stream implementations.', +'http\Encoding\Stream\Debrotli' => 'A [brotli](https://brotli.org) decoding stream.', +'http\Encoding\Stream\Dechunk' => 'A stream decoding data encoded with chunked transfer encoding.', +'http\Encoding\Stream\Deflate' => 'A deflate stream supporting deflate, zlib and gzip encodings.', +'http\Encoding\Stream\Enbrotli' => 'A [brotli](https://brotli.org) encoding stream.', +'http\Encoding\Stream\Inflate' => 'A inflate stream supporting deflate, zlib and gzip encodings.', +'http\Env' => 'The http\Env class provides static methods to manipulate and inspect the server’s current request’s HTTP environment', +'http\Env\Request' => 'The http\Env\Request class\' instances represent the server’s current HTTP request. + +See http\Message for inherited members.', +'http\Env\Response' => 'Class Response + +The http\Env\Response class\' instances represent the server’s current HTTP response. + +See http\Message for inherited members.', +'http\Env\Url' => 'URL class using the HTTP environment by default. + +Always adds http\Url::FROM_ENV to the $flags constructor argument. See also http\Url.', +'http\Exception' => 'The http extension\'s Exception interface. + +Use it to catch any Exception thrown by pecl/http. + +The individual exception classes extend their equally named native PHP extensions, if such exist, and implement this empty interface. For example the http\Exception\BadMethodCallException extends SPL\'s BadMethodCallException.', +'http\Exception\BadConversionException' => 'A bad conversion (e.g. character conversion) was encountered.', +'http\Exception\BadHeaderException' => 'A bad HTTP header was encountered.', +'http\Exception\BadMessageException' => 'A bad HTTP message was encountered.', +'http\Exception\BadMethodCallException' => 'A method was called on an object, which was in an invalid or unexpected state.', +'http\Exception\BadQueryStringException' => 'A bad querystring was encountered.', +'http\Exception\BadUrlException' => 'A bad HTTP URL was encountered.', +'http\Exception\InvalidArgumentException' => 'One or more invalid arguments were passed to a method.', +'http\Exception\RuntimeException' => 'A generic runtime exception.', +'http\Exception\UnexpectedValueException' => 'An unexpected value was encountered.', +'http\Header' => 'The http\Header class provides methods to manipulate, match, negotiate and serialize HTTP headers.', +'http\Header\Parser' => 'The parser which is underlying http\Header and http\Message.', +'http\Message' => 'The message class builds the foundation for any request and response message. + +See http\Client\Request and http\Client\Response, as well as http\Env\Request and http\Env\Response.', +'http\Message\Body' => 'The message body, represented as a PHP (temporary) stream.', +'http\Message\Parser' => 'The parser which is underlying http\Message.', +'http\Params' => 'Parse, interpret and compose HTTP (header) parameters.', +'http\QueryString' => 'The http\QueryString class provides versatile facilities to retrieve, use and manipulate query strings and form data.', +'http\Url' => 'The http\Url class provides versatile means to parse, construct and manipulate URLs.', +'imageObj' => 'Instances of imageObj are always created by the `mapObj`_ class methods.', +'InfiniteIterator' => 'The `InfiniteIterator` allows one to infinitely iterate over an iterator without having to manually rewind the iterator upon reaching its end.', +'IntlBreakIterator' => 'A “break iterator” is an ICU object that exposes methods for locating boundaries in text (e.g. word or sentence boundaries). The PHP `IntlBreakIterator` serves as the base class for all types of ICU break iterators. Where extra functionality is available, the intl extension may expose the ICU break iterator with suitable subclasses, such as `IntlRuleBasedBreakIterator` or `IntlCodePointBreakIterator`. + +This class implements `Traversable`. Traversing an `IntlBreakIterator` yields non-negative integer values representing the successive locations of the text boundaries, expressed as UTF-8 code units (byte) counts, taken from the beginning of the text (which has the location `0`). The keys yielded by the iterator simply form the sequence of natural numbers `{0, 1, 2, …}`.', +'IntlChar' => '`IntlChar` provides access to a number of utility methods that can be used to access information about Unicode characters. + +The methods and constants adhere closely to the names and behavior used by the underlying ICU library.', +'IntlCodePointBreakIterator' => 'This break iterator identifies the boundaries between UTF-8 code points.', +'IntlException' => 'This class is used for generating exceptions when errors occur inside intl functions. Such exceptions are only generated when intl.use_exceptions is enabled.', +'IntlIterator' => 'This class represents iterator objects throughout the intl extension whenever the iterator cannot be identified with any other object provided by the extension. The distinct iterator object used internally by the `foreach` construct can only be obtained (in the relevant part here) from objects, so objects of this class serve the purpose of providing the hook through which this internal object can be obtained. As a convenience, this class also implements the `Iterator` interface, allowing the collection of values to be navigated using the methods defined in that interface. Both these methods and the internal iterator objects provided to `foreach` are backed by the same state (e.g. the position of the iterator and its current value). + +Subclasses may provide richer functionality.', +'IntlPartsIterator' => 'Objects of this class can be obtained from `IntlBreakIterator` objects. While the break iterators provide a sequence of boundary positions when iterated, `IntlPartsIterator` objects provide, as a convenience, the text fragments comprehended between two successive boundaries. + +The keys may represent the offset of the left boundary, right boundary, or they may just the sequence of non-negative integers. See `IntlBreakIterator::getPartsIterator`.', +'IntlRuleBasedBreakIterator' => 'A subclass of `IntlBreakIterator` that encapsulates ICU break iterators whose behavior is specified using a set of rules. This is the most common kind of break iterators. + +These rules are described in the ICU Boundary Analysis User Guide.', +'InvalidArgumentException' => 'Exception thrown if an argument is not of the expected type.', +'IteratorIterator' => 'This iterator wrapper allows the conversion of anything that is Traversable into an Iterator. It is important to understand that most classes that do not implement Iterators have reasons as most likely they do not allow the full Iterator feature set. If so, techniques should be provided to prevent misuse, otherwise expect exceptions or fatal errors.', +'JsonException' => 'Exception thrown if `JSON_THROW_ON_ERROR` option is set for `json_encode` or `json_decode`.', +'JsonSerializable' => 'Objects implementing `JsonSerializable` can customize their JSON representation when encoded with `json_encode`.', +'Judy' => 'The Judy class implements the ArrayAccess interface and the Iterator interface. This class, once instantiated, can be accessed like a PHP array. + +A PHP Judy object (or Judy Array) can be one of the following type : Judy::BITSET Judy::INT_TO_INT Judy::INT_TO_MIXED Judy::STRING_TO_INT Judy::STRING_TO_MIXED + +Judy array example ]]>', +'KTaglib_ID3v2_AttachedPictureFrame' => 'Represents an ID3v2 frame that can hold a picture.', +'KTaglib_ID3v2_Frame' => 'The base class for ID3v2 frames. ID3v2 tags are separated in various specialized frames. Some frames can exists multiple times.', +'KTaglib_ID3v2_Tag' => 'Represents and ID3v2 tag. It provides a list of ID3v2 frames and can be used to add and remove additional frames.', +'KTaglib_MPEG_Audioproperties' => 'Represents the audio properties of a MPEG file, like length, bitrate or samplerate.', +'KTaglib_MPEG_File' => 'Represents an MPEG file. MPEG files can have ID3v1, ID3v2 tags and audio properties.', +'KTaglib_Tag' => 'Base class for ID3v1 or ID3v2 tags', +'labelObj' => 'labelObj are always embedded inside other classes.', +'Lapack' => 'LAPACK is written in Fortran 90 and provides routines for solving systems of simultaneous linear equations, least-squares solutions of linear systems of equations, eigenvalue problems, and singular value problems. This extension wraps the LAPACKE C bindings to allow access to several processes exposed by the library. Most functions work with arrays of arrays, representing rectangular matrices in row major order - so a two by two matrix [1 2; 3 4] would be array(array(1, 2), array(3, 4)). + +All of the functions are called statically, for example $eig = Lapack::eigenvalues($a);', +'lapackexception' => 'Exception thrown when an error is caught in the LAPACK functions', +'layerObj' => 'Layer Objects can be returned by the `mapObj`_ class, or can be +created using: +A second optional argument can be given to ms_newLayerObj() to create +the new layer as a copy of an existing layer. If a layer is given as +argument then all members of a this layer will be copied in the new +layer created.', +'legendObj' => 'Instances of legendObj are always are always embedded inside the `mapObj`_.', +'LengthException' => 'Exception thrown if a length is invalid.', +'libXMLError' => 'Contains various information about errors thrown by libxml. The error codes are described within the official xmlError API documentation.', +'LimitIterator' => 'The `LimitIterator` class allows iteration over a limited subset of items in an `Iterator`.', +'Locale' => 'Examples of identifiers include: en-US (English, United States) zh-Hant-TW (Chinese, Traditional Script, Taiwan) fr-CA, fr-FR (French for Canada and France respectively)', +'LogicException' => 'Exception that represents error in the program logic. This kind of exception should lead directly to a fix in your code.', +'LuaClosure' => 'LuaClosure is a wrapper class for LUA_TFUNCTION which could be return from calling to Lua function.', +'LuaSandbox' => 'The LuaSandbox class creates a Lua environment and allows for execution of Lua code.', +'LuaSandboxError' => 'Base class for LuaSandbox exceptions', +'LuaSandboxErrorError' => 'Exception thrown when Lua encounters an error inside an error handler.', +'LuaSandboxFatalError' => 'Uncatchable LuaSandbox exceptions. + +These may not be caught inside Lua using `pcall()` or `xpcall()`.', +'LuaSandboxFunction' => 'Represents a Lua function, allowing it to be called from PHP. + +A LuaSandboxFunction may be obtained as a return value from Lua, as a parameter passed to a callback from Lua, or by using `LuaSandbox::wrapPhpFunction`, `LuaSandbox::loadString`, or `LuaSandbox::loadBinary`.', +'LuaSandboxMemoryError' => 'Exception thrown when Lua cannot allocate memory.', +'LuaSandboxRuntimeError' => 'Catchable LuaSandbox runtime exceptions. + +These may be caught inside Lua using `pcall()` or `xpcall()`.', +'LuaSandboxSyntaxError' => 'Exception thrown when Lua code cannot be parsed.', +'LuaSandboxTimeoutError' => 'Exception thrown when the configured CPU time limit is exceeded.', +'Memcache' => 'Represents a connection to a set of memcache servers.', +'Memcached' => 'Represents a connection to a set of memcached servers.', +'Mongo' => 'A connection between PHP and MongoDB. + +This class extends `MongoClient` and provides access to several deprecated methods. + +For backwards compatibility, it also defaults the `"w"` option of its constructor argument to `0`, which does not require write operations to be acknowledged by the server. See `MongoClient::__construct` for more information.', +'MongoBinData' => 'An object that can be used to store or retrieve binary data from the database. + +The maximum size of a single object that can be inserted into the database is 16MB. For data that is larger than this (movies, music, Henry Kissinger\'s autobiography), use `MongoGridFS`. For data that is smaller than 16MB, you may find it easier to embed it within the document using `MongoBinData`. + +For example, to embed an image in a document, one could write: + +This class contains a type field, which currently gives no additional functionality in the PHP driver or the database. There are seven predefined types, which are defined as class constants below. For backwards compatibility, the PHP driver uses `MongoBinData::BYTE_ARRAY` as the default; however, this may change to `MongoBinData::GENERIC` in the future. Users are encouraged to specify a type in `MongoBinData::__construct`.', +'MongoClient' => 'A connection manager for PHP and MongoDB. + +This class is used to create and manage connections. A typical use is: `MongoClient` basic usage foo; // get the database named "foo" ?> ]]> + +See `MongoClient::__construct` and the section on connecting for more information about creating connections.', +'MongoCode' => 'Represents JavaScript code for the database. + +MongoCode objects are composed of two parts: a string of code and an optional scope. The string of code must be valid JavaScript. The scope is a associative array of variable name/value pairs.', +'MongoCollection' => 'Represents a MongoDB collection. + +Collection names can use any character in the ASCII set. Some valid collection names are "", "...", "my collection", and "*&#@". + +User-defined collection names cannot contain the $ symbol. There are certain system collections which use a $ in their names (e.g., local.oplog.$main), but it is a reserved character. If you attempt to create and use a collection with a $ in the name, MongoDB will assert.', +'MongoCommandCursor' => 'A command cursor is similar to a `MongoCursor` except that you use it for iterating through the results of a database command instead of a normal query. Command cursors are useful for iterating over large result sets that might exceed the document size limit (currently 16MB) of a single `MongoDB::command` response. + +While you can create command cursors using `MongoCommandCursor::__construct` or the `MongoCommandCursor::createFromDocument` factory method, you will generally want to use command-specific helpers such as `MongoCollection::aggregateCursor`. + +Note that the cursor does not "contain" the database command\'s results; it just manages iteration through them. Thus, if you print a cursor (f.e. with `var_dump` or `print_r`), you will see the cursor object but not the result documents.', +'MongoConnectionException' => 'Thrown when the driver fails to connect to the database. + +There are a number of possible error messages to help you diagnose the connection problem. These are: + +If the error message is not listed above, it is probably an error from the C socket, and you can search the web for its usual cause.', +'MongoCursor' => 'A cursor is used to iterate through the results of a database query. For example, to query the database and see all results, you could do: `MongoCursor` basic usage find(); var_dump(iterator_to_array($cursor)); ?> ]]> + +You don\'t generally create cursors using the `MongoCursor` constructor, you get a new cursor by calling `MongoCollection::find` (as shown above). + +Suppose that, in the example above, `$collection` was a 50GB collection. We certainly wouldn\'t want to load that into memory all at once, which is what a cursor is for: allowing the client to access the collection in dribs and drabs. + +If we have a large result set, we can iterate through it, loading a few megabytes of results into memory at a time. For example, we could do: Iterating over `MongoCursor` find(); foreach ($cursor as $doc) { // do something to each document } ?> ]]> This will go through each document in the collection, loading and garbage collecting documents as needed. + +Note that this means that a cursor does not "contain" the database results, it just manages them. Thus, if you print a cursor (with, say, `var_dump` or `print_r`), you\'ll just get the cursor object, not your documents. To get the documents themselves, you can use one of the methods shown above.', +'MongoCursorException' => 'Caused by accessing a cursor incorrectly or a error receiving a reply. Note that this can be thrown by any database request that receives a reply, not just queries. Writes, commands, and any other operation that sends information to the database and waits for a response can throw a `MongoCursorException`. The only exception is `new MongoClient()` (creating a new connection), which will only throw `MongoConnectionException`s. + +This returns a specific error message to help diagnose the problem and a numeric error code associated with the cause of the exception. + +For example, suppose you tried to insert two documents with the same _id: insert(array("_id" => 1), array("w" => 1)); $collection->insert(array("_id" => 1), array("w" => 1)); } catch (MongoCursorException $e) { echo "error message: ".$e->getMessage()."\n"; echo "error code: ".$e->getCode()."\n"; } ?> ]]> This would produce output like: Note that the MongoDB error code (11000) is used for the PHP error code. The PHP driver uses the "native" error code wherever possible. + +The following is a list of common errors, codes, and causes. Exact errors are in italics, errors where the message can vary are described in obliques.', +'MongoCursorInterface' => 'Interface for cursors, which can be used to iterate through results of a database query or command. This interface is implemented by the `MongoCursor` and `MongoCommandCursor` classes.', +'MongoCursorTimeoutException' => 'Caused by a query timing out. You can set the length of time to wait before this exception is thrown by calling `MongoCursor::timeout` on the cursor or setting `MongoCursor::$timeout`. The static variable is useful for queries such as database commands and `MongoCollection::findOne`, both of which implicitly use cursors.', +'MongoDate' => 'Represent date objects for the database. This class should be used to save dates to the database and to query for dates. For example: + +MongoDB stores dates as milliseconds past the epoch. This means that dates *do not* contain timezone information. Timezones must be stored in a separate field if needed. Second, this means that any precision beyond milliseconds will be lost when the document is sent to/from the database.', +'MongoDB' => 'Instances of this class are used to interact with a database. To get a database: Selecting a database selectDB("example"); ?> ]]> Database names can use almost any character in the ASCII range. However, they cannot contain " ", "." or be the empty string. The name "system" is also reserved. + +A few unusual, but valid, database names: "null", "[x,y]", "3", "\"", "/". + +Unlike collection names, database names may contain "$".', +'MongoDB\BSON\Binary' => 'BSON type for binary data (i.e. array of bytes). Binary values also have a subtype, which is used to indicate what kind of data is in the byte array. Subtypes from zero to 127 are predefined or reserved. Subtypes from 128-255 are user-defined.', +'MongoDB\BSON\BinaryInterface' => 'This interface is implemented by `MongoDB\BSON\Binary` but may also be used for type-hinting and userland classes.', +'MongoDB\BSON\DBPointer' => 'BSON type for the "DBPointer" type. This BSON type is deprecated, and this class can not be instantiated. It will be created from a BSON DBPointer type while converting BSON to PHP, and can also be converted back into BSON while storing documents in the database.', +'MongoDB\BSON\Decimal128' => 'BSON type for the Decimal128 floating-point format, which supports numbers with up to 34 decimal digits (i.e. significant digits) and an exponent range of −6143 to +6144. + +Unlike the double BSON type (i.e. `float` in PHP), which only stores an approximation of the decimal values, the decimal data type stores the exact value. For example, `MongoDB\BSON\Decimal128(\'9.99\')` has a precise value of 9.99 where as a double 9.99 would have an approximate value of 9.9900000000000002131628….', +'MongoDB\BSON\Decimal128Interface' => 'This interface is implemented by `MongoDB\BSON\Decimal128` but may also be used for type-hinting and userland classes.', +'MongoDB\BSON\Int64' => 'BSON type for a 64-bit integer. This class cannot be instantiated and is only created during BSON decoding when a 64-bit integer cannot be represented as a PHP integer on a 32-bit platform. Versions of the driver before 1.5.0 would throw an exception when attempting to decode a 64-bit integer on a 32-bit platform. + +During BSON encoding, objects of this class will convert back to a 64-bit integer type. This allows 64-bit integers to be roundtripped through a 32-bit PHP environment without any loss of precision. The __toString() method allows the 64-bit integer value to be accessed as a string.', +'MongoDB\BSON\Javascript' => 'BSON type for Javascript code. An optional scope document may be specified that maps identifiers to values and defines the scope in which the code should be evaluated by the server.', +'MongoDB\BSON\JavascriptInterface' => 'This interface is implemented by `MongoDB\BSON\Javascript` but may also be used for type-hinting and userland classes.', +'MongoDB\BSON\MaxKey' => 'Special BSON type which compares higher than all other possible BSON element values.', +'MongoDB\BSON\MaxKeyInterface' => 'This interface is implemented by `MongoDB\BSON\MaxKey` but may also be used for type-hinting and userland classes.', +'MongoDB\BSON\MinKey' => 'Special BSON type which compares lower than all other possible BSON element values.', +'MongoDB\BSON\MinKeyInterface' => 'This interface is implemented by `MongoDB\BSON\MinKey` but may also be used for type-hinting and userland classes.', +'MongoDB\BSON\ObjectId' => 'BSON type for an ObjectId. The value consists of 12 bytes, where the first four bytes are a timestamp that reflect the ObjectId\'s creation. Specifically, the value consists of: + +In MongoDB, each document stored in a collection requires a unique `_id` field that acts as a primary key. If an inserted document omits the `_id` field, the driver automatically generates an ObjectId for the `_id` field. + +Using ObjectIds for the `_id` field provides the following additional benefits:', +'MongoDB\BSON\ObjectIdInterface' => 'This interface is implemented by `MongoDB\BSON\ObjectId` but may also be used for type-hinting and userland classes.', +'MongoDB\BSON\Persistable' => 'Classes may implement this interface to take advantage of automatic ODM (object document mapping) behavior in the driver. During serialization, the driver will inject a `__pclass` property containing the PHP class name into the data returned by `MongoDB\BSON\Serializable::bsonSerialize`. During unserialization, the same `__pclass` property will then be used to infer the PHP class (independent of any type map configuration) to be constructed before `MongoDB\BSON\Unserializable::bsonUnserialize` is invoked. See the PHP manual\'s section on mongodb.persistence for additional information.', +'MongoDB\BSON\Regex' => 'BSON type for a regular expression pattern and optional flags.', +'MongoDB\BSON\RegexInterface' => 'This interface is implemented by `MongoDB\BSON\Regex` but may also be used for type-hinting and userland classes.', +'MongoDB\BSON\Serializable' => 'Classes that implement this interface may return data to be serialized as a BSON array or document in lieu of the object\'s public properties.', +'MongoDB\BSON\Symbol' => 'BSON type for the "Symbol" type. This BSON type is deprecated, and this class can not be instantiated. It will be created from a BSON symbol type while converting BSON to PHP, and can also be converted back into BSON while storing documents in the database.', +'MongoDB\BSON\Timestamp' => 'Represents a BSON timestamp, The value consists of a 4-byte timestamp (i.e. seconds since the epoch) and a 4-byte increment.', +'MongoDB\BSON\TimestampInterface' => 'This interface is implemented by `MongoDB\BSON\Timestamp` but may also be used for type-hinting and userland classes.', +'MongoDB\BSON\Type' => 'Abstract base interface that should not be implemented directly.', +'MongoDB\BSON\Undefined' => 'BSON type for the "Undefined" type. This BSON type is deprecated, and this class can not be instantiated. It will be created from a BSON undefined type while converting BSON to PHP, and can also be converted back into BSON while storing documents in the database.', +'MongoDB\BSON\Unserializable' => 'Classes that implement this interface may be specified in a type map for unserializing BSON arrays and documents (both root and embedded).', +'MongoDB\BSON\UTCDateTime' => 'Represents a BSON date. The value is a 64-bit integer that represents the number of milliseconds since the Unix epoch (Jan 1, 1970). Negative values represent dates before 1970.', +'MongoDB\BSON\UTCDateTimeInterface' => 'This interface is implemented by `MongoDB\BSON\UTCDateTime` but may also be used for type-hinting and userland classes.', +'MongoDB\Driver\BulkWrite' => 'The `MongoDB\Driver\BulkWrite` collects one or more write operations that should be sent to the server. After adding any number of insert, update, and delete operations, the collection may be executed via `MongoDB\Driver\Manager::executeBulkWrite`. + +Write operations may either be ordered (default) or unordered. Ordered write operations are sent to the server, in the order provided, for serial execution. If a write fails, any remaining operations will be aborted. Unordered operations are sent to the server in an arbitrary order where they may be executed in parallel. Any errors that occur are reported after all operations have been attempted.', +'MongoDB\Driver\ClientEncryption' => 'The `MongoDB\Driver\ClientEncryption` class handles creation of data keys for client-side encryption, as well as manually encrypting and decrypting values.', +'MongoDB\Driver\Command' => 'The `MongoDB\Driver\Command` class is a value object that represents a database command. + +To provide Command Helpers the `MongoDB\Driver\Command` object should be composed.', +'MongoDB\Driver\Cursor' => 'The `MongoDB\Driver\Cursor` class encapsulates the results of a MongoDB command or query and may be returned by `MongoDB\Driver\Manager::executeCommand` or `MongoDB\Driver\Manager::executeQuery`, respectively.', +'MongoDB\Driver\CursorId' => 'The `MongoDB\Driver\CursorID` class is a value object that represents a cursor ID. Instances of this class are returned by `MongoDB\Driver\Cursor::getId`.', +'MongoDB\Driver\CursorInterface' => 'This interface is implemented by `MongoDB\Driver\Cursor` but may also be used for type-hinting and userland classes.', +'MongoDB\Driver\Exception\AuthenticationException' => 'Thrown when the driver fails to authenticate with the server.', +'MongoDB\Driver\Exception\BulkWriteException' => 'Thrown when a bulk write operation fails.', +'MongoDB\Driver\Exception\CommandException' => 'Thrown when a command fails.', +'MongoDB\Driver\Exception\ConnectionException' => 'Base class for exceptions thrown when the driver fails to establish a database connection.', +'MongoDB\Driver\Exception\ConnectionTimeoutException' => 'Thrown when the driver fails to establish a database connection within a specified time limit (connectTimeoutMS) or server selection fails (serverSelectionTimeoutMS).', +'MongoDB\Driver\Exception\EncryptionException' => 'Base class for exceptions thrown during client-side encryption.', +'MongoDB\Driver\Exception\Exception' => 'Common interface for all driver exceptions. This may be used to catch only exceptions originating from the driver itself.', +'MongoDB\Driver\Exception\ExecutionTimeoutException' => 'Thrown when a query or command fails to complete within a specified time limit (e.g. maxTimeMS).', +'MongoDB\Driver\Exception\InvalidArgumentException' => 'Thrown when a driver method is given invalid arguments (e.g. invalid option types).', +'MongoDB\Driver\Exception\LogicException' => 'Thrown when the driver is incorrectly used (e.g. rewinding a cursor).', +'MongoDB\Driver\Exception\RuntimeException' => 'Thrown when the driver encounters a runtime error (e.g. internal error from libmongoc).', +'MongoDB\Driver\Exception\ServerException' => 'Base class for exceptions thrown by the server. The code of this exception and its subclasses will correspond to the original error code from the server.', +'MongoDB\Driver\Exception\SSLConnectionException' => 'Thrown when the driver fails to establish an SSL connection with the server.', +'MongoDB\Driver\Exception\UnexpectedValueException' => 'Thrown when the driver encounters an unexpected value (e.g. during BSON serialization or deserialization).', +'MongoDB\Driver\Exception\WriteException' => 'Base class for exceptions thrown by a failed write operation. The exception encapsulates a `MongoDB\Driver\WriteResult` object.', +'MongoDB\Driver\Manager' => 'The `MongoDB\Driver\Manager` is the main entry point to the extension. It is responsible for maintaining connections to MongoDB (be it standalone server, replica set, or sharded cluster). + +No connection to MongoDB is made upon instantiating the Manager. This means the `MongoDB\Driver\Manager` can always be constructed, even though one or more MongoDB servers are down. + +Any write or query can throw connection exceptions as connections are created lazily. A MongoDB server may also become unavailable during the life time of the script. It is therefore important that all actions on the Manager to be wrapped in try/catch statements.', +'MongoDB\Driver\Monitoring\CommandFailedEvent' => 'The `MongoDB\Driver\Monitoring\CommandFailedEvent` class encapsulates information about a failed command.', +'MongoDB\Driver\Monitoring\CommandStartedEvent' => 'The `MongoDB\Driver\Monitoring\CommandStartedEvent` class encapsulates information about a started command.', +'MongoDB\Driver\Monitoring\CommandSubscriber' => 'Classes may implement this interface to register an event subscriber that is notified for each started, successful, and failed command event. See the PHP manual\'s section on mongodb.tutorial.apm for additional information.', +'MongoDB\Driver\Monitoring\CommandSucceededEvent' => 'The `MongoDB\Driver\Monitoring\CommandSucceededEvent` class encapsulates information about a successful command.', +'MongoDB\Driver\Monitoring\Subscriber' => 'Base interface for event subscribers. This is used for type-hinting `MongoDB\Driver\Monitoring\addSubscriber` and `MongoDB\Driver\Monitoring\removeSubscriber` and should not be implemented directly.', +'MongoDB\Driver\Query' => 'The `MongoDB\Driver\Query` class is a value object that represents a database query.', +'MongoDB\Driver\ReadConcern' => '`MongoDB\Driver\ReadConcern` controls the level of isolation for read operations for replica sets and replica set shards. This option requires MongoDB 3.2 or later.', +'MongoDB\Driver\ReadPreference' => 'Class ReadPreference', +'MongoDB\Driver\Session' => 'The `MongoDB\Driver\Session` class represents a client session and is returned by `MongoDB\Driver\Manager::startSession`. Commands, queries, and write operations may then be associated the session.', +'MongoDB\Driver\WriteConcern' => '`MongoDB\Driver\WriteConcern` describes the level of acknowledgement requested from MongoDB for write operations to a standalone `mongod` or to replica sets or to sharded clusters. In sharded clusters, `mongos` instances will pass the write concern on to the shards.', +'MongoDB\Driver\WriteConcernError' => 'The `MongoDB\Driver\WriteConcernError` class encapsulates information about a write concern error and may be returned by `MongoDB\Driver\WriteResult::getWriteConcernError`.', +'MongoDB\Driver\WriteError' => 'The `MongoDB\Driver\WriteError` class encapsulates information about a write error and may be returned as an array element from `MongoDB\Driver\WriteResult::getWriteErrors`.', +'MongoDB\Driver\WriteResult' => 'The `MongoDB\Driver\WriteResult` class encapsulates information about an executed `MongoDB\Driver\BulkWrite` and may be returned by `MongoDB\Driver\Manager::executeBulkWrite`.', +'MongoDBRef' => 'This class can be used to create lightweight links between objects in different collections. + +*Motivation*: Suppose we need to refer to a document in another collection. The easiest way is to create a field in the current document. For example, if we had a "people" collection and an "addresses" collection, we might want to create a link between each person document and an address document: Linking documents people; $addresses = $db->addresses; $myAddress = array("line 1" => "123 Main Street", "line 2" => null, "city" => "Springfield", "state" => "Vermont", "country" => "USA"); // save the address $addresses->insert($myAddress); // save a person with a reference to the address $me = array("name" => "Fred", "address" => $myAddress[\'_id\']); $people->insert($me); ?> ]]> + +Then, later on, we can find the person\'s address by querying the "addresses" collection with the `MongoId` we saved in the "people" collection. + +Suppose now that we have a more general case, where we don\'t know which collection (or even which database) contains the referenced document. `MongoDBRef` is a good choice for this case, as it is a common format that all of the drivers and the database understand. + +If each person had a list of things they liked which could come from multiple collections, such as "hobbies", "sports", "books", etc., we could use `MongoDBRef`s to keep track of what "like" went with what collection: Creating MongoDBRef links selectCollection("people"); // model trains are in the "hobbies" collection $trainRef = MongoDBRef::create("hobbies", $modelTrains[\'_id\']); // soccer is in the "sports" collection $soccerRef = MongoDBRef::create("sports", $soccer[\'_id\']); // now we\'ll know what collections the items in the "likes" array came from when // we retrieve this document $people->insert(array("name" => "Fred", "likes" => array($trainRef, $soccerRef))); ?> ]]> + +Database references can be thought of as hyperlinks: they give the unique address of another document, but they do not load it or automatically follow the link/reference. + +A database reference is just a normal associative array, not an instance of `MongoDBRef`, so this class is a little different than the other data type classes. This class contains exclusively static methods for manipulating database references.', +'MongoDeleteBatch' => 'Constructs a batch of DELETE operations. See `MongoWriteBatch`.', +'MongoDuplicateKeyException' => 'Thrown when attempting to insert a document into a collection which already contains the same values for the unique keys.', +'MongoException' => 'Default Mongo exception. + +This covers a bunch of different error conditions that may eventually be moved to more specific exceptions, but will always extend `MongoException`.', +'MongoExecutionTimeoutException' => 'Thrown when a operation times out server side (i.e. in MongoDB). + +To configure the operation timeout threshold, use `MongoCursor::maxTimeMS` or the `"maxTimeMS"` command option.', +'MongoGridFS' => 'Utilities for storing and retrieving files from the database. + +GridFS is a storage specification all supported drivers implement. Basically, it defines two collections: `files`, for file metadata, and `chunks`, for file content. If the file is large, it will automatically be split into smaller chunks and each chunk will be saved as a document in the chunks collection. + +Each document in the files collection contains the filename, upload date, and md5 hash. It also contains a unique `_id` field, which can be used to query the chunks collection for the file\'s content. Each document in the chunks collection contains a chunk of binary data, a `files_id` field that matches its file\'s `_id`, and the position of this chunk in the overall file. + +For example, the files document is something like: 123456789, "filename" => "foo.txt", "chunkSize" => 3, "length" => 12); ?> ]]> and the chunks documents look like: 123456789, "n" => 0, "data" => new MongoBinData("abc")); array("files_id" => 123456789, "n" => 1, "data" => new MongoBinData("def")); array("files_id" => 123456789, "n" => 2, "data" => new MongoBinData("ghi")); array("files_id" => 123456789, "n" => 3, "data" => new MongoBinData("jkl")); ?> ]]> Of course, the default chunk size is thousands of bytes, but that makes an unwieldy example.', +'MongoGridFSCursor' => 'Cursor for database file results.', +'MongoGridFSException' => 'Thrown when there are errors reading or writing files to or from the database.', +'MongoGridFSFile' => 'A database file object.', +'MongoId' => 'A unique identifier created for database objects. If an object is inserted into the database without an _id field, an _id field will be added to it with a `MongoId` instance as its value. If the data has a naturally occurring unique field (e.g. username or timestamp) it is fine to use this as the _id field instead, and it will not be replaced with a `MongoId`. + +Instances of the `MongoId` class fulfill the role that autoincrementing does in a relational database: to provide a unique key if the data does not naturally have one. Autoincrementing does not work well with a sharded database, as it is difficult to determine the next number in the sequence. This class fulfills the constraints of quickly generating a value that is unique across shards. + +Each MongoId is 12 bytes (making its string form 24 hexadecimal characters). The first four bytes are a timestamp, the next three are a hash of the client machine\'s hostname, the next two are the two least significant bytes of the process id running the script, and the last three bytes are an incrementing value. + +`MongoId`s are serializable/unserializable. Their serialized form is similar to their string form:', +'MongoInsertBatch' => 'Constructs a batch of INSERT operations. See `MongoWriteBatch`.', +'MongoInt32' => 'The class can be used to save 32-bit integers to the database on a 64-bit system.', +'MongoInt64' => 'The class can be used to save 64-bit integers to the database on a 32-bit system.', +'MongoLog' => 'Logging can be used to get detailed information about what the driver is doing. Logging is disabled by default, but this class allows you to activate specific levels of logging for various parts of the driver. Some examples:', +'MongoMaxKey' => '`MongoMaxKey` is an special type used by the database that compares greater than all other possible BSON values. Thus, if a query is sorted by a given field in ascending order, any document with a `MongoMaxKey` as its value will be returned last. + +`MongoMaxKey` has no associated fields, methods, or constants. It is merely the "greatest" value that can be represented in the database.', +'MongoMinKey' => '`MongoMinKey` is an special type used by the database that compares less than all other possible BSON values. Thus, if a query is sorted by a given field in ascending order, any document with a `MongoMinKey` as its value will be returned first. + +`MongoMinKey` has no associated fields, methods, or constants. It is merely the "smallest" value that can be represented in the database.', +'MongoProtocolException' => 'When talking to MongoDB 2.6.0, and later, certain operations (such as writes) may throw MongoProtocolException when the response from the server did not make sense - for example during network failure (we could read the entire response) or data corruption. + +This exception is also thrown when attempting to talk newer protocols then the server supports, for example using the `MongoWriteBatch` when talking to a MongoDB server prior to 2.6.0.', +'MongoRegex' => 'This class can be used to create regular expressions. Typically, these expressions will be used to query the database and find matching strings. More unusually, they can be saved to the database and retrieved. + +Regular expressions consist of four parts. First a `/` as starting delimiter, then the pattern, another `/` and finally a string containing flags. + +Regular expression pattern + +MongoDB recognizes six regular expression flags:', +'MongoResultException' => 'The MongoResultException is thrown by several command helpers (such as `MongoCollection::findAndModify`) in the event of failure. The original result document is available through `MongoResultException::getDocument`.', +'MongoTimestamp' => '`MongoTimestamp` is an internal type used by MongoDB for replication and sharding. It consists of a 4-byte timestamp (i.e. seconds since the epoch) and a 4-byte increment. This type is not intended for storing time or date values (e.g. a "createdAt" field on a document).', +'MongoUpdateBatch' => 'Constructs a batch of UPDATE operations. See `MongoWriteBatch`.', +'MongoWriteBatch' => 'MongoWriteBatch is the base class for the `MongoInsertBatch`, `MongoUpdateBatch` and `MongoDeleteBatch` classes. + +MongoWriteBatch allows you to "batch up" multiple operations (of same type) and shipping them all to MongoDB at the same time. This can be especially useful when operating on many documents at the same time to reduce roundtrips. + +Prior to version 1.5.0 of the driver it was possible to use `MongoCollection::batchInsert`, however, as of 1.5.0 that method is now discouraged. + +Note: This class is only available when talking to MongoDB 2.6.0 (and later) servers. It will throw `MongoProtocolException` if attempting to use it on older MongoDB servers.', +'MongoWriteConcernException' => 'MongoWriteConcernException is thrown when a write fails. See the PHP manual\'s section on mongo.writeconcerns for how to set failure thresholds. + +Prior to MongoDB 2.6.0, the getLastError command would determine whether a write failed.', +'MultipleIterator' => 'An Iterator that sequentially iterates over all attached iterators', +'Mutex' => 'The static methods contained in the Mutex class provide direct access to Posix Mutex functionality.', +'mysql_xdevapi\Client' => 'Provides access to the connection pool.', +'mysql_xdevapi\Table' => 'Provides access to the table through INSERT/SELECT/UPDATE/DELETE statements.', +'mysql_xdevapi\TableDelete' => 'A statement for delete operations on Table.', +'mysql_xdevapi\TableInsert' => 'A statement for insert operations on Table.', +'mysql_xdevapi\TableSelect' => 'A statement for record retrieval operations on a Table.', +'mysql_xdevapi\TableUpdate' => 'A statement for record update operations on a Table.', +'mysqli' => 'Represents a connection between PHP and a MySQL database.', +'mysqli_driver' => 'MySQLi Driver.', +'mysqli_result' => 'Represents the result set obtained from a query against the database. + +`Changelog`*', +'mysqli_sql_exception' => 'The mysqli exception handling class.', +'mysqli_stmt' => 'Represents a prepared statement.', +'mysqli_warning' => 'Represents a MySQL warning.', +'NoRewindIterator' => 'This iterator cannot be rewound.', +'Normalizer' => 'The Unicode Consortium has defined a number of normalization forms reflecting the various needs of applications: Normalization Form D (NFD) - Canonical Decomposition Normalization Form C (NFC) - Canonical Decomposition followed by Canonical Composition Normalization Form KD (NFKD) - Compatibility Decomposition Normalization Form KC (NFKC) - Compatibility Decomposition followed by Canonical Composition The different forms are defined in terms of a set of transformations on the text, transformations that are expressed by both an algorithm and a set of data files.', +'NumberFormatter' => 'For currencies you can use currency format type to create a formatter that returns a string with the formatted number and the appropriate currency sign. Of course, the NumberFormatter class is unaware of exchange rates so, the number output is the same regardless of the specified currency. This means that the same number has different monetary values depending on the currency locale. If the number is 9988776.65 the results will be: 9 988 776,65 € in France 9.988.776,65 € in Germany $9,988,776.65 in the United States', +'OAuth' => 'The OAuth extension provides a simple interface to interact with data providers using the OAuth HTTP specification to protect private resources.', +'OAuthException' => 'This exception is thrown when exceptional errors occur while using the OAuth extension and contains useful debugging information.', +'OAuthProvider' => 'Manages an OAuth provider class. + +See also an external in-depth tutorial titled Writing an OAuth Provider Service, which takes a hands-on approach to providing this service. There are also OAuth provider examples within the OAuth extensions sources.', +'OCI-Collection' => 'OCI8 Collection functionality.', +'OCI-Lob' => 'OCI8 LOB functionality for large binary (BLOB) and character (CLOB) objects.', +'OCICollection' => 'OCI8 Collection functionality.', +'OCILob' => 'OCI8 LOB functionality for large binary (BLOB) and character (CLOB) objects.', +'OuterIterator' => 'Classes implementing `OuterIterator` can be used to iterate over iterators.', +'OutOfBoundsException' => 'Exception thrown if a value is not a valid key. This represents errors that cannot be detected at compile time.', +'OutOfRangeException' => 'Exception thrown when an illegal index was requested. This represents errors that should be detected at compile time.', +'outputformatObj' => 'Instance of outputformatObj is always embedded inside the `mapObj`_. +It is uses a read only. +No constructor available (coming soon, see ticket 979)', +'OverflowException' => 'Exception thrown when adding an element to a full container.', +'parallel\Channel' => 'An unbuffered channel will block on calls to `parallel\Channel::send` until there is a receiver, and block on calls to `parallel\Channel::recv` until there is a sender. This means an unbuffered channel is not only a way to share data among tasks but also a simple method of synchronization. + +An unbuffered channel is the fastest way to share data among tasks, requiring the least copying.', +'parallel\Events' => 'The Event loop monitors the state of sets of futures and or channels (targets) in order to perform read (`parallel\Future::value`, `parallel\Channel::recv`) and write (`parallel\Channel::send`) operations as the targets become available and the operations may be performed without blocking the event loop.', +'parallel\Events\Event' => 'When an Event is returned, Event::$object shall be removed from the loop that returned it, should the event be a write event the `Input` for Event::$source shall also be removed.', +'parallel\Events\Input' => 'An Input object is a container for data that the `parallel\Events` object will write to `parallel\Channel` objects as they become available. Multiple event loops may share an Input container - parallel does not verify the contents of the container when it is set as the input for a `parallel\Events` object.', +'parallel\Future' => 'A Future represents the return value or uncaught exception from a task, and exposes an API for cancellation. + +The behaviour of a future also allows it to be used as a simple synchronization point even where the task does not return a value explicitly.', +'parallel\Runtime' => 'Each runtime represents a single PHP thread, the thread is created (and bootstrapped) upon construction. The thread then waits for tasks to be scheduled: Scheduled tasks will be executed FIFO and then the thread will resume waiting until more tasks are scheduled, or it\'s closed, killed, or destroyed by the normal scoping rules of PHP objects.', +'parallel\Sync' => 'The `parallel\Sync` class provides access to low level synchronization primitives, mutex, condition variables, and allows the implementation of semaphores. + +Synchronization for most applications is much better implemented using channels, however, in some cases authors of low level code may find it useful to be able to access these lower level mechanisms.', +'ParentIterator' => 'This extended `FilterIterator` allows a recursive iteration using `RecursiveIteratorIterator` that only shows those elements which have children.', +'Parle\ErrorInfo' => 'The class represents detailed error information as supplied by `Parle\Parser::errorInfo`', +'Parle\Lexer' => 'Single state lexer class. Lexemes can be defined on the fly. If the particular lexer instance is meant to be used with `Parle\Parser`, the token IDs need to be taken from there. Otherwise, arbitrary token IDs can be supplied. This lexer can give a certain performance advantage over `Parle\RLexer`, if no multiple states are required. Note, that `Parle\RParser` is not compatible with this lexer.', +'Parle\Parser' => 'Parser class. Rules can be defined on the fly. Once finalized, a `Parle\Lexer` instance is required to deliver the token stream.', +'Parle\RLexer' => 'Multistate lexer class. Lexemes can be defined on the fly. If the particular lexer instance is meant to be used with `Parle\RParser`, the token IDs need to be taken from there. Otherwise, arbitrary token IDs can be supplied. Note, that `Parle\Parser` is not compatible with this lexer.', +'Parle\RParser' => 'Parser class. Rules can be defined on the fly. Once finalized, a `Parle\RLexer` instance is required to deliver the token stream.', +'Parle\Stack' => '`Parle\Stack` is a LIFO stack. The elements are inserted and and removed only from one end.', +'Parle\Token' => 'This class represents a token. Lexer returns instances of this class.', +'ParseError' => '`ParseError` is thrown when an error occurs while parsing PHP code, such as when `eval` is called.', +'PDO' => 'Represents a connection between PHP and a database server.', +'PDOException' => 'Represents an error raised by PDO. You should not throw a `PDOException` from your own code. See Exceptions for more information about Exceptions in PHP.', +'PDOStatement' => 'Represents a prepared statement and, after the statement is executed, an associated result set.', +'Phar' => 'The Phar class provides a high-level interface to accessing and creating phar archives.', +'PharData' => 'The PharData class provides a high-level interface to accessing and creating non-executable tar and zip archives. Because these archives do not contain a stub and cannot be executed by the phar extension, it is possible to create and manipulate regular zip and tar files using the PharData class even if `phar.readonly` php.ini setting is `1`.', +'PharException' => 'The PharException class provides a phar-specific exception class for try/catch blocks.', +'PharFileInfo' => 'The PharFileInfo class provides a high-level interface to the contents and attributes of a single file within a phar archive.', +'php_user_filter' => 'Children of this class are passed to `stream_filter_register`.', +'pht\AtomicInteger' => 'The `pht\AtomicInteger` class is currently the only supported atomic value. It allows for an integer to be safely passed around between, and manipulated, by multiple threads. The methods exposed by this class do not need mutex locking, since they will acquire the internal mutex lock implicitly. `pht\AtomicInteger::lock` and `pht\AtomicInteger::unlock` are still exposed, however, for when multiple operations involving the same `pht\AtomicInteger` object need to be grouped together. + +The mutex locks of the atomic values are reentrant safe.', +'pht\HashTable' => 'The `pht\HashTable` class is one of the Inter-Thread Communication (ITC) data structures exposed by pht. It can be safely passed around between threads, and manipulated by multiple threads using the mutex locks that have been packed in with the data structure. It is reference-counted across threads, and so it does not need to be explicitly destroyed. + +The `pht\HashTable` class enables for array access upon its objects (along with the `isset` and `unset` functions). The `ArrayAccess` interface is not explicitly implemented, however, because it is only needed for such abilities by userland classes.', +'pht\Queue' => 'The `pht\Queue` class is one of the Inter-Thread Communication (ITC) data structures exposed by pht. It can be safely passed around between threads, and manipulated by multiple threads using the mutex locks that have been packed in with the data structure. It is reference-counted across threads, and so it does not need to be explicitly destroyed.', +'pht\Runnable' => 'The `pht\Runnable` interface enforces the implementation of a run() method on classes that should be threaded. This method acts as the entry point of the threaded class.', +'pht\Thread' => 'The `pht\Thread` class abstracts away a native thread. It has an internal task queue, where the methods `pht\Thread::addClassTask`, `pht\Thread::addFunctionTask`, and `pht\Thread::addFileTask` push new tasks onto this queue. Invoking the `pht\Thread::start` method will cause the new thread to be spawned, where it will then begin working through the task queue. A thread may be reused for any number of tasks.', +'pht\Threaded' => 'The `pht\Threaded` interface is an internal interface used by the Inter-Thread Communication (ITC) data structures (`pht\HashTable`, `pht\Queue`, and `pht\Vector`). It allows those data structures to be threaded and ensures that the mutex locking API (`pht\Threaded::lock` and `pht\Threaded::unlock`) is implemented by each of the ITC data structures. It is not implementable by userland classes (since standalone mutex locks are not exposed).', +'pht\Vector' => 'The `pht\Vector` class is one of the Inter-Thread Communication (ITC) data structures exposed by pht. It can be safely passed around between threads, and manipulated by multiple threads using the mutex locks that have been packed in with the data structure. It is reference-counted across threads, and so is does not need to be explicitly destroyed. + +The `pht\Vector` class enables for array access upon its objects (along with the `isset` and `unset` functions). The `ArrayAccess` interface is not explicitly implemented, however, because it is only needed for such abilities by userland classes.', +'Pool' => 'A Pool is a container for, and controller of, an adjustable number of Workers. + +Pooling provides a higher level abstraction of the Worker functionality, including the management of references in the way required by pthreads.', +'querymapObj' => 'Instances of querymapObj are always are always embedded inside the +`mapObj`_.', +'QuickHashIntHash' => 'This class wraps around a hash containing integer numbers, where the values are also integer numbers. Hashes are also available as implementation of the ArrayAccess interface. + +Hashes can also be iterated over with foreach as the Iterator interface is implemented as well. The order of which elements are returned in is not guaranteed.', +'QuickHashIntSet' => 'This class wraps around a set containing integer numbers. + +Sets can also be iterated over with foreach as the Iterator interface is implemented as well. The order of which elements are returned in is not guaranteed.', +'QuickHashIntStringHash' => 'This class wraps around a hash containing integer numbers, where the values are strings. Hashes are also available as implementation of the ArrayAccess interface. + +Hashes can also be iterated over with foreach as the Iterator interface is implemented as well. The order of which elements are returned in is not guaranteed.', +'QuickHashStringIntHash' => 'This class wraps around a hash containing strings, where the values are integer numbers. Hashes are also available as implementation of the ArrayAccess interface. + +Hashes can also be iterated over with foreach as the Iterator interface is implemented as well. The order of which elements are returned in is not guaranteed.', +'RangeException' => 'Exception thrown to indicate range errors during program execution. Normally this means there was an arithmetic error other than under/overflow. This is the runtime version of `DomainException`.', +'RarArchive' => 'This class represents a RAR archive, which may be formed by several volumes (parts) and which contains a number of RAR entries (i.e., files, directories and other special objects such as symbolic links). + +Objects of this class can be traversed, yielding the entries stored in the respective RAR archive. Those entries can also be obtained through `RarArchive::getEntry` and `RarArchive::getEntries`.', +'RarEntry' => 'A RAR entry, representing a directory or a compressed file inside a RAR archive.', +'RarException' => 'This class serves two purposes: it is the type of the exceptions thrown by the RAR extension functions and methods and it allows, through static methods to query and define the error behaviour of the extension, i.e., whether exceptions are thrown or only warnings are emitted. + +The following error codes are used:', +'RdKafka\Conf' => 'Configuration reference: https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md', +'RdKafka\TopicConf' => 'Configuration reference: https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md', +'rectObj' => 'rectObj are sometimes embedded inside other objects. New ones can +also be created with:', +'RecursiveArrayIterator' => 'This iterator allows to unset and modify values and keys while iterating over Arrays and Objects in the same way as the `ArrayIterator`. Additionally it is possible to iterate over the current iterator entry.', +'RecursiveCachingIterator' => '...', +'RecursiveDirectoryIterator' => 'The `RecursiveDirectoryIterator` provides an interface for iterating recursively over filesystem directories.', +'RecursiveFilterIterator' => 'This abstract iterator filters out unwanted values for a `RecursiveIterator`. This class should be extended to implement custom filters. The `RecursiveFilterIterator::accept` must be implemented in the subclass.', +'RecursiveIterator' => 'Classes implementing `RecursiveIterator` can be used to iterate over iterators recursively.', +'RecursiveIteratorIterator' => 'Can be used to iterate through recursive iterators.', +'RecursiveRegexIterator' => 'This recursive iterator can filter another recursive iterator via a regular expression.', +'RecursiveTreeIterator' => 'Allows iterating over a `RecursiveIterator` to generate an ASCII graphic tree.', +'referenceMapObj' => 'Instances of referenceMapObj are always embedded inside the `mapObj`_.', +'Reflection' => 'The reflection class.', +'ReflectionClass' => 'The `ReflectionClass` class reports information about a class.', +'ReflectionClassConstant' => 'The `ReflectionClassConstant` class reports information about a class constant.', +'ReflectionException' => 'The ReflectionException class.', +'ReflectionExtension' => 'The `ReflectionExtension` class reports information about an extension.', +'ReflectionFunction' => 'The `ReflectionFunction` class reports information about a function.', +'ReflectionFunctionAbstract' => 'A parent class to `ReflectionFunction`, read its description for details.', +'ReflectionGenerator' => 'The `ReflectionGenerator` class reports information about a generator.', +'ReflectionMethod' => 'The `ReflectionMethod` class reports information about a method.', +'ReflectionObject' => 'The `ReflectionObject` class reports information about an `object`.', +'ReflectionParameter' => 'The `ReflectionParameter` class retrieves information about function\'s or method\'s parameters. + +To introspect function parameters, first create an instance of the `ReflectionFunction` or `ReflectionMethod` classes and then use their `ReflectionFunctionAbstract::getParameters` method to retrieve an array of parameters.', +'ReflectionProperty' => 'The `ReflectionProperty` class reports information about classes properties.', +'ReflectionType' => 'The `ReflectionType` class reports information about a function\'s return type.', +'Reflector' => '`Reflector` is an interface implemented by all exportable Reflection classes.', +'RegexIterator' => 'This iterator can be used to filter another iterator based on a regular expression.', +'RRDCreator' => 'Class for creation of RRD database file.', +'RRDGraph' => 'Class for exporting data from RRD database to image file.', +'RRDUpdater' => 'Class for updating RDD database file.', +'RuntimeException' => 'Exception thrown if an error which can only be found on runtime occurs.', +'scalebarObj' => 'Instances of scalebarObj are always embedded inside the `mapObj`_.', +'SeekableIterator' => 'The Seekable iterator.', +'SessionHandler' => '`SessionHandler` is a special class that can be used to expose the current internal PHP session save handler by inheritance. There are seven methods which wrap the seven internal session save handler callbacks (open, close, read, write, destroy, gc and create_sid). By default, this class will wrap whatever internal save handler is set as defined by the session.save_handler configuration directive which is usually files by default. Other internal session save handlers are provided by PHP extensions such as SQLite (as sqlite), Memcache (as memcache), and Memcached (as memcached). + +When a plain instance of `SessionHandler` is set as the save handler using `session_set_save_handler` it will wrap the current save handlers. A class extending from `SessionHandler` allows you to override the methods or intercept or filter them by calls the parent class methods which ultimately wrap the internal PHP session handlers. + +This allows you, for example, to intercept the read and write methods to encrypt/decrypt the session data and then pass the result to and from the parent class. Alternatively one might chose to entirely override a method like the garbage collection callback gc. + +Because the `SessionHandler` wraps the current internal save handler methods, the above example of encryption can be applied to any internal save handler without having to know the internals of the handlers. + +To use this class, first set the save handler you wish to expose using session.save_handler and then pass an instance of `SessionHandler` or one extending it to `session_set_save_handler`. + +Please note the callback methods of this class are designed to be called internally by PHP and are not meant to be called from user-space code. The return values are equally processed internally by PHP. For more information on the session workflow, please refer `session_set_save_handler`.', +'SessionHandlerInterface' => '`SessionHandlerInterface` is an interface which defines a prototype for creating a custom session handler. In order to pass a custom session handler to `session_set_save_handler` using its `OOP` invocation, the class must implement this interface. + +Please note the callback methods of this class are designed to be called internally by PHP and are not meant to be called from user-space code.', +'SimpleXMLElement' => 'Represents an element in an XML document.', +'SimpleXMLIterator' => 'The SimpleXMLIterator provides recursive iteration over all nodes of a `SimpleXMLElement` object.', +'SNMP' => 'Represents SNMP session.', +'SNMPException' => 'Represents an error raised by SNMP. You should not throw a `SNMPException` from your own code. See Exceptions for more information about Exceptions in PHP.', +'SoapClient' => 'The SoapClient class provides a client for SOAP 1.1, SOAP 1.2 servers. It can be used in WSDL or non-WSDL mode.', +'SoapFault' => 'Represents a SOAP fault.', +'SoapHeader' => 'Represents a SOAP header.', +'SoapParam' => 'Represents parameter to a SOAP call.', +'SoapServer' => 'The SoapServer class provides a server for the SOAP 1.1 and SOAP 1.2 protocols. It can be used with or without a WSDL service description.', +'SoapVar' => 'A class representing a variable or object for use with SOAP services.', +'SolrClient' => 'Used to send requests to a Solr server. Currently, cloning and serialization of SolrClient instances is not supported.', +'SolrClientException' => 'An exception thrown when there is an error while making a request to the server from the client.', +'SolrCollapseFunction' => 'Class SolrCollapseFunction', +'SolrDisMaxQuery' => 'Version not present on php.net documentation, determined here by using PECL solr changelog: +https://pecl.php.net/package-changelog.php?package=solr&release=2.1.0
+Class SolrDisMaxQuery
', +'SolrDocument' => 'Represents a Solr document retrieved from a query response.', +'SolrDocumentField' => 'This represents a field in a Solr document. All its properties are read-only.', +'SolrException' => 'This is the base class for all exception thrown by the Solr extension classes.', +'SolrGenericResponse' => 'Represents a response from the solr server.', +'SolrIllegalArgumentException' => 'This object is thrown when an illegal or invalid argument is passed to a method.', +'SolrIllegalOperationException' => 'This object is thrown when an illegal or unsupported operation is performed on an object.', +'SolrInputDocument' => 'This class represents a Solr document that is about to be submitted to the Solr index.', +'SolrMissingMandatoryParameterException' => 'Class SolrMissingMandatoryParameterException
', +'SolrModifiableParams' => 'Represents a collection of name-value pairs sent to the Solr server during a request.', +'SolrObject' => 'This is an object whose properties can also by accessed using the array syntax. All its properties are read-only.', +'SolrParams' => 'Represents a collection of name-value pairs sent to the Solr server during a request.', +'SolrPingResponse' => 'Represents a response to a ping request to the server', +'SolrQuery' => 'Represents a collection of name-value pairs sent to the Solr server during a request.', +'SolrQueryResponse' => 'Represents a response to a query request.', +'SolrResponse' => 'Represents a response from the Solr server.', +'SolrServerException' => 'An exception thrown when there is an error produced by the Solr Server itself.', +'SolrUpdateResponse' => 'Represents a response to an update request.', +'SolrUtils' => 'Contains utility methods for retrieving the current extension version and preparing query phrases. + +Also contains method for escaping query strings and parsing XML responses.', +'SphinxClient' => 'The SphinxClient class provides object-oriented interface to Sphinx.', +'SplBool' => 'The SplBool class is used to enforce strong typing of the bool type.', +'SplDoublyLinkedList' => 'The SplDoublyLinkedList class provides the main functionalities of a doubly linked list.', +'SplEnum' => 'SplEnum gives the ability to emulate and create enumeration objects natively in PHP.', +'SplFileInfo' => 'The SplFileInfo class offers a high-level object oriented interface to information for an individual file.', +'SplFileObject' => 'The SplFileObject class offers an object oriented interface for a file.', +'SplFixedArray' => 'The SplFixedArray class provides the main functionalities of array. The main differences between a SplFixedArray and a normal PHP array is that the SplFixedArray is of fixed length and allows only integers within the range as indexes. The advantage is that it allows a faster array implementation.', +'SplFloat' => 'The SplFloat class is used to enforce strong typing of the float type.', +'SplHeap' => 'The SplHeap class provides the main functionalities of a Heap.', +'SplInt' => 'The SplInt class is used to enforce strong typing of the integer type.', +'SplMaxHeap' => 'The SplMaxHeap class provides the main functionalities of a heap, keeping the maximum on the top.', +'SplMinHeap' => 'The SplMinHeap class provides the main functionalities of a heap, keeping the minimum on the top.', +'SplObjectStorage' => 'The SplObjectStorage class provides a map from objects to data or, by ignoring data, an object set. This dual purpose can be useful in many cases involving the need to uniquely identify objects.', +'SplObserver' => 'The `SplObserver` interface is used alongside `SplSubject` to implement the Observer Design Pattern.', +'SplPriorityQueue' => 'The SplPriorityQueue class provides the main functionalities of a prioritized queue, implemented using a max heap.', +'SplQueue' => 'The SplQueue class provides the main functionalities of a queue implemented using a doubly linked list.', +'SplStack' => 'The SplStack class provides the main functionalities of a stack implemented using a doubly linked list.', +'SplString' => 'The SplString class is used to enforce strong typing of the string type.', +'SplSubject' => 'The `SplSubject` interface is used alongside `SplObserver` to implement the Observer Design Pattern.', +'SplTempFileObject' => 'The SplTempFileObject class offers an object oriented interface for a temporary file.', +'SplType' => 'Parent class for all SPL types.', +'Spoofchecker' => 'This class is provided because Unicode contains large number of characters and incorporates the varied writing systems of the world and their incorrect usage can expose programs or systems to possible security attacks using characters similarity. + +Provided methods allow to check whether an individual string is likely an attempt at confusing the reader (`spoof detection`), such as "pаypаl" spelled with Cyrillic \'а\' characters.', +'SQLite3' => 'A class that interfaces SQLite 3 databases.', +'SQLite3Result' => 'A class that handles result sets for the SQLite 3 extension.', +'SQLite3Stmt' => 'A class that handles prepared statements for the SQLite 3 extension.', +'SQLiteUnbuffered' => 'Represents an unbuffered SQLite result set. Unbuffered results sets are sequential, forward-seeking only.', +'Stackable' => 'Stackable is an alias of Threaded. This class name was used in pthreads until +version 2.0.0', +'Stomp' => 'Represents a connection between PHP and a Stomp compliant Message Broker.', +'StompException' => 'Represents an error raised by the stomp extension. See Exceptions for more information about Exceptions in PHP.', +'StompFrame' => 'Represents a message which was sent or received from a Stomp compliant Message Broker.', +'streamWrapper' => 'Allows you to implement your own protocol handlers and streams for use with all the other filesystem functions (such as `fopen`, `fread` etc.). + +An instance of this class is initialized as soon as a stream function tries to access the protocol it is associated with.', +'StubTests\Parsers\Visitors\ParentConnector' => 'The visitor is required to provide "parent" attribute to nodes', +'styleObj' => 'Instances of styleObj are always embedded inside a `classObj`_ or `labelObj`_.', +'SVMException' => 'The exception object thrown on errors from the SVM and SVMModel classes.', +'SVMModel' => 'The SVMModel is the end result of the training process. It can be used to classify previously unseen data.', +'SWFAction' => 'SWFAction.', +'SWFBitmap' => 'SWFBitmap.', +'SWFButton' => 'SWFButton.', +'SWFDisplayItem' => 'SWFDisplayItem.', +'SWFFill' => 'The `SWFFill` object allows you to transform (scale, skew, rotate) bitmap and gradient fills. + +`swffill` objects are created by the `SWFShape::addFill` method.', +'SWFFont' => 'The `SWFFont` object represent a reference to the font definition, for us with `SWFText::setFont` and `SWFTextField::setFont`.', +'SWFFontChar' => 'SWFFontChar.', +'SWFGradient' => 'SWFGradient.', +'SWFMorph' => 'The methods here are sort of weird. It would make more sense to just have newSWFMorph(shape1, shape2);, but as things are now, shape2 needs to know that it\'s the second part of a morph. (This, because it starts writing its output as soon as it gets drawing commands- if it kept its own description of its shapes and wrote on completion this and some other things would be much easier.)', +'SWFMovie' => '`SWFMovie` is a movie object representing an SWF movie.', +'SWFPrebuiltClip' => 'SWFPrebuiltClip.', +'SWFShape' => 'SWFShape.', +'SWFSound' => 'SWFSound.', +'SWFSoundInstance' => '`SWFSoundInstance` objects are returned by the `SWFSprite::startSound` and `SWFMovie::startSound` methods.', +'SWFSprite' => 'An `SWFSprite` is also known as a "movie clip", this allows one to create objects which are animated in their own timelines. Hence, the sprite has most of the same methods as the movie.', +'SWFText' => 'SWFText.', +'SWFTextField' => 'SWFTextField.', +'SWFVideoStream' => 'SWFVideoStream.', +'SyncEvent' => 'A cross-platform, native implementation of named and unnamed event objects. Both automatic and manual event objects are supported. + +An event object waits, without polling, for the object to be fired/set. One instance waits on the event object while another instance fires/sets the event. Event objects are useful wherever a long-running process would otherwise poll a resource (e.g. checking to see if uploaded data needs to be processed).', +'SyncMutex' => 'A cross-platform, native implementation of named and unnamed countable mutex objects. + +A mutex is a mutual exclusion object that restricts access to a shared resource (e.g. a file) to a single instance. Countable mutexes acquire the mutex a single time and internally track the number of times the mutex is locked. The mutex is unlocked as soon as it goes out of scope or is unlocked the same number of times that it was locked.', +'SyncReaderWriter' => 'A cross-platform, native implementation of named and unnamed reader-writer objects. + +A reader-writer object allows many readers or one writer to access a resource. This is an efficient solution for managing resources where access will primarily be read-only but exclusive write access is occasionally necessary.', +'SyncSemaphore' => 'A cross-platform, native implementation of named and unnamed semaphore objects. + +A semaphore restricts access to a limited resource to a limited number of instances. Semaphores differ from mutexes in that they can allow more than one instance to access a resource at one time while a mutex only allows one instance at a time.', +'SyncSharedMemory' => 'A cross-platform, native, consistent implementation of named shared memory objects. + +Shared memory lets two separate processes communicate without the need for complex pipes or sockets. There are several integer-based shared memory implementations for PHP. Named shared memory is an alternative. + +Synchronization objects (e.g. SyncMutex) are still required to protect most uses of shared memory.', +'Thread' => 'When the start method of a Thread is invoked, the run method code will be executed in separate Thread, in parallel. + +After the run method is executed the Thread will exit immediately, it will be joined with the creating Thread at the appropriate time.', +'Threaded' => 'Threaded objects form the basis of pthreads ability to execute user code in parallel; they expose synchronization methods and various useful interfaces. + +Threaded objects, most importantly, provide implicit safety for the programmer; all operations on the object scope are safe.', +'tidy' => 'An HTML node in an HTML file, as detected by tidy.', +'tidyNode' => 'An HTML node in an HTML file, as detected by tidy.', +'TokyoTyrant' => 'The main Tokyo Tyrant class', +'tokyotyrantexception' => 'TokyoTyrantException', +'TokyoTyrantIterator' => 'Provides an iterator for TokyoTyrant and TokyoTyrantTable objects. The iterator iterates over all keys and values in the database. TokyoTyrantIterator was added in version 0.2.0.', +'TokyoTyrantQuery' => 'This class is used to query the table databases', +'TokyoTyrantTable' => 'Provides an API to the table databases. A table database can be create using the following command: `ttserver -port 1979 /tmp/tt_table.tct`. In Tokyo Tyrant the table API is a schemaless database which can store arbitrary amount of key-value pairs under a single primary key.', +'Traversable' => 'Interface to detect if a class is traversable using `foreach`. + +Abstract base interface that cannot be implemented alone. Instead it must be implemented by either `IteratorAggregate` or `Iterator`.', +'TypeError' => 'There are three scenarios where a `TypeError` may be thrown. The first is where the argument type being passed to a function does not match its corresponding declared parameter type. The second is where a value being returned from a function does not match the declared function return type. The third is where an invalid number of arguments are passed to a built-in PHP function (strict mode only).', +'UI\Area' => 'An Area represents a canvas which can be used to draw, and respond to mouse and key events.', +'UI\Control' => 'This is the closed base class for all UI Controls.', +'UI\Controls\Box' => 'A Box allows the arrangement of other controls', +'UI\Controls\Button' => 'Represents a labelled clickable button', +'UI\Controls\Check' => 'A Check is a labelled checkable box', +'UI\Controls\ColorButton' => 'A Color Button is a button which displays a color picker when clicked', +'UI\Controls\Combo' => 'A Combo control represents a list of options, like the familiar select HTML element.', +'UI\Controls\EditableCombo' => 'An Editable Combo is a Combo which allows the user to enter custom options', +'UI\Controls\Entry' => 'An Entry is a text entry control, suitable for entering plain text, passwords, or search terms.', +'UI\Controls\Form' => 'A Form is a control which allows the arrangement of other controls into a familiar layout (the form).', +'UI\Controls\Grid' => 'A Grid is a control which is allows the arrangement of children into a grid', +'UI\Controls\Group' => 'A Group is a titled container for child controls', +'UI\Controls\Label' => 'A Label is a single line of text, meant to identify, for the user, some element of the interface.', +'UI\Controls\MultilineEntry' => 'A Multiline Entry is a text entry control able to hold multiple lines of text, with or without wrapping.', +'UI\Controls\Picker' => 'A Picker represents a button which when clicked presents a native Date/Time/DateTime Picker interface to the user.', +'UI\Controls\Progress' => 'A Progress control is a familiar Progress bar: It represents progress as a percentage, with a possible range of 0 to 100 (inclusive).', +'UI\Controls\Radio' => 'A Radio is similar to the radio input type familiar from HTML', +'UI\Controls\Separator' => 'A Separator represents a control separator, it has no other function.', +'UI\Controls\Slider' => 'A Slider is a control which represents a range, and a current value in the range. The sliding element of the control (sometimes called the "thumb") reflects the value, and can be adjusted within the range.', +'UI\Controls\Spin' => 'A Spin box is a text box with an up-down control which changes the integer value in the box, within a defined range', +'UI\Controls\Tab' => 'A Tab can contain many pages of Controls, each with a title, each selectable by the user.', +'UI\Draw\Brush' => 'Represents a solid color brush', +'UI\Draw\Brush\Gradient' => 'Abstract for gradient brushes', +'UI\Draw\Color' => 'Represents RGBA colours, individual channels are accessible via public properties.', +'UI\Draw\Path' => 'A Draw Path guides a Draw Pen, telling the Pen where to draw on an Area.', +'UI\Draw\Pen' => 'The Pen is passed to the Area Draw event handler, it is used for clipping, filling, stroking, and writing to Draw Paths.', +'UI\Draw\Stroke' => 'Holds the configuration for the Pen to perform a stroke', +'UI\Draw\Text\Font' => 'Loads a described font', +'UI\Draw\Text\Font\Descriptor' => 'Describes a font', +'UI\Draw\Text\Layout' => 'A Text Layout represents the layout of text which will be drawn by the Pen', +'UI\Executor' => 'This facility schedules repetitive execution of a callback, useful for animations and other such activities.', +'UI\Menu' => 'Menus must be constructed before the first Window, and can be shown on any Window', +'UI\MenuItem' => 'Menu Items should only be created by the Menu', +'UI\Point' => 'Points are used throughout UI to represent co-ordinates on a screen, control, or area.', +'UI\Size' => 'Sizes are used throughout UI to represent the size of a screen, control, or area.', +'UI\Window' => 'Represents a UI Window', +'UnderflowException' => 'Exception thrown when performing an invalid operation on an empty container, such as removing an element.', +'UnexpectedValueException' => 'Exception thrown if a value does not match with a set of values. Typically this happens when a function calls another function and expects the return value to be of a certain type or value not including arithmetic or buffer related errors.', +'V8Js' => 'This is the core class for V8Js extension. Each instance created from this class has own context in which all JavaScript is compiled and executed. + +See `V8Js::__construct` for more information.', +'VARIANT' => 'The VARIANT is COM\'s equivalent of the PHP zval; it is a structure that can contain a value with a range of different possible types. The VARIANT class provided by the COM extension allows you to have more control over the way that PHP passes values to and from COM.', +'Volatile' => 'The `Volatile` class is new to pthreads v3. Its introduction is a consequence of the new immutability semantics of `Threaded` members of `Threaded` classes. The `Volatile` class enables for mutability of its `Threaded` members, and is also used to store PHP arrays in `Threaded` contexts.', +'Vtiful\Kernel\Excel' => 'Create xlsx files and set cells and output xlsx files', +'Vtiful\Kernel\Format' => 'Create a cell format object', +'WeakRef' => 'The WeakRef class provides a gateway to objects without preventing the garbage collector from freeing those objects. It also provides a way to turn a weak reference into a strong one.', +'WeakReference' => 'Weak references allow the programmer to retain a reference to an +object which does not prevent the object from being destroyed. +They are useful for implementing cache like structures.', +'webObj' => 'Instances of webObj are always are always embedded inside the `mapObj`_.', +'wkhtmltox\Image\Converter' => 'Converts an HTML input into various image formats', +'wkhtmltox\PDF\Converter' => 'Converts an HTML input, or set of HTML inputs, into PDF output', +'wkhtmltox\PDF\Object' => 'Represents an HTML document, input to PDF converter', +'Worker' => 'Worker Threads have a persistent context, as such should be used over Threads in most cases. + +When a Worker is started, the run method will be executed, but the Thread will not leave until one of the following conditions are met: + +This means the programmer can reuse the context throughout execution; placing objects on the stack of the Worker will cause the Worker to execute the stacked objects run method.', +'XMLDiff\Base' => 'Base abstract class for all the comparison classes in the extension.', +'XMLReader' => 'The XMLReader extension is an XML Pull parser. The reader acts as a cursor going forward on the document stream and stopping at each node on the way.', +'Yaconf' => 'Yaconf is a configurations container, it parses INIT files, stores the result in PHP when PHP is started, the result lives with the whole PHP lifecycle.', +'Yaf\Action_Abstract' => '

A action can be defined in a separate file in Yaf(see \Yaf\Controller_Abstract). that is a action method can also be a \Yaf\Action_Abstract class.

+
+

Since there should be a entry point which can be called by Yaf (as of PHP 5.3, there is a new magic method __invoke, but Yaf is not only works with PHP 5.3+, Yaf choose another magic method execute), you must implement the abstract method \Yaf\Action_Abstract::execute() in your custom action class.

', +'Yaf\Application' => '\Yaf\Application provides a bootstrapping facility for applications which provides reusable resources, common- and module-based bootstrap classes and dependency checking. +
+Note: +

+\Yaf\Application implements the singleton pattern, and \Yaf\Application can not be serialized or un-serialized which will cause problem when you try to use PHPUnit to write some test case for Yaf.
+You may use @backupGlobals annotation of PHPUnit to control the backup and restore operations for global variables. thus can solve this problem. +

', +'Yaf\Bootstrap_Abstract' => '

Bootstrap is a mechanism used to do some initial config before a Application run.

+

User may define their own Bootstrap class by inheriting \Yaf\Bootstrap_Abstract

+

Any method declared in Bootstrap class with leading "_init", will be called by \Yaf\Application::bootstrap() one by one according to their defined order

', +'Yaf\Config\Ini' => '

\Yaf\Config\Ini enables developers to store configuration data in a familiar INI format and read them in the application by using nested object property syntax. The INI format is specialized to provide both the ability to have a hierarchy of configuration data keys and inheritance between configuration data sections. Configuration data hierarchies are supported by separating the keys with the dot or period character ("."). A section may extend or inherit from another section by following the section name with a colon character (":") and the name of the section from which data are to be inherited.


+Note: +

\Yaf\Config\Ini utilizes the » parse_ini_file() PHP function. Please review this documentation to be aware of its specific behaviors, which propagate to \Yaf\Config\Ini, such as how the special values of "TRUE", "FALSE", "yes", "no", and "NULL" are handled.

', +'Yaf\Controller_Abstract' => '

\Yaf\Controller_Abstract is the heart of Yaf\'s system. MVC stands for Model-View-Controller and is a design pattern targeted at separating application logic from display logic.

+
+

Every custom controller shall inherit \Yaf\Controller_Abstract.

+
+

You will find that you can not define __construct function for your custom controller, thus, \Yaf\Controller_Abstract provides a magic method: \Yaf\Controller_Abstract::init().

+
+

If you have defined a init() method in your custom controller, it will be called as long as the controller was instantiated.

+
+

Action may have arguments, when a request coming, if there are the same name variable in the request parameters(see \Yaf\Request_Abstract::getParam()) after routed, Yaf will pass them to the action method (see \Yaf\Action_Abstract::execute()).

+
+Note: +

These arguments are directly fetched without filtering, it should be carefully processed before use them.

', +'Yaf\Dispatcher' => '

\Yaf\Dispatcher purpose is to initialize the request environment, route the incoming request, and then dispatch any discovered actions; it aggregates any responses and returns them when the process is complete.


+

\Yaf\Dispatcher also implements the Singleton pattern, meaning only a single instance of it may be available at any given time. This allows it to also act as a registry on which the other objects in the dispatch process may draw.

', +'Yaf\Loader' => '

\Yaf\Loader introduces a comprehensive autoloading solution for Yaf.

+
+

The first time an instance of \Yaf\Application is retrieved, \Yaf\Loader will instance a singleton, and registers itself with spl_autoload. You retrieve an instance using the \Yaf\Loader::getInstance()

+
+

\Yaf\Loader attempt to load a class only one shot, if failed, depend on yaf.use_spl_autoload, if this config is On \Yaf\Loader::autoload() will return FALSE, thus give the chance to other autoload function. if it is Off (by default), \Yaf\Loader::autoload() will return TRUE, and more important is that a very useful warning will be triggered (very useful to find out why a class could not be loaded).

+
+Note: +

Please keep yaf.use_spl_autoload Off unless there is some library have their own autoload mechanism and impossible to rewrite it.

+
+

If you want \Yaf\Loader search some classes(libraries) in the local class directory(which is defined in application.ini, and by default, it is application.directory . "/library"), you should register the class prefix using the \Yaf\Loader::registerLocalNameSpace()

', +'Yaf\Plugin_Abstract' => '

Plugins allow for easy extensibility and customization of the framework.

+
+

Plugins are classes. The actual class definition will vary based on the component -- you may need to implement this interface, but the fact remains that the plugin is itself a class.

+
+

A plugin could be loaded into Yaf by using \Yaf\Dispatcher::registerPlugin(), after registered, All the methods which the plugin implemented according to this interface, will be called at the proper time.

', +'Yaf\Registry' => '

All methods of \Yaf\Registry declared as static, making it universally accessible. This provides the ability to get or set any custom data from anyway in your code as necessary.

', +'Yaf\Request\Simple' => '\Yaf\Request\Simple is particularly used for test purpose. ie. simulate a spacial request under CLI mode.', +'Yaf\Route\Map' => '

\Yaf\Route\Map is a built-in route, it simply convert a URI endpoint (that part of the URI which comes after the base URI: see \Yaf\Request_Abstract::setBaseUri()) to a controller name or action name(depends on the parameter passed to \Yaf\Route\Map::__construct()) in following rule: A => controller A. A/B/C => controller A_B_C. A/B/C/D/E => controller A_B_C_D_E.

+
+

If the second parameter of \Yaf\Route\Map::__construct() is specified, then only the part before delimiter of URI will used to routing, the part after it is used to routing request parameters (see the example section of \Yaf\Route\Map::__construct()).

', +'Yaf\Route\Regex' => '

\Yaf\Route\Regex is the most flexible route among the Yaf built-in routes.

', +'Yaf\Route\Rewrite' => '

For usage, please see the example section of \Yaf\Route\Rewrite::__construct()

', +'Yaf\Route\Simple' => '

\Yaf\Route\Simple will match the query string, and find the route info.

+
+

all you need to do is tell \Yaf\Route\Simple what key in the $_GET is module, what key is controller, and what key is action.

+
+

\Yaf\Route\Simple::route() will always return TRUE, so it is important put \Yaf\Route\Simple in the front of the Route stack, otherwise all the other routes will not be called

', +'Yaf\Route_Interface' => '\Yaf\Route_Interface used for developer defined their custom route.', +'Yaf\Route_Static' => '

by default, \Yaf\Router only have a \Yaf\Route_Static as its default route.

+
+

\Yaf\Route_Static is designed to handle 80% of normal requirements.

+
+Note: +

it is unnecessary to instance a \Yaf\Route_Static, also unnecessary to add it into \Yaf\Router\'s routes stack, since there is always be one in \Yaf\Router\'s routes stack, and always be called at the last time.

', +'Yaf\Router' => '

\Yaf\Router is the standard framework router. Routing is the process of taking a URI endpoint (that part of the URI which comes after the base URI: see \Yaf\Request_Abstract::setBaseUri()) and decomposing it into parameters to determine which module, controller, and action of that controller should receive the request. This values of the module, controller, action and other parameters are packaged into a \Yaf\Request_Abstract object which is then processed by \Yaf\Dispatcher. Routing occurs only once: when the request is initially received and before the first controller is dispatched. \Yaf\Router is designed to allow for mod_rewrite-like functionality using pure PHP structures. It is very loosely based on Ruby on Rails routing and does not require any prior knowledge of webserver URL rewriting

+
+Default Route +
+

\Yaf\Router comes pre-configured with a default route \Yaf\Route_Static, which will match URIs in the shape of controller/action. Additionally, a module name may be specified as the first path element, allowing URIs of the form module/controller/action. Finally, it will also match any additional parameters appended to the URI by default - controller/action/var1/value1/var2/value2.

+
+Note: +

Module name must be defined in config, considering application.module="Index,Foo,Bar", in this case, only index, foo and bar can be considered as a module name. if doesn\'t config, there is only one module named "Index".

+
+

** See examples by opening the external documentation

', +'Yaf\View\Simple' => '\Yaf\View\Simple is the built-in template engine in Yaf, it is a simple but fast template engine, and only support PHP script template.', +'Yaf\View_Interface' => 'Yaf provides a ability for developers to use custom view engine instead of built-in engine which is \Yaf\View\Simple. There is a example to explain how to do this, please see \Yaf\Dispatcher::setView()', +'Yaf_Action_Abstract' => 'A action can be defined in a separate file in Yaf(see `Yaf_Controller_Abstract`). that is a action method can also be a `Yaf_Action_Abstract` class. + +Since there should be a entry point which can be called by Yaf (as of PHP 5.3, there is a new magic method __invoke, but Yaf is not only works with PHP 5.3+, Yaf choose another magic method execute), you must implement the abstract method `Yaf_Action_Abstract::execute` in your custom action class.', +'Yaf_Application' => '`Yaf_Application` provides a bootstrapping facility for applications which provides reusable resources, common- and module-based bootstrap classes and dependency checking. + +`Yaf_Application` implements the singleton pattern, and `Yaf_Application` can not be serialized or unserialized which will cause problem when you try to use PHPUnit to write some test case for Yaf. You may use @backupGlobals annotation of PHPUnit to control the backup and restore operations for global variables. thus can solve this problem.', +'Yaf_Bootstrap_Abstract' => 'Bootstrap is a mechanism used to do some initial config before a Application run. + +User may define their own Bootstrap class by inheriting `Yaf_Bootstrap_Abstract` + +Any method declared in Bootstrap class with leading "_init", will be called by `Yaf_Application::bootstrap` one by one according to their defined order.', +'Yaf_Config_Ini' => 'Yaf_Config_Ini enables developers to store configuration data in a familiar INI format and read them in the application by using nested object property syntax. The INI format is specialized to provide both the ability to have a hierarchy of configuration data keys and inheritance between configuration data sections. Configuration data hierarchies are supported by separating the keys with the dot or period character ("."). A section may extend or inherit from another section by following the section name with a colon character (":") and the name of the section from which data are to be inherited. Yaf_Config_Ini utilizes the » parse_ini_file() PHP function. Please review this documentation to be aware of its specific behaviors, which propagate to Yaf_Config_Ini, such as how the special values of "`true`", "`false`", "yes", "no", and "`null`" are handled.', +'Yaf_Controller_Abstract' => '`Yaf_Controller_Abstract` is the heart of Yaf\'s system. MVC stands for Model-View-Controller and is a design pattern targeted at separating application logic from display logic. + +Every custom controller shall inherit `Yaf_Controller_Abstract`. + +You will find that you can not define __construct function for your custom controller, thus, `Yaf_Controller_Abstract` provides a magic method: `Yaf_Controller_Abstract::init`. + +If you have defined a init() method in your custom controller, it will be called as long as the controller was instantiated. + +Action may have arguments, when a request coming, if there are the same name variable in the request parameters(see `Yaf_Request_Abstract::getParam`) after routed, Yaf will pass them to the action method (see `Yaf_Action_Abstract::execute`). These arguments are directly fetched without filtering, it should be carefully processed before use them.', +'Yaf_Dispatcher' => '`Yaf_Dispatcher` purpose is to initialize the request environment, route the incoming request, and then dispatch any discovered actions; it aggregates any responses and returns them when the process is complete. + +`Yaf_Dispatcher` also implements the Singleton pattern, meaning only a single instance of it may be available at any given time. This allows it to also act as a registry on which the other objects in the dispatch process may draw.', +'Yaf_Loader' => '`Yaf_Loader` introduces a comprehensive autoloading solution for Yaf. + +The first time an instance of `Yaf_Application` is retrieved, `Yaf_Loader` will instance a singleton, and registers itself with spl_autoload. You retrieve an instance using the `Yaf_Loader::getInstance` + +`Yaf_Loader` attempt to load a class only one shot, if failed, depend on yaf.use_spl_auload, if this config is On `Yaf_Loader::autoload` will return `false`, thus give the chance to other autoload function. if it is Off (by default), `Yaf_Loader::autoload` will return `true`, and more important is that a very useful warning will be triggered (very useful to find out why a class could not be loaded). Please keep yaf.use_spl_autoload Off unless there is some library have their own autoload mechanism and impossible to rewrite it. + +By default, `Yaf_Loader` assume all library (class defined script) store in the global library directory, which is defined in the php.ini(yaf.library). + +If you want `Yaf_Loader` search some classes(libraries) in the local class directory(which is defined in application.ini, and by default, it is application.directory . "/library"), you should register the class prefix using the `Yaf_Loader::registerLocalNameSpace` + +Let\'s see some examples(assuming APPLICATION_PATH is application.directory): Config example Assuming the following local name space registered: Register localnamespace registerLocalNameSpace(array("Foo", "Bar")); } ?> ]]> Then the autoload examples: Load class example // APPLICATION_PATH/library/Foo/Bar/Test.php class GLO_Name => // /global_dir/Glo/Name.php class BarNon_Test // /global_dir/Barnon/Test.php ]]> As of PHP 5.3, you can use namespace: Load namespace class example // APPLICATION_PATH/library/Foo/Bar/Dummy.php class \FooBar\Bar\Dummy => // /global_dir/FooBar/Bar/Dummy.php ]]> + +You may noticed that all the folder with the first letter capitalized, you can make them lowercase by set yaf.lowcase_path = On in php.ini + +`Yaf_Loader` is also designed to load the MVC classes, and the rule is: MVC class loading example // APPLICATION_PATH/controllers/ Model Classes => // APPLICATION_PATH/models/ Plugin Classes => // APPLICATION_PATH/plugins/ ]]> Yaf identify a class\'s suffix(this is by default, you can also change to the prefix by change the configure yaf.name_suffix) to decide whether it is a MVC class: MVC class distinctions // ***Controller Model Classes => // ***Model Plugin Classes => // ***Plugin ]]> some examples: MVC loading example // APPLICATION_PATH/models/Data.php class DummyPlugin => // APPLICATION_PATH/plugins/Dummy.php class A_B_TestModel => // APPLICATION_PATH/models/A/B/Test.php ]]> As of 2.1.18, Yaf supports Controllers autoloading for user script side, (which means the autoloading triggered by user php script, eg: access a controller static property in Bootstrap or Plugins), but autoloader only try to locate controller class script under the default module folder, which is "APPLICATION_PATH/controllers/". also, the directory will be affected by yaf.lowcase_path.', +'Yaf_Plugin_Abstract' => 'Plugins allow for easy extensibility and customization of the framework. + +Plugins are classes. The actual class definition will vary based on the component -- you may need to implement this interface, but the fact remains that the plugin is itself a class. + +A plugin could be loaded into Yaf by using `Yaf_Dispatcher::registerPlugin`, after registering, All the methods which the plugin implemented according to this interface, will be called at the proper time.', +'Yaf_Registry' => 'All methods of `Yaf_Registry` declared as static, making it unversally accessible. This provides the ability to get or set any custom data from anyway in your code as necessary.', +'Yaf_Request_Http' => 'Any request from client is initialized as a `Yaf_Request_Http`. you can get the request information like, uri query and post parameters via methods of this class. For security, $_GET/$_POST are readonly in Yaf, which means if you set a value to these global variables, you can not get it from `Yaf_Request_Http::getQuery` or `Yaf_Request_Http::getPost`. But there do is some usage need such feature, like unit testing. thus Yaf can be built with --enable-yaf-debug, which will allow Yaf read the value user set via script. in such case, Yaf will throw a E_STRICT warning to remind you about that: Strict Standards: you are running yaf in debug mode', +'Yaf_Request_Simple' => '`Yaf_Request_Simple` is particularlly used for test puporse. ie. simulate some espacial request under CLI mode.', +'Yaf_Route_Interface' => '`Yaf_Route_Interface` used for developer defined their custom route.', +'Yaf_Route_Map' => '`Yaf_Route_Map` is a built-in route, it simply convert a URI endpoint (that part of the URI which comes after the base URI: see `Yaf_Request_Abstract::setBaseUri`) to a controller name or action name(depends on the parameter passed to `Yaf_Route_Map::__construct`) in following rule: A => controller A. A/B/C => controller A_B_C. A/B/C/D/E => controller A_B_C_D_E. + +If the second parameter of `Yaf_Route_Map::__construct` is specified, then only the part before delimiter of URI will used to routing, the part after it is used to routing request parameters (see the example section of `Yaf_Route_Map::__construct`).', +'Yaf_Route_Regex' => '`Yaf_Route_Regex` is the most flexible route among the Yaf built-in routes.', +'Yaf_Route_Rewrite' => 'For usage, please see the example section of `Yaf_Route_Rewrite::__construct`', +'Yaf_Route_Simple' => '`Yaf_Route_Simple` will match the query string, and find the route info. + +all you need to do is tell `Yaf_Route_Simple` what key in the $_GET is module, what key is controller, and what key is action. + +`Yaf_Route_Simple::route` will always return `true`, so it is important put `Yaf_Route_Simple` in the front of the Route stack, otherwise all the other routes will not be called.', +'Yaf_Route_Static' => 'Defaultly, `Yaf_Router` only have a `Yaf_Route_Static` as its default route. + +And `Yaf_Route_Static` is designed to handle the 80% requirement. + +please *NOTE* that it is unnecessary to instance a `Yaf_Route_Static`, also unecesary to add it into `Yaf_Router`\'s routes stack, since there is always be one in `Yaf_Router`\'s routes stack, and always be called at the last time.', +'Yaf_Router' => '`Yaf_Router` is the standard framework router. Routing is the process of taking a URI endpoint (that part of the URI which comes after the base URI: see `Yaf_Request_Abstract::setBaseUri`) and decomposing it into parameters to determine which module, controller, and action of that controller should receive the request. This values of the module, controller, action and other parameters are packaged into a `Yaf_Request_Abstract` object which is then processed by `Yaf_Dispatcher`. Routing occurs only once: when the request is initially received and before the first controller is dispatched. `Yaf_Router` is designed to allow for mod_rewrite-like functionality using pure PHP structures. It is very loosely based on Ruby on Rails routing and does not require any prior knowledge of webserver URL rewriting. It is designed to work with a single Apache mod_rewrite rule (one of): Rewrite rule for Apache or (preferred): Rewrite rule for Apache If using Lighttpd, the following rewrite rule is valid: Rewrite rule for Lighttpd "/index.php?$1", ".*\.(js|ico|gif|jpg|png|css|html)$" => "$0", "" => "/index.php" ) ]]> If using Nginx, use the following rewrite rule: Rewrite rule for Nginx', +'Yaf_View_Interface' => 'Yaf provides a ability for developers to use coustom view engine instead of built-in engine which is `Yaf_View_Simple`. There is a example to explain how to do this, please see `Yaf_Dispatcher::setView`.', +'Yaf_View_Simple' => '`Yaf_View_Simple` is the built-in template engine in Yaf, it is a simple but fast template engine, and only support PHP script template.', +'Yar_Client_Exception' => 'Class Yar_Client_Exception +Date 2018/6/9 下午3:05', +'Yar_Server' => 'Class Yar_Server +Date 2018/6/9 下午3:02', +'Yar_Server_Exception' => 'If service threw exceptions, A Yar_Server_Exception will be threw in client side.', +'ZendAPI_Job' => 'Describing a job in a queue +In order to add/modify a job in the queue, a Job class must be created/retrieved and than saved in a queue + +For simplicity, a job can be added directly to a queue and without creating an instant of a Queue object', +'ZipArchive' => 'A file archive, compressed with Zip.', +'ZMQ' => 'Class ZMQ', +'ZMQContext' => 'Class ZMQContext', +'ZMQDevice' => 'Class ZMQDevice', +'ZMQPoll' => 'Class ZMQPoll', +'ZMQSocket' => 'Class ZMQSocket', +'Zookeeper' => 'Represents ZooKeeper session.', +'ZookeeperAuthenticationException' => 'The ZooKeeper authentication exception handling class.', +'ZookeeperConfig' => 'The ZooKeeper Config handling class.', +'ZookeeperConnectionException' => 'The ZooKeeper connection exception handling class.', +'ZookeeperException' => 'The ZooKeeper exception handling class.', +'ZookeeperMarshallingException' => 'The ZooKeeper exception (while marshalling or unmarshalling data) handling class.', +'ZookeeperNoNodeException' => 'The ZooKeeper exception (when node does not exist) handling class.', +'ZookeeperOperationTimeoutException' => 'The ZooKeeper operation timeout exception handling class.', +'ZookeeperSessionException' => 'The ZooKeeper session exception handling class.', +]; diff --git a/bundled-libs/phan/phan/src/Phan/Language/Internal/ConstantDocumentationMap.php b/bundled-libs/phan/phan/src/Phan/Language/Internal/ConstantDocumentationMap.php new file mode 100644 index 000000000..f25530ebc --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Internal/ConstantDocumentationMap.php @@ -0,0 +1,3804 @@ +' => 'documentation', + * + * NOTE: This format will very likely change as information is added and should not be used directly. + * + * Sources of function/method summary info: + * + * 1. docs.php.net's SVN repo or website, and examples (See internal/internalsignatures.php) + * + * See https://secure.php.net/manual/en/copyright.php + * + * The PHP manual text and comments are covered by the [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/legalcode), + * copyright (c) the PHP Documentation Group + * 2. Various websites documenting individual extensions (e.g. php-ast) + * 3. PHPStorm stubs (for anything missing from the above sources) + * See internal/internalsignatures.php + * + * Available from https://github.com/JetBrains/phpstorm-stubs under the [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0) + * + * + * CONTRIBUTING: + * + * Running `internal/internalstubs.php` can be used to update signature maps + * + * There are no plans for these signatures to diverge from what the above upstream sources contain. + * + * - If the descriptions cause Phan to crash, bug reports are welcome + * - If Phan improperly extracted text from a summary (and this affects multiple signatures), patches fixing the extraction will be accepted. + * - Otherwise, fixes for typos/grammar/inaccuracies in the summary will only be accepted once they are contributed upstream and can be regenerated (e.g. to the svn repo for docs.php.net). + * + * Note that the summaries are used in a wide variety of contexts, and what makes sense for Phan may not make sense for those projects, and vice versa. + */ +return [ +'__COMPILER_HALT_OFFSET__' => 'Available since PHP 5.1.0', +'AF_INET6' => 'Only available if compiled with IPv6 support.', +'AL_BITS' => 'Buffer Setting', +'AL_BUFFER' => 'Source/Listener Setting (Integer)', +'AL_CHANNELS' => 'Buffer Setting', +'AL_CONE_INNER_ANGLE' => 'Source/Listener Setting (Float)', +'AL_CONE_OUTER_ANGLE' => 'Source/Listener Setting (Float)', +'AL_CONE_OUTER_GAIN' => 'Source/Listener Setting (Float)', +'AL_DIRECTION' => 'Source/Listener Setting (Float Vector)', +'AL_FALSE' => 'Boolean value recognized by OpenAL', +'AL_FORMAT_MONO16' => 'PCM Format', +'AL_FORMAT_MONO8' => 'PCM Format', +'AL_FORMAT_STEREO16' => 'PCM Format', +'AL_FORMAT_STEREO8' => 'PCM Format', +'AL_FREQUENCY' => 'Buffer Setting', +'AL_GAIN' => 'Source/Listener Setting (Float)', +'AL_INITIAL' => 'Source State', +'AL_LOOPING' => 'Source State', +'AL_MAX_DISTANCE' => 'Source/Listener Setting (Float)', +'AL_MAX_GAIN' => 'Source/Listener Setting (Float)', +'AL_MIN_GAIN' => 'Source/Listener Setting (Float)', +'AL_ORIENTATION' => 'Source/Listener Setting (Float Vector)', +'AL_PAUSED' => 'Source State', +'AL_PITCH' => 'Source/Listener Setting (Float)', +'AL_PLAYING' => 'Source State', +'AL_POSITION' => 'Source/Listener Setting (Float Vector)', +'AL_REFERENCE_DISTANCE' => 'Source/Listener Setting (Float)', +'AL_ROLLOFF_FACTOR' => 'Source/Listener Setting (Float)', +'AL_SIZE' => 'Buffer Setting', +'AL_SOURCE_RELATIVE' => 'Source/Listener Setting (Integer)', +'AL_SOURCE_STATE' => 'Source/Listener Setting (Integer)', +'AL_STOPPED' => 'Source State', +'AL_TRUE' => 'Boolean value recognized by OpenAL', +'AL_VELOCITY' => 'Source/Listener Setting (Float Vector)', +'ALC_FREQUENCY' => 'Context Attribute', +'ALC_REFRESH' => 'Context Attribute', +'ALC_SYNC' => 'Context Attribute', +'ARRAY_FILTER_USE_BOTH' => '`ARRAY_FILTER_USE_BOTH` is used with `array_filter` to pass both value and key to the given callback function. Added in PHP 5.6.0.', +'ARRAY_FILTER_USE_KEY' => '`ARRAY_FILTER_USE_KEY` is used with `array_filter` to pass each key as the first argument to the given callback function. Added in PHP 5.6.0.', +'ast\AST_ARG_LIST' => 'numerically indexed children of an argument list of a function/method invocation', +'ast\AST_ARRAY' => 'numerically indexed children of an array literal.', +'ast\AST_ARRAY_ELEM' => 'An element of an array literal. The key is `null` if there is no key (children: value, key)', +'ast\AST_ARROW_FUNC' => 'A short arrow function declaration. (children: name, docComment, params, stmts, returnType, __declId)', +'ast\AST_ASSIGN' => 'An assignment of the form `var = expr` (children: var, expr)', +'ast\AST_ASSIGN_OP' => 'An assignment operation of the form `var op= expr`. The operation is determined by the flags `ast\flags\BINARY_*` (children: var, expr)', +'ast\AST_ASSIGN_REF' => 'An assignment by reference, of the form `var =& expr`. (children: var, expr)', +'ast\AST_ATTRIBUTE' => 'A single attribute of an element of the form `class(args)` (children: class, args)', +'ast\AST_ATTRIBUTE_GROUP' => 'Numerically indexed ast\AST_ATTRIBUTE children of an attribute group', +'ast\AST_ATTRIBUTE_LIST' => 'Numerically indexed ast\AST_ATTRIBUTE_GROUP children of an attribute list of an element', +'ast\AST_BINARY_OP' => 'A binary operation of the form `left op right`. The operation is determined by the flags `ast\flags\BINARY_*` (children: left, right)', +'ast\AST_BREAK' => 'A break statement. The depth is null or an integer. (children: depth)', +'ast\AST_CALL' => 'A global function invocation of the form `expr(args)` (children: expr, args)', +'ast\AST_CAST' => 'A cast operation of the form `(type)expr`. The flags are the type: `ast\flags\TYPE_*` (children: expr)', +'ast\AST_CATCH' => 'An individual catch block catching an `ast\AST_CLASS_LIST` of possible classes. This is of the form `catch(class var) { stmts }`. (children: class, var, stmts)', +'ast\AST_CATCH_LIST' => 'A list of 1 or more `ast\AST_CATCH` nodes for a try statement. (numerically indexed children)', +'ast\AST_CLASS' => 'A class declaration of the form `docComment name EXTENDS extends IMPLEMENTS implements { stmts }` (children: name, docComment, extends, implements, stmts, __declId)', +'ast\AST_CLASS_CONST' => 'A class constant usage of the form `class::const`. (children: class, const)', +'ast\AST_CLASS_CONST_DECL' => 'A class constant declaration with one or more class constants. (numerically indexed children)', +'ast\AST_CLASS_CONST_GROUP' => 'A class constant declaration with attributes and the list of one or more class constants. (children: const, attributes)', +'ast\AST_CLASS_NAME' => 'A usage of `class::CLASS` in AST version 70 (children: class)', +'ast\AST_CLONE' => 'An expression cloning an object, of the form `clone(expr)`. (children: expr)', +'ast\AST_CLOSURE' => 'A closure declaration. (children: name, docComment, params, uses, stmts, returnType, __declId)', +'ast\AST_CLOSURE_USES' => 'A list of one or more nodes of type `ast\AST_CLOSURE_VAR` for a closure declaration. (numerically indexed children)', +'ast\AST_CLOSURE_VAR' => 'A variable in the list of `uses` of a closure declaration. (children: name)', +'ast\AST_CONDITIONAL' => 'A conditional expression of the form `cond ? true : false` or `cond ?: false`. (children: cond, true, false)', +'ast\AST_CONST' => 'A usage of a global constant `name`. (children: name)', +'ast\AST_CONST_DECL' => 'A declaration of a group of class constants of kind `ast\AST_CONST_ELEM`. (numerically indexed children)', +'ast\AST_CONST_ELEM' => 'A declaration of a class constant. (children: name, value, docComment)', +'ast\AST_CONTINUE' => 'A continue statement with a depth of `null` or an integer. (children: depth)', +'ast\AST_DECLARE' => 'A declare statement at the top of a php file (children: declares, stmts)', +'ast\AST_DIM' => 'A usage of an array/array-like field, of the form `expr[dim]` (children: expr, dim)', +'ast\AST_DO_WHILE' => 'A do-while statement of the form `do {stmts} while (cond);`. (children: stmts, cond)', +'ast\AST_ECHO' => 'An echo statement or inline HTML (children: expr)', +'ast\AST_EMPTY' => 'An `empty(expr)` expression (children: expr)', +'ast\AST_ENCAPS_LIST' => 'interpolated string with non-literals, e.g. `"foo$bar"` or heredoc (numerically indexed children)', +'ast\AST_EXIT' => 'An `exit`/`die` statement. (children: expr)', +'ast\AST_EXPR_LIST' => 'A comma separated list of expressions, e.g. for the condition of a `for` loop. (numerically indexed children)', +'ast\AST_FOR' => 'A for loop of the form `for (init; cond; loop) { stmts; }`. (children: init, cond, loop, stmts)', +'ast\AST_FOREACH' => 'A foreach loop of the form `foreach (expr as [key =>] value) {stmts} (children: expr, value, key, stmts)', +'ast\AST_FUNC_DECL' => 'A global function declaration. (children: name, docComment, params, stmts, returnType, __declId)', +'ast\AST_GLOBAL' => 'A usage of a global variable of the form `global var`. (children: var)', +'ast\AST_GOTO' => 'A goto statement of the form `goto label;` (children: label)', +'ast\AST_GROUP_USE' => 'A use statement (for classes, namespaces, functions, and/or constants) containing a list of one or more elements. (children: prefix, uses)', +'ast\AST_HALT_COMPILER' => 'A `__halt_compiler;` statement. (children: offset)', +'ast\AST_IF' => 'A list of `ast\AST_IF_ELEM` nodes for a chain of 1 or more `if`/`elseif`/`else` statements (numerically indexed children)', +'ast\AST_IF_ELEM' => 'An `if`/`elseif`/`elseif` statement of the form `if (cond) stmts` (children: cond, stmts)', +'ast\AST_INCLUDE_OR_EVAL' => 'An `include*(expr)`, `require*(expr)`, or `eval(expr)` statement. The type can be determined from the flags (`ast\flags\EXEC_*`). (children: expr)', +'ast\AST_INSTANCEOF' => 'An `expr instanceof class` expression. (children: expr, class)', +'ast\AST_ISSET' => 'An `isset(var)` expression. (children: var)', +'ast\AST_LABEL' => 'A `name:` expression (a target for `goto name`). (children: name)', +'ast\AST_LIST' => 'Used for `list() = ` unpacking, etc. Predates AST version 50. (numerically indexed children)', +'ast\AST_MAGIC_CONST' => 'A magic constant (depends on flags that are one of `ast\flags\MAGIC_*`)', +'ast\AST_MATCH' => 'A match expression of the form `match(cond) { stmts }` (children: cond, stmts)', +'ast\AST_MATCH_ARM' => 'An arm of a match expression of the form `cond => expr` (children: cond, expr)', +'ast\AST_MATCH_ARM_LIST' => 'Numerically indexed children of the kind `ast\AST_MATCH_ARM` for the statements of a match expression', +'ast\AST_METHOD' => 'A method declaration. (children: name, docComment, params, stmts, returnType, __declId)', +'ast\AST_METHOD_CALL' => 'An invocation of an instance method, of the form `expr->method(args)` (children: expr, method, args)', +'ast\AST_METHOD_REFERENCE' => 'A reference to a method when using a trait inside a class declaration. (children: class, method)', +'ast\AST_NAME' => 'A name token (e.g. of a constant/class/class type) (children: name)', +'ast\AST_NAMED_ARG' => 'A named argument in an argument list of a function/method call. (children: name, expr)', +'ast\AST_NAME_LIST' => 'A list of names (e.g. for catching multiple classes in a `catch` statement) (numerically indexed children)', +'ast\AST_NAMESPACE' => 'A namespace declaration of the form `namespace name;` or `namespace name { stmts }`. (children: name, stmts)', +'ast\AST_NEW' => 'An object creation expression of the form `new class(args)` (children: class, args)', +'ast\AST_NULLABLE_TYPE' => 'A nullable node with a child node of kind `ast\AST_TYPE` or `ast\AST_NAME` (children: type)', +'ast\AST_NULLSAFE_METHOD_CALL' => 'A nullsafe method call of the form `expr?->method(args)`. (children: expr, method, args)', +'ast\AST_NULLSAFE_PROP' => 'A nullsafe property read of the form `expr?->prop`. (children: expr, prop)', +'ast\AST_PARAM' => 'A parameter of a function, closure, or method declaration. (children: type, name, default)', +'ast\AST_PARAM_LIST' => 'The list of parameters of a function, closure, or method declaration. (numerically indexed children)', +'ast\AST_POST_DEC' => 'A `var--` expression. (children: var)', +'ast\AST_POST_INC' => 'A `var++` expression. (children: var)', +'ast\AST_PRE_DEC' => 'A `--var` expression. (children: var)', +'ast\AST_PRE_INC' => 'A `++var` expression. (children: var)', +'ast\AST_PRINT' => 'A `print expr` expression. (children: expr)', +'ast\AST_PROP' => 'An instance property usage of the form `expr->prop` (children: expr, prop)', +'ast\AST_PROP_DECL' => 'A single group of property declarations inside a class. (numerically indexed children)', +'ast\AST_PROP_ELEM' => 'A class property declaration. (children: name, default, docComment)', +'ast\AST_PROP_GROUP' => 'A class property group declaration with optional type information for the group (in PHP 7.4+). Used in AST version 70+ (children: type, props, __declId)', +'ast\AST_REF' => 'only used for `&$v` in `foreach ($a as &$v)` (children: var)', +'ast\AST_RETURN' => 'A `return;` or `return expr` statement. (children: expr)', +'ast\AST_SHELL_EXEC' => 'A `\`some_shell_command\`` expression. (children: expr)', +'ast\AST_STATIC' => 'A declaration of a static local variable of the form `static var = default`. (children: var, default)', +'ast\AST_STATIC_CALL' => 'A call of a static method, of the form `class::method(args)`. (children: class, method, args)', +'ast\AST_STATIC_PROP' => 'A usage of a static property, of the form `class::prop`. (children: class, prop)', +'ast\AST_STMT_LIST' => 'A list of statements. The statements are usually nodes but can be non-nodes, e.g. `;2;`. (numerically indexed children)', +'ast\AST_SWITCH' => 'A switch statement of the form `switch(cond) { stmts }`. `stmts` is a node of the kind `ast\AST_SWITCH_LIST`. (children: cond, stmts)', +'ast\AST_SWITCH_CASE' => 'A case statement of a switch, of the form `case cond: stmts` (children: cond, stmts)', +'ast\AST_SWITCH_LIST' => 'The full list of nodes inside a switch statement body, each of kind `ast\AST_SWITCH_CASE`. (numerically indexed children)', +'ast\AST_THROW' => 'A throw statement of the form `throw expr;` (children: expr)', +'ast\AST_TRAIT_ADAPTATIONS' => 'The optional adaptations to a statement using a trait inside a class (numerically indexed children)', +'ast\AST_TRAIT_ALIAS' => 'Adds an alias inside of a use of a trait (`method as alias`) (children: method, alias)', +'ast\AST_TRAIT_PRECEDENCE' => 'Indicates the precedent of a trait over another trait (`method INSTEADOF insteadof`) (children: method, insteadof)', +'ast\AST_TRY' => 'A try/catch(es)/finally block. (children: try, catches, finally)', +'ast\AST_TYPE' => 'A type such as `bool`. Other types such as specific classes are represented as `ast\AST_NAME`s (depends on flags of `ast\flags\TYPE_*`)', +'ast\AST_TYPE_UNION' => 'A union type made up of individual types, such as `bool|int` (numerically indexed children)', +'ast\AST_UNARY_OP' => 'A unary operation of the form `op expr` (e.g. `-expr`, the flags can be one of `ast\flags\UNARY_*`) (children: expr)', +'ast\AST_UNPACK' => 'An expression unpacking an array/iterable (i.e. `...expr`) (children: expr)', +'ast\AST_UNSET' => '`unset(var)` - A statement unsetting the expression `var` (children: var)', +'ast\AST_USE' => 'A list of uses of classes/namespaces, functions, and constants. The child nodes are of the kind `ast\AST_USE_ELEM` (numerically indexed children)', +'ast\AST_USE_ELEM' => 'A single use statement for a group of (children: name, alias)', +'ast\AST_USE_TRAIT' => 'Represents `use traits {[adaptations]}` within a class declaration (children: traits, adaptations)', +'ast\AST_VAR' => 'An occurrence of a variable `$name` or `${name}`. (children: name)', +'ast\AST_WHILE' => 'A while loop of the form `while (cond) { stmts }` (children: cond, stmts)', +'ast\AST_YIELD' => 'An expression of the form `yield [key =>] value` (children: value, key)', +'ast\AST_YIELD_FROM' => 'An expression of the form `yield from expr` (children: expr)', +'ast\flags\ARRAY_ELEM_REF' => 'Marks an `ast\AST_ARRAY_ELEM` as a reference', +'ast\flags\ARRAY_SYNTAX_LIST' => 'Marks an `ast\AST_ARRAY` as using the `list(...)` syntax', +'ast\flags\ARRAY_SYNTAX_LONG' => 'Marks an `ast\AST_ARRAY` as using the `array(...)` syntax', +'ast\flags\ARRAY_SYNTAX_SHORT' => 'Marks an `ast\AST_ARRAY` as using the `[...]` syntax', +'ast\flags\BINARY_ADD' => 'Marks an ast\AST_BINARY_OP as being a `+`', +'ast\flags\BINARY_BITWISE_AND' => 'Marks an ast\AST_BINARY_OP as being a `&`', +'ast\flags\BINARY_BITWISE_OR' => 'Marks an ast\AST_BINARY_OP as being a `|`', +'ast\flags\BINARY_BITWISE_XOR' => 'Marks an ast\AST_BINARY_OP as being a `^`', +'ast\flags\BINARY_BOOL_AND' => 'Marks an ast\AST_BINARY_OP as being a `&&` or `and`', +'ast\flags\BINARY_BOOL_OR' => 'Marks an ast\AST_BINARY_OP as being a `||` or `or`', +'ast\flags\BINARY_BOOL_XOR' => 'Marks an ast\AST_BINARY_OP as being an `xor`', +'ast\flags\BINARY_COALESCE' => 'Marks an ast\AST_BINARY_OP as being a `??`', +'ast\flags\BINARY_CONCAT' => 'Marks an ast\AST_BINARY_OP as being a `.`', +'ast\flags\BINARY_DIV' => 'Marks an ast\AST_BINARY_OP as being a `/`', +'ast\flags\BINARY_IS_EQUAL' => 'Marks an ast\AST_BINARY_OP as being a `==`', +'ast\flags\BINARY_IS_GREATER' => 'Marks an ast\AST_BINARY_OP as being a `>`', +'ast\flags\BINARY_IS_GREATER_OR_EQUAL' => 'Marks an ast\AST_BINARY_OP as being a `>=`', +'ast\flags\BINARY_IS_IDENTICAL' => 'Marks an ast\AST_BINARY_OP as being a `===`', +'ast\flags\BINARY_IS_NOT_EQUAL' => 'Marks an ast\AST_BINARY_OP as being a `!=`', +'ast\flags\BINARY_IS_NOT_IDENTICAL' => 'Marks an ast\AST_BINARY_OP as being a `!==`', +'ast\flags\BINARY_IS_SMALLER' => 'Marks an ast\AST_BINARY_OP as being a `<`', +'ast\flags\BINARY_IS_SMALLER_OR_EQUAL' => 'Marks an ast\AST_BINARY_OP as being a `<=`', +'ast\flags\BINARY_MOD' => 'Marks an ast\AST_BINARY_OP as being a `%`', +'ast\flags\BINARY_MUL' => 'Marks an ast\AST_BINARY_OP as being a `*`', +'ast\flags\BINARY_POW' => 'Marks an ast\AST_BINARY_OP as being a `**`', +'ast\flags\BINARY_SHIFT_LEFT' => 'Marks an ast\AST_BINARY_OP as being a `<<`', +'ast\flags\BINARY_SHIFT_RIGHT' => 'Marks an ast\AST_BINARY_OP as being a `>>`', +'ast\flags\BINARY_SPACESHIP' => 'Marks an ast\AST_BINARY_OP as being a `<=>`', +'ast\flags\BINARY_SUB' => 'Marks an ast\AST_BINARY_OP as being a `-`', +'ast\flags\CLASS_ABSTRACT' => 'Marks a `ast\AST_CLASS` (class-like declaration) as being abstract', +'ast\flags\CLASS_ANONYMOUS' => 'Marks a `ast\AST_CLASS` (class-like declaration) as being anonymous', +'ast\flags\CLASS_FINAL' => 'Marks a `ast\AST_CLASS` (class-like declaration) as being final', +'ast\flags\CLASS_INTERFACE' => 'Marks a `ast\AST_CLASS` (class-like declaration) as being an interface', +'ast\flags\CLASS_TRAIT' => 'Marks a `ast\AST_CLASS` (class-like declaration) as being a trait', +'ast\flags\CLOSURE_USE_REF' => 'Marks an `ast\AST_CLOSURE_USE` as using a variable by reference', +'ast\flags\DIM_ALTERNATIVE_SYNTAX' => 'Marks an `ast\AST_DIM` as using the alternative `expr{dim}` syntax', +'ast\flags\EXEC_EVAL' => 'Marks an `ast\AST_EXEC` as being an `eval(...)`', +'ast\flags\EXEC_INCLUDE' => 'Marks an `ast\AST_EXEC` as being an `include`', +'ast\flags\EXEC_INCLUDE_ONCE' => 'Marks an `ast\AST_EXEC` as being an `include_once`', +'ast\flags\EXEC_REQUIRE' => 'Marks an `ast\AST_EXEC` as being a `require`', +'ast\flags\EXEC_REQUIRE_ONCE' => 'Marks an `ast\AST_EXEC` as being a `require_once`', +'ast\flags\FUNC_GENERATOR' => 'Marks an `ast\AST_FUNC_DECL` as being a generator', +'ast\flags\FUNC_RETURNS_REF' => 'Marks an `ast\AST_FUNC_DECL` as returning a value by reference', +'ast\flags\MAGIC_CLASS' => 'Marks an `ast\AST_MAGIC_CONST` as being `__CLASS__`', +'ast\flags\MAGIC_DIR' => 'Marks an `ast\AST_MAGIC_CONST` as being `__DIR__`', +'ast\flags\MAGIC_FILE' => 'Marks an `ast\AST_MAGIC_CONST` as being `__FILE__`', +'ast\flags\MAGIC_FUNCTION' => 'Marks an `ast\AST_MAGIC_CONST` as being `__FUNCTION__`', +'ast\flags\MAGIC_LINE' => 'Marks an `ast\AST_MAGIC_CONST` as being `__LINE__`', +'ast\flags\MAGIC_METHOD' => 'Marks an `ast\AST_MAGIC_CONST` as being `__METHOD__`', +'ast\flags\MAGIC_NAMESPACE' => 'Marks an `ast\AST_MAGIC_CONST` as being `__NAMESPACE__`', +'ast\flags\MAGIC_TRAIT' => 'Marks an `ast\AST_MAGIC_CONST` as being `__TRAIT__`', +'ast\flags\MODIFIER_ABSTRACT' => 'Marks an element declaration as being `abstract`', +'ast\flags\MODIFIER_FINAL' => 'Marks an element declaration as being `final`', +'ast\flags\MODIFIER_PRIVATE' => 'Marks an element declaration as being `private`', +'ast\flags\MODIFIER_PROTECTED' => 'Marks an element declaration as being `protected`', +'ast\flags\MODIFIER_PUBLIC' => 'Marks an element declaration as being `public`', +'ast\flags\MODIFIER_STATIC' => 'Marks an element declaration as being `static`', +'ast\flags\NAME_FQ' => 'Marks an `ast\AST_NAME` as being fully qualified (`\Name`)', +'ast\flags\NAME_NOT_FQ' => 'Marks an `ast\AST_NAME` as being not fully qualified (`Name`)', +'ast\flags\NAME_RELATIVE' => 'Marks an `ast\AST_NAME` as being relative to the current namespace (`namespace\Name`)', +'ast\flags\PARAM_MODIFIER_PRIVATE' => 'Marks a promoted constructor property as being `private`', +'ast\flags\PARAM_MODIFIER_PROTECTED' => 'Marks a promoted constructor property as being `protected`', +'ast\flags\PARAM_MODIFIER_PUBLIC' => 'Marks a promoted constructor property as being `public`', +'ast\flags\PARAM_REF' => 'Marks an `ast\AST_PARAM` as being a reference (`&$x`)', +'ast\flags\PARAM_VARIADIC' => 'Marks an `ast\AST_PARAM` as being variadic (`...$x`)', +'ast\flags\PARENTHESIZED_CONDITIONAL' => 'Marks a `ast\AST_CONDITIONAL` as being parenthesized (`(cond ? true : false)`)', +'ast\flags\RETURNS_REF' => 'Marks a function-like as returning an argument by reference', +'ast\flags\TYPE_ARRAY' => 'Marks an `ast\AST_TYPE` as being `array`', +'ast\flags\TYPE_BOOL' => 'Marks an `ast\AST_TYPE` as being `bool`', +'ast\flags\TYPE_CALLABLE' => 'Marks an `ast\AST_TYPE` as being `callable`', +'ast\flags\TYPE_DOUBLE' => 'Marks an `ast\AST_TYPE` as being `float`', +'ast\flags\TYPE_FALSE' => 'Marks an `ast\AST_TYPE` as being `false` (for union types)', +'ast\flags\TYPE_MIXED' => 'Marks an `ast\AST_TYPE` as being `mixed`', +'ast\flags\TYPE_ITERABLE' => 'Marks an `ast\AST_TYPE` as being `iterable`', +'ast\flags\TYPE_LONG' => 'Marks an `ast\AST_TYPE` as being `int`', +'ast\flags\TYPE_NULL' => 'Marks an `ast\AST_TYPE` as being `null` (for union types)', +'ast\flags\TYPE_OBJECT' => 'Marks an `ast\AST_TYPE` as being `object`', +'ast\flags\TYPE_STATIC' => 'Marks an `ast\AST_TYPE` as being `static` (for return types)', +'ast\flags\TYPE_STRING' => 'Marks an `ast\AST_TYPE` as being `string`', +'ast\flags\TYPE_VOID' => 'Marks an `ast\AST_TYPE` as being `void` (for return types)', +'ast\flags\UNARY_BITWISE_NOT' => 'Marks an `ast\AST_UNARY_OP` as being `~expr`', +'ast\flags\UNARY_BOOL_NOT' => 'Marks an `ast\AST_UNARY_OP` as being `!expr`', +'ast\flags\UNARY_MINUS' => 'Marks an `ast\AST_UNARY_OP` as being `-expr`', +'ast\flags\UNARY_PLUS' => 'Marks an `ast\AST_UNARY_OP` as being `+expr`', +'ast\flags\UNARY_SILENCE' => 'Marks an `ast\AST_UNARY_OP` as being `@expr`', +'ast\flags\USE_CONST' => 'Marks an `ast\AST_USE` or `ast\AST_GROUP_USE` (namespace use statement) as being `use const name;`', +'ast\flags\USE_FUNCTION' => 'Marks an `ast\AST_USE` or `ast\AST_GROUP_USE` (namespace use statement) as being `use function name;`', +'ast\flags\USE_NORMAL' => 'Marks an `ast\AST_USE` or `ast\AST_GROUP_USE` (namespace use statement) as being `use name;`', +'BBCODE_ARG_DOUBLE_QUOTE' => 'This is a parser option allowing argument quoting with double quotes (`"`)', +'BBCODE_ARG_HTML_QUOTE' => 'This is a parser option allowing argument quoting with HTML version of double quotes (`&quot;`)', +'BBCODE_ARG_QUOTE_ESCAPING' => 'This is a parser option allowing argument quotes to be escaped this permit the quote delimiter to be found in the string escaping character is \ it can escape any quoting character or itself, if found in front of a non escapable character, it will be dropped. Default behaviour is not to use escaping.', +'BBCODE_ARG_SINGLE_QUOTE' => 'This is a parser option allowing argument quoting with single quotes (`\'`)', +'BBCODE_AUTO_CORRECT' => 'This is a parser option changing the way errors are treated. It automatically closes tag in the order they are opened. And treat tags with only an open tag as if there were a close tag present.', +'BBCODE_CORRECT_REOPEN_TAGS' => 'This is a parser option changing the way errors are treated. It automatically reopens tag if close tags are not in the good order.', +'BBCODE_DEFAULT_SMILEYS_OFF' => 'This is a parser option setting smileys to OFF if no flag is given at tag level.', +'BBCODE_DEFAULT_SMILEYS_ON' => 'This is a parser option setting smileys to ON if no flag is given at tag level.', +'BBCODE_DISABLE_TREE_BUILD' => 'This is a parser option disabling the BBCode parsing it can be useful if only the "smiley" replacement must be used.', +'BBCODE_FLAGS_ARG_PARSING' => 'This BBCode tag require argument sub-parsing (the argument is also parsed by the BBCode extension). As Of 0.10.2 another parser can be used as argument parser.', +'BBCODE_FLAGS_CDATA_NOT_ALLOWED' => 'This BBCode Tag does not accept content (it voids it automatically).', +'BBCODE_FLAGS_DENY_REOPEN_CHILD' => 'This BBCode Tag does not allow unclosed children to reopen when automatically closed.', +'BBCODE_FLAGS_ONE_OPEN_PER_LEVEL' => 'This BBCode Tag automatically closes if another tag of the same type is found at the same nesting level.', +'BBCODE_FLAGS_REMOVE_IF_EMPTY' => 'This BBCode Tag is automatically removed if content is empty it allows to produce lighter HTML.', +'BBCODE_FLAGS_SMILEYS_OFF' => 'This BBCode Tag does not accept smileys.', +'BBCODE_FLAGS_SMILEYS_ON' => 'This BBCode Tag accepts smileys.', +'BBCODE_FORCE_SMILEYS_OFF' => 'This is a parser option disabling completely the smileys parsing.', +'BBCODE_SET_FLAGS_ADD' => 'This permits to switch a flag set ON on a parser.', +'BBCODE_SET_FLAGS_REMOVE' => 'This permits to switch a flag set OFF on a parser.', +'BBCODE_SET_FLAGS_SET' => 'This permits to SET the complete flag set on a parser.', +'BBCODE_SMILEYS_CASE_INSENSITIVE' => 'Use a case insensitive Detection for smileys instead of a simple binary search.', +'BBCODE_TYPE_ARG' => 'This BBCode tag need an argument.', +'BBCODE_TYPE_NOARG' => 'This BBCode tag does not accept any arguments.', +'BBCODE_TYPE_OPTARG' => 'This BBCode tag accept an optional argument.', +'BBCODE_TYPE_ROOT' => 'This BBCode tag is the special tag root (nesting level 0).', +'BBCODE_TYPE_SINGLE' => 'This BBCode tag does not have a corresponding close tag.', +'BUS_ADRALN' => 'Available since PHP 5.3.0.', +'BUS_ADRERR' => 'Available since PHP 5.3.0.', +'BUS_OBJERR' => 'Available since PHP 5.3.0.', +'CAL_DOW_DAYNO' => 'For `jddayofweek`: the day of the week as `integer`, where `0` means Sunday and `6` means Saturday.', +'CAL_DOW_LONG' => 'For `jddayofweek`: the English name of the day of the week.', +'CAL_DOW_SHORT' => 'For `jddayofweek`: the abbreviated English name of the day of the week.', +'CAL_EASTER_ALWAYS_GREGORIAN' => 'For `easter_days`: calculate Easter according to the proleptic Gregorian calendar.', +'CAL_EASTER_ALWAYS_JULIAN' => 'For `easter_days`: calculate Easter according to the Julian calendar.', +'CAL_EASTER_DEFAULT' => 'For `easter_days`: calculate Easter for years before 1753 according to the Julian calendar, and for later years according to the Gregorian calendar.', +'CAL_EASTER_ROMAN' => 'For `easter_days`: calculate Easter for years before 1583 according to the Julian calendar, and for later years according to the Gregorian calendar.', +'CAL_FRENCH' => 'For `cal_days_in_month`, `cal_from_jd`, `cal_info` and `cal_to_jd`: use the French Repuclican calendar.', +'CAL_GREGORIAN' => 'For `cal_days_in_month`, `cal_from_jd`, `cal_info` and `cal_to_jd`: use the proleptic Gregorian calendar.', +'CAL_JEWISH' => 'For `cal_days_in_month`, `cal_from_jd`, `cal_info` and `cal_to_jd`: use the Jewish calendar.', +'CAL_JEWISH_ADD_ALAFIM' => 'For `jdtojewish`: adds the word alafim as thousands separator to the year number.', +'CAL_JEWISH_ADD_ALAFIM_GERESH' => 'For `jdtojewish`: adds a geresh symbol (which resembles a single-quote mark) as thousands separator to the year number.', +'CAL_JEWISH_ADD_GERESHAYIM' => 'For `jdtojewish`: add a gershayim symbol (which resembles a double-quote mark) before the final letter of the day and year numbers.', +'CAL_JULIAN' => 'For `cal_days_in_month`, `cal_from_jd`, `cal_info` and `cal_to_jd`: use the Julian calendar.', +'CAL_MONTH_FRENCH' => 'For `jdmonthname`: the French Republican month name.', +'CAL_MONTH_GREGORIAN_LONG' => 'For `jdmonthname`: the Gregorian month name.', +'CAL_MONTH_GREGORIAN_SHORT' => 'For `jdmonthname`: the abbreviated Gregorian month name.', +'CAL_MONTH_JEWISH' => 'For `jdmonthname`: the Jewish month name.', +'CAL_MONTH_JULIAN_LONG' => 'For `jdmonthname`: the Julian month name.', +'CAL_MONTH_JULIAN_SHORT' => 'For `jdmonthname`: the abbreviated Julian month name.', +'CAL_NUM_CALS' => 'The number of available calendars.', +'CASE_LOWER' => '`CASE_LOWER` is used with `array_change_key_case` and is used to convert array keys to lower case. This is also the default case for `array_change_key_case`.', +'CASE_UPPER' => '`CASE_UPPER` is used with `array_change_key_case` and is used to convert array keys to upper case.', +'Cassandra::CONSISTENCY_ALL' => 'Consistency level ALL guarantees that data has been written to all +Replica nodes.', +'Cassandra::CONSISTENCY_ANY' => 'Consistency level ANY means the request is fulfilled as soon as the data +has been written on the Coordinator. Requests with this consistency level +are not guaranteed to make it to Replica nodes.', +'Cassandra::CONSISTENCY_EACH_QUORUM' => 'Consistency level EACH_QUORUM guarantees that data has been written to at +least a majority Replica nodes in all datacenters. This consistency level +works only with `NetworkTopologyStrategy` replication.', +'Cassandra::CONSISTENCY_LOCAL_ONE' => 'Same as `CONSISTENCY_ONE`, but confined to the local data center. This +consistency level works only with `NetworkTopologyStrategy` replication.', +'Cassandra::CONSISTENCY_LOCAL_QUORUM' => 'Same as `CONSISTENCY_QUORUM`, but confined to the local data center. This +consistency level works only with `NetworkTopologyStrategy` replication.', +'Cassandra::CONSISTENCY_LOCAL_SERIAL' => 'Same as `CONSISTENCY_SERIAL`, but confined to the local data center. This +consistency level works only with `NetworkTopologyStrategy` replication.', +'Cassandra::CONSISTENCY_ONE' => 'Consistency level ONE guarantees that data has been written to at least +one Replica node.', +'Cassandra::CONSISTENCY_QUORUM' => 'Consistency level QUORUM guarantees that data has been written to at least +the majority of Replica nodes. How many nodes exactly are a majority +depends on the replication factor of a given keyspace and is calculated +using the formula `ceil(RF / 2 + 1)`, where `ceil` is a mathematical +ceiling function and `RF` is the replication factor used. For example, +for a replication factor of `5`, the majority is `ceil(5 / 2 + 1) = 3`.', +'Cassandra::CONSISTENCY_SERIAL' => 'This is a serial consistency level, it is used in conditional updates, +e.g. (`CREATE|INSERT ... IF NOT EXISTS`), and should be specified as the +`serial_consistency` execution option when invoking `session.execute` +or `session.execute_async`. + +Consistency level SERIAL, when set, ensures that a Paxos commit fails if +any of the replicas is down.', +'Cassandra::CONSISTENCY_THREE' => 'Consistency level THREE guarantees that data has been written to at least +three Replica nodes.', +'Cassandra::CONSISTENCY_TWO' => 'Consistency level TWO guarantees that data has been written to at least +two Replica nodes.', +'Cassandra::CPP_DRIVER_VERSION' => 'The version of the cpp-driver the extension is compiled against.', +'Cassandra::LOG_CRITICAL' => 'Allow critical level logging.', +'Cassandra::LOG_DEBUG' => 'Allow debug level logging.', +'Cassandra::LOG_DISABLED' => 'Used to disable logging.', +'Cassandra::LOG_ERROR' => 'Allow error level logging.', +'Cassandra::LOG_INFO' => 'Allow info level logging.', +'Cassandra::LOG_TRACE' => 'Allow trace level logging.', +'Cassandra::LOG_WARN' => 'Allow warning level logging.', +'Cassandra::TYPE_ASCII' => 'When using a map, collection or set of type ascii, all of its elements +must be strings.', +'Cassandra::TYPE_BIGINT' => 'When using a map, collection or set of type bigint, all of its elements +must be instances of Bigint.', +'Cassandra::TYPE_BLOB' => 'When using a map, collection or set of type blob, all of its elements +must be instances of Blob.', +'Cassandra::TYPE_BOOLEAN' => 'When using a map, collection or set of type bool, all of its elements +must be boolean.', +'Cassandra::TYPE_COUNTER' => 'When using a map, collection or set of type counter, all of its elements +must be instances of Bigint.', +'Cassandra::TYPE_DECIMAL' => 'When using a map, collection or set of type decimal, all of its elements +must be instances of Decimal.', +'Cassandra::TYPE_DOUBLE' => 'When using a map, collection or set of type double, all of its elements +must be doubles.', +'Cassandra::TYPE_FLOAT' => 'When using a map, collection or set of type float, all of its elements +must be instances of Float.', +'Cassandra::TYPE_INET' => 'When using a map, collection or set of type inet, all of its elements +must be instances of Inet.', +'Cassandra::TYPE_INT' => 'When using a map, collection or set of type int, all of its elements +must be ints.', +'Cassandra::TYPE_SMALLINT' => 'When using a map, collection or set of type smallint, all of its elements +must be instances of Inet.', +'Cassandra::TYPE_TEXT' => 'When using a map, collection or set of type text, all of its elements +must be strings.', +'Cassandra::TYPE_TIMESTAMP' => 'When using a map, collection or set of type timestamp, all of its elements +must be instances of Timestamp.', +'Cassandra::TYPE_TIMEUUID' => 'When using a map, collection or set of type timeuuid, all of its elements +must be instances of Timeuuid.', +'Cassandra::TYPE_TINYINT' => 'When using a map, collection or set of type tinyint, all of its elements +must be instances of Inet.', +'Cassandra::TYPE_UUID' => 'When using a map, collection or set of type uuid, all of its elements +must be instances of Uuid.', +'Cassandra::TYPE_VARCHAR' => 'When using a map, collection or set of type varchar, all of its elements +must be strings.', +'Cassandra::TYPE_VARINT' => 'When using a map, collection or set of type varint, all of its elements +must be instances of Varint.', +'Cassandra::VERIFY_NONE' => 'Perform no verification of nodes when using SSL encryption.', +'Cassandra::VERIFY_PEER_CERT' => 'Verify presence and validity of SSL certificates.', +'Cassandra::VERIFY_PEER_IDENTITY' => 'Verify that the IP address matches the SSL certificate’s common name or +one of its subject alternative names. This implies the certificate is +also present.', +'Cassandra::VERSION' => 'The current version of the extension.', +'CL_EXPUNGE' => 'silently expunge the mailbox before closing when calling `imap_close`', +'CLASSKIT_ACC_PRIVATE' => 'Marks the method `private`', +'CLASSKIT_ACC_PROTECTED' => 'Marks the method `protected`', +'CLASSKIT_ACC_PUBLIC' => 'Marks the method `public`', +'CLASSKIT_AGGREGATE_OVERRIDE' => 'PHP 5 specific flag to `classkit_import` Only defined when classkit compatibility is enabled.', +'CLASSKIT_VERSION' => 'Defined to the current version of the runkit package. Only defined when classkit compatibility is enabled.', +'CLD_CONTINUED' => 'Available since PHP 5.3.0.', +'CLD_DUMPED' => 'Available since PHP 5.3.0.', +'CLD_EXITED' => 'Available since PHP 5.3.0.', +'CLD_KILLED' => 'Available since PHP 5.3.0.', +'CLD_STOPPED' => 'Available since PHP 5.3.0.', +'CLD_TRAPPED' => 'Available since PHP 5.3.0.', +'Couchbase\Bucket::PINGSVC_FTS' => 'Ping full text search (FTS) service.', +'Couchbase\Bucket::PINGSVC_KV' => 'Ping data (Key/Value) service.', +'Couchbase\Bucket::PINGSVC_N1QL' => 'Ping query (N1QL) service.', +'Couchbase\Bucket::PINGSVC_VIEWS' => 'Ping views (Map/Reduce) service.', +'Couchbase\ClusterManager::RBAC_DOMAIN_EXTERNAL' => 'The user account managed by external system (e.g. LDAP).', +'Couchbase\ClusterManager::RBAC_DOMAIN_LOCAL' => 'The user account managed by Couchbase Cluster.', +'Couchbase\N1qlQuery::NOT_BOUNDED' => 'This is the default (for single-statement requests). +No timestamp vector is used in the index scan. +This is also the fastest mode, because we avoid the cost of obtaining the vector, +and we also avoid any wait for the index to catch up to the vector.', +'Couchbase\N1qlQuery::PROFILE_NONE' => 'Disables profiling. This is the default', +'Couchbase\N1qlQuery::PROFILE_PHASES' => 'Enables phase profiling.', +'Couchbase\N1qlQuery::PROFILE_TIMINGS' => 'Enables general timing profiling.', +'Couchbase\N1qlQuery::REQUEST_PLUS' => 'This implements strong consistency per request. +Before processing the request, a current vector is obtained. +The vector is used as a lower bound for the statements in the request. +If there are DML statements in the request, RYOW is also applied within the request.', +'Couchbase\N1qlQuery::STATEMENT_PLUS' => 'This implements strong consistency per statement. +Before processing each statement, a current vector is obtained +and used as a lower bound for that statement.', +'Couchbase\ViewQuery::UPDATE_AFTER' => 'Allow stale view, update view after it has been accessed.', +'Couchbase\ViewQuery::UPDATE_BEFORE' => 'Force a view update before returning data', +'Couchbase\ViewQuery::UPDATE_NONE' => 'Allow stale views', +'CP_MOVE' => 'Delete the messages from the current mailbox after copying with `imap_mail_copy`', +'CP_UID' => 'the sequence numbers contain UIDS', +'Crypto\Base64Exception::DECODE_FINISH_FORBIDDEN' => 'The object has not been initialized for decoding', +'Crypto\Base64Exception::DECODE_UPDATE_FAILED' => 'Base64 decoded string does not contain valid characters', +'Crypto\Base64Exception::DECODE_UPDATE_FORBIDDEN' => 'The object is already used for encoding', +'Crypto\Base64Exception::ENCODE_FINISH_FORBIDDEN' => 'The object has not been initialized for encoding', +'Crypto\Base64Exception::ENCODE_UPDATE_FORBIDDEN' => 'The object is already used for decoding', +'Crypto\Base64Exception::INPUT_DATA_LENGTH_HIGH' => 'Input data length can\'t exceed max integer length', +'Crypto\CipherException::AAD_LENGTH_HIGH' => 'AAD length can\'t exceed max integer length', +'Crypto\CipherException::AAD_SETTER_FAILED' => 'AAD setter failed', +'Crypto\CipherException::AAD_SETTER_FORBIDDEN' => 'AAD setter has to be called before encryption or decryption', +'Crypto\CipherException::ALGORITHM_NOT_FOUND' => 'Cipher \'%s\' algorithm not found', +'Crypto\CipherException::AUTHENTICATION_NOT_SUPPORTED' => 'The authentication is not supported for %s cipher mode', +'Crypto\CipherException::FINISH_DECRYPT_FORBIDDEN' => 'Cipher object is not initialized for decryption', +'Crypto\CipherException::FINISH_ENCRYPT_FORBIDDEN' => 'Cipher object is not initialized for encryption', +'Crypto\CipherException::FINISH_FAILED' => 'Finalizing of cipher failed', +'Crypto\CipherException::INIT_ALG_FAILED' => 'Initialization of cipher algorithm failed', +'Crypto\CipherException::INIT_CTX_FAILED' => 'Initialization of cipher context failed', +'Crypto\CipherException::INIT_DECRYPT_FORBIDDEN' => 'Cipher object is already used for encryption', +'Crypto\CipherException::INIT_ENCRYPT_FORBIDDEN' => 'Cipher object is already used for decryption', +'Crypto\CipherException::INPUT_DATA_LENGTH_HIGH' => 'Input data length can\'t exceed max integer length', +'Crypto\CipherException::IV_LENGTH_INVALID' => 'Invalid length of initial vector for cipher \'%s\' algorithm (required length: %d)', +'Crypto\CipherException::KEY_LENGTH_INVALID' => 'Invalid length of key for cipher \'%s\' algorithm (required length: %d)', +'Crypto\CipherException::MODE_NOT_AVAILABLE' => 'Cipher mode %s is not available in installed OpenSSL library', +'Crypto\CipherException::MODE_NOT_FOUND' => 'Cipher mode not found', +'Crypto\CipherException::STATIC_METHOD_NOT_FOUND' => 'Cipher static method \'%s\' not found', +'Crypto\CipherException::STATIC_METHOD_TOO_MANY_ARGS' => 'Cipher static method %s can accept max two arguments', +'Crypto\CipherException::TAG_GETTER_FAILED' => 'Tag getter failed', +'Crypto\CipherException::TAG_GETTER_FORBIDDEN' => 'Tag getter has to be called after encryption', +'Crypto\CipherException::TAG_LENGTH_HIGH' => 'Tag length can\'t exceed 128 bits (16 characters)', +'Crypto\CipherException::TAG_LENGTH_LOW' => 'Tag length can\'t be lower than 32 bits (4 characters)', +'Crypto\CipherException::TAG_LENGTH_SETTER_FORBIDDEN' => 'Tag length setter has to be called before encryption', +'Crypto\CipherException::TAG_SETTER_FAILED' => 'Tag setter failed', +'Crypto\CipherException::TAG_SETTER_FORBIDDEN' => 'Tag setter has to be called before decryption', +'Crypto\CipherException::TAG_VERIFY_FAILED' => 'Tag verification failed', +'Crypto\CipherException::UPDATE_DECRYPT_FORBIDDEN' => 'Cipher object is not initialized for decryption', +'Crypto\CipherException::UPDATE_ENCRYPT_FORBIDDEN' => 'Cipher object is not initialized for encryption', +'Crypto\CipherException::UPDATE_FAILED' => 'Updating of cipher failed', +'Crypto\HashException::DIGEST_FAILED' => 'Creating of hash digest failed', +'Crypto\HashException::HASH_ALGORITHM_NOT_FOUND' => 'Hash algorithm \'%s\' not found', +'Crypto\HashException::INIT_FAILED' => 'Initialization of hash failed', +'Crypto\HashException::INPUT_DATA_LENGTH_HIGH' => 'Input data length can\'t exceed max integer length', +'Crypto\HashException::STATIC_METHOD_NOT_FOUND' => 'Hash static method \'%s\' not found', +'Crypto\HashException::STATIC_METHOD_TOO_MANY_ARGS' => 'Hash static method %s can accept max one argument', +'Crypto\HashException::UPDATE_FAILED' => 'Updating of hash context failed', +'Crypto\KDFException::DERIVATION_FAILED' => 'KDF derivation failed', +'Crypto\KDFException::KEY_LENGTH_HIGH' => 'The key length is too high', +'Crypto\KDFException::KEY_LENGTH_LOW' => 'The key length is too low', +'Crypto\KDFException::PASSWORD_LENGTH_INVALID' => 'The password is too long', +'Crypto\KDFException::SALT_LENGTH_HIGH' => 'The salt is too long', +'Crypto\MACException::KEY_LENGTH_INVALID' => 'The key length for MAC is invalid', +'Crypto\MACException::MAC_ALGORITHM_NOT_FOUND' => 'MAC algorithm \'%s\' not found', +'Crypto\PBKDF2Exception::HASH_ALGORITHM_NOT_FOUND' => 'Hash algorithm \'%s\' not found', +'Crypto\PBKDF2Exception::ITERATIONS_HIGH' => 'Iterations count is too high', +'Crypto\RandException::FILE_WRITE_PREDICTABLE' => 'The bytes written were generated without appropriate seed', +'Crypto\RandException::GENERATE_PREDICTABLE' => 'The PRNG state is not yet unpredictable', +'Crypto\RandException::REQUESTED_BYTES_NUMBER_TOO_HIGH' => 'The requested number of bytes is too high', +'Crypto\RandException::SEED_LENGTH_TOO_HIGH' => 'The supplied seed length is too high', +'CURL_HTTP_VERSION_2' => 'Available since PHP 7.0.7 and cURL 7.43.0', +'CURL_HTTP_VERSION_2_0' => 'Available since PHP 5.5.24 and 5.6.8 and cURL 7.33.0', +'CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE' => 'Available since PHP 7.0.7 and cURL 7.49.0', +'CURL_HTTP_VERSION_2TLS' => 'Available since PHP 7.0.7 and cURL 7.47.0', +'CURL_LOCK_DATA_CONNECT' => 'Available since PHP 7.3.0 and cURL 7.10.0', +'CURL_LOCK_DATA_PSL' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURL_MAX_READ_SIZE' => 'Available since PHP 7.3.0 and cURL 7.53.0', +'CURL_PUSH_DENY' => 'Available since PHP 7.1.0 and cURL 7.44.0', +'CURL_PUSH_OK' => 'Available since PHP 7.1.0 and cURL 7.44.0', +'CURL_REDIR_POST_301' => 'Available since PHP 7.0.7 and cURL 7.18.2', +'CURL_REDIR_POST_302' => 'Available since PHP 7.0.7 and cURL 7.18.2', +'CURL_REDIR_POST_303' => 'Available since PHP 7.0.7 and cURL 7.25.1', +'CURL_REDIR_POST_ALL' => 'Available since PHP 7.0.7 and cURL 7.18.2', +'CURL_SSLVERSION_MAX_DEFAULT' => 'Available since PHP 7.3.0 and cURL 7.54.0', +'CURL_SSLVERSION_MAX_NONE' => 'Available since PHP 7.3.0 and cURL 7.54.0', +'CURL_SSLVERSION_MAX_TLSv1_0' => 'Available since PHP 7.3.0 and cURL 7.54.0', +'CURL_SSLVERSION_MAX_TLSv1_1' => 'Available since PHP 7.3.0 and cURL 7.54.0', +'CURL_SSLVERSION_MAX_TLSv1_2' => 'Available since PHP 7.3.0 and cURL 7.54.0', +'CURL_SSLVERSION_MAX_TLSv1_3' => 'Available since PHP 7.3.0 and cURL 7.54.0', +'CURL_SSLVERSION_TLSv1_0' => 'Available since PHP 5.5.19 and 5.6.3', +'CURL_SSLVERSION_TLSv1_1' => 'Available since PHP 5.5.19 and 5.6.3', +'CURL_SSLVERSION_TLSv1_2' => 'Available since PHP 5.5.19 and 5.6.3', +'CURL_SSLVERSION_TLSv1_3' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURL_VERSION_ALTSVC' => 'Available since PHP 7.3.6 and cURL 7.64.1', +'CURL_VERSION_ASYNCHDNS' => 'Available since PHP 7.3.0 and cURL 7.10.7', +'CURL_VERSION_BROTLI' => 'Available since PHP 7.3.0 and cURL 7.57.0', +'CURL_VERSION_CONV' => 'Available since PHP 7.3.0 and cURL 7.15.4', +'CURL_VERSION_CURLDEBUG' => 'Available since PHP 7.3.6 and cURL 7.19.6', +'CURL_VERSION_DEBUG' => 'Available since PHP 7.3.0 and cURL 7.10.6', +'CURL_VERSION_GSSAPI' => 'Available since PHP 7.3.0 and cURL 7.38.0', +'CURL_VERSION_GSSNEGOTIATE' => 'Available since PHP 7.3.0 and cURL 7.10.6 (deprecated since 7.38.0)', +'CURL_VERSION_HTTP2' => 'Available since PHP 5.5.24 and 5.6.8 and cURL 7.33.0', +'CURL_VERSION_HTTPS_PROXY' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURL_VERSION_IDN' => 'Available since PHP 7.3.0 and cURL 7.12.0', +'CURL_VERSION_KERBEROS5' => 'Available since PHP 7.0.7 and cURL 7.40.0', +'CURL_VERSION_MULTI_SSL' => 'Available since PHP 7.3.0 and cURL 7.56.0', +'CURL_VERSION_NTLM' => 'Available since PHP 7.3.0 and cURL 7.10.6', +'CURL_VERSION_NTLM_WB' => 'Available since PHP 7.3.0 and cURL 7.22.0', +'CURL_VERSION_PSL' => 'Available since PHP 7.0.7 and cURL 7.47.0', +'CURL_VERSION_SPNEGO' => 'Available since PHP 7.3.0 and cURL 7.10.8', +'CURL_VERSION_SSPI' => 'Available since PHP 7.3.0 and cURL 7.13.2', +'CURL_VERSION_TLSAUTH_SRP' => 'Available since PHP 7.3.0 and cURL 7.21.4', +'CURL_VERSION_UNIX_SOCKETS' => 'Available since PHP 7.0.7 and cURL 7.40.0', +'CURL_WRAPPERS_ENABLED' => 'Defined if PHP was configured with --with-curlwrappers. Moved to PECL in PHP 5.5.0.', +'CURLAUTH_BEARER' => 'Available since PHP 7.3.0 and cURL 7.61.0.', +'CURLAUTH_GSSAPI' => 'Available since PHP 7.3.0 and cURL 7.54.1', +'CURLAUTH_NEGOTIATE' => 'Available since PHP 7.0.7 and cURL 7.38.0.', +'CURLAUTH_NTLM_WB' => 'Available since PHP 7.0.7 and cURL 7.22.0', +'CURLCLOSEPOLICY_CALLBACK' => 'Removed in PHP 5.6.0.', +'CURLCLOSEPOLICY_LEAST_RECENTLY_USED' => 'Removed in PHP 5.6.0.', +'CURLCLOSEPOLICY_LEAST_TRAFFIC' => 'Removed in PHP 5.6.0.', +'CURLCLOSEPOLICY_OLDEST' => 'Removed in PHP 5.6.0.', +'CURLCLOSEPOLICY_SLOWEST' => 'Removed in PHP 5.6.0.', +'CURLE_SSH' => 'Available since PHP 5.3.0 and cURL 7.16.1.', +'CURLE_WEIRD_SERVER_REPLY' => 'Available since PHP 7.3.0 and cURL 7.51.0', +'CURLFTP_CREATE_DIR' => 'Available since PHP 7.0.7 and cURL 7.19.3', +'CURLFTP_CREATE_DIR_NONE' => 'Available since PHP 7.0.7 and cURL 7.19.3', +'CURLFTP_CREATE_DIR_RETRY' => 'Available since PHP 7.0.7 and cURL 7.19.3', +'CURLFTPAUTH_DEFAULT' => 'Available since PHP 5.1.0', +'CURLFTPAUTH_SSL' => 'Available since PHP 5.1.0', +'CURLFTPAUTH_TLS' => 'Available since PHP 5.1.0', +'CURLFTPSSL_ALL' => 'Available since PHP 5.2.0', +'CURLFTPSSL_CONTROL' => 'Available since PHP 5.2.0', +'CURLFTPSSL_NONE' => 'Available since PHP 5.2.0', +'CURLFTPSSL_TRY' => 'Available since PHP 5.2.0', +'CURLHEADER_SEPARATE' => 'Available since PHP 7.0.7 and cURL 7.37.0.', +'CURLHEADER_UNIFIED' => 'Available since PHP 7.0.7 and cURL 7.37.0.', +'CURLINFO_APPCONNECT_TIME_T' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLINFO_CONNECT_TIME_T' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLINFO_CONTENT_LENGTH_DOWNLOAD_T' => 'Available since PHP 7.3.0 and cURL 7.55.0', +'CURLINFO_CONTENT_LENGTH_UPLOAD_T' => 'Available since PHP 7.3.0 and cURL 7.55.0', +'CURLINFO_FILETIME_T' => 'Available since PHP 7.3.0 and cURL 7.59.0', +'CURLINFO_HEADER_OUT' => 'Available since PHP 5.1.3', +'CURLINFO_HTTP_CODE' => 'As of PHP 5.5.0 and cURL 7.10.8, this is a legacy alias of `CURLINFO_RESPONSE_CODE`', +'CURLINFO_HTTP_VERSION' => 'Available since PHP 7.3.0 and cURL 7.50.0', +'CURLINFO_LOCAL_IP' => 'Available since PHP 5.4.7', +'CURLINFO_LOCAL_PORT' => 'Available since PHP 5.4.7', +'CURLINFO_NAMELOOKUP_TIME_T' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLINFO_PRETRANSFER_TIME_T' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLINFO_PRIMARY_IP' => 'Available since PHP 5.4.7', +'CURLINFO_PRIMARY_PORT' => 'Available since PHP 5.4.7', +'CURLINFO_PRIVATE' => 'Available since PHP 5.2.4', +'CURLINFO_PROTOCOL' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLINFO_PROXY_SSL_VERIFYRESULT' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLINFO_REDIRECT_TIME_T' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLINFO_REDIRECT_URL' => 'Available since PHP 5.3.7', +'CURLINFO_RESPONSE_CODE' => 'Available since PHP 5.5.0 and cURL 7.10.8', +'CURLINFO_SCHEME' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLINFO_SIZE_DOWNLOAD_T' => 'Available since PHP 7.3.0 and cURL 7.50.0', +'CURLINFO_SIZE_UPLOAD_T' => 'Available since PHP 7.3.0 and cURL 7.50.0', +'CURLINFO_SPEED_DOWNLOAD_T' => 'Available since PHP 7.3.0 and cURL 7.50.0', +'CURLINFO_SPEED_UPLOAD_T' => 'Available since PHP 7.3.0 and cURL 7.50.0', +'CURLINFO_STARTTRANSFER_TIME_T' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLINFO_TOTAL_TIME_T' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLMOPT_CHUNK_LENGTH_PENALTY_SIZE' => 'Available since PHP 7.0.7 and cURL 7.30.0', +'CURLMOPT_CONTENT_LENGTH_PENALTY_SIZE' => 'Available since PHP 7.0.7 and cURL 7.30.0', +'CURLMOPT_MAX_HOST_CONNECTIONS' => 'Available since PHP 7.0.7 and cURL 7.30.0', +'CURLMOPT_MAX_PIPELINE_LENGTH' => 'Available since PHP 7.0.7 and cURL 7.30.0', +'CURLMOPT_MAX_TOTAL_CONNECTIONS' => 'Available since PHP 7.0.7 and cURL 7.30.0', +'CURLMOPT_MAXCONNECTS' => 'Available since PHP 5.5.0 and cURL 7.16.3.', +'CURLMOPT_PIPELINING' => 'Available since PHP 5.5.0 and cURL 7.16.0.', +'CURLMOPT_PUSHFUNCTION' => 'Available since PHP 7.1.0 and cURL 7.44.0', +'CURLOPT_ABSTRACT_UNIX_SOCKET' => 'Available since PHP 7.3.0 and cURL 7.53.0', +'CURLOPT_AUTOREFERER' => 'Available since PHP 5.1.0', +'CURLOPT_CLOSEPOLICY' => 'Removed in PHP 5.6.0.', +'CURLOPT_CONNECT_TO' => 'Available since PHP 7.0.7 and cURL 7.49.0', +'CURLOPT_COOKIELIST' => 'Available since PHP 5.5.0 and cURL 7.14.1', +'CURLOPT_COOKIESESSION' => 'Available since PHP 5.1.0', +'CURLOPT_DEFAULT_PROTOCOL' => 'Available since PHP 7.0.7 and cURL 7.45.0', +'CURLOPT_DISALLOW_USERNAME_IN_URL' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLOPT_DNS_INTERFACE' => 'Available since PHP 7.0.7 and cURL 7.33.0', +'CURLOPT_DNS_LOCAL_IP4' => 'Available since PHP 7.0.7 and cURL 7.33.0', +'CURLOPT_DNS_LOCAL_IP6' => 'Available since PHP 7.0.7 and cURL 7.33.0', +'CURLOPT_DNS_SHUFFLE_ADDRESSES' => 'Available since PHP 7.3.0 and cURL 7.60.0', +'CURLOPT_EXPECT_100_TIMEOUT_MS' => 'Available since PHP 7.0.7 and cURL 7.36.0', +'CURLOPT_FOLLOWLOCATION' => 'This constant is not available when open_basedir or safe_mode are enabled.', +'CURLOPT_FTP_SSL' => 'Available since PHP 5.2.0', +'CURLOPT_FTPSSLAUTH' => 'Available since PHP 5.1.0', +'CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS' => 'Available since PHP 7.3.0 and cURL 7.59.0', +'CURLOPT_HAPROXYPROTOCOL' => 'Available since PHP 7.3.0 and cURL 7.60.0', +'CURLOPT_HEADEROPT' => 'Available since PHP 7.0.7 and cURL 7.37.0', +'CURLOPT_HTTP09_ALLOWED ' => 'Available since PHP 7.3.15 and 7.4.3, respectively, and cURL 7.64.0', +'CURLOPT_KEEP_SENDING_ON_ERROR' => 'Available since PHP 7.3.0 and cURL 7.51.0', +'CURLOPT_LOGIN_OPTIONS' => 'Available since PHP 7.0.7 and cURL 7.34.0', +'CURLOPT_MAX_RECV_SPEED_LARGE' => 'Available since PHP 5.4.0 and cURL 7.15.5', +'CURLOPT_MAX_SEND_SPEED_LARGE' => 'Available since PHP 5.4.0 and cURL 7.15.5', +'CURLOPT_PATH_AS_IS' => 'Available since PHP 7.0.7 and cURL 7.42.0', +'CURLOPT_PINNEDPUBLICKEY' => 'Available since PHP 7.0.7 and cURL 7.39.0', +'CURLOPT_PIPEWAIT' => 'Available since PHP 7.0.7 and cURL 7.43.0', +'CURLOPT_PRE_PROXY' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PRIVATE' => 'Available since PHP 5.2.4', +'CURLOPT_PROGRESSFUNCTION' => 'Available since PHP 5.3.0', +'CURLOPT_PROXY_CAINFO' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_CAPATH' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_CRLFILE' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_KEYPASSWD' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_PINNEDPUBLICKEY' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_SERVICE_NAME' => 'Available since PHP 7.0.7 and cURL 7.43.0', +'CURLOPT_PROXY_SSL_CIPHER_LIST' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_SSL_OPTIONS' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_SSL_VERIFYHOST' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_SSL_VERIFYPEER' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_SSLCERT' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_SSLCERTTYPE' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_SSLKEY' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_SSLKEYTYPE' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_SSLVERSION' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_TLS13_CIPHERS' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLOPT_PROXY_TLSAUTH_PASSWORD' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_TLSAUTH_TYPE' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXY_TLSAUTH_USERNAME' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLOPT_PROXYHEADER' => 'Available since PHP 7.0.7 and cURL 7.37.0', +'CURLOPT_PROXYTYPE' => 'Available as of cURL 7.10.', +'CURLOPT_REQUEST_TARGET' => 'Available since PHP 7.3.0 and cURL 7.55.0.', +'CURLOPT_SASL_IR' => 'Available since PHP 7.0.7 and cURL 7.31.0', +'CURLOPT_SERVICE_NAME' => 'Available since PHP 7.0.7 and cURL 7.43.0', +'CURLOPT_SOCKS5_AUTH' => 'Available since PHP 7.3.0 and cURL 7.55.0', +'CURLOPT_SSH_COMPRESSION' => 'Available since PHP 7.3.0 and cURL 7.56.0', +'CURLOPT_SSL_ENABLE_ALPN' => 'Available since PHP 7.0.7 and cURL 7.36.0', +'CURLOPT_SSL_ENABLE_NPN' => 'Available since PHP 7.0.7 and cURL 7.36.0', +'CURLOPT_SSL_FALSESTART' => 'Available since PHP 7.0.7 and cURL 7.42.0', +'CURLOPT_SSL_OPTIONS' => 'Available since PHP 5.5.0 and cURL 7.25.0', +'CURLOPT_SSL_VERIFYSTATUS' => 'Available since PHP 7.0.7 and cURL 7.41.0', +'CURLOPT_STREAM_WEIGHT' => 'Available since PHP 7.0.7 and cURL 7.46.0', +'CURLOPT_SUPPRESS_CONNECT_HEADERS' => 'Available since PHP 7.3.0 and cURL 7.54.0', +'CURLOPT_TCP_FASTOPEN' => 'Available since PHP 7.0.7 and cURL 7.49.0', +'CURLOPT_TCP_KEEPALIVE' => 'Available since PHP 5.5.0 and cURL 7.25.0', +'CURLOPT_TCP_KEEPIDLE' => 'Available since PHP 5.5.0 and cURL 7.25.0', +'CURLOPT_TCP_KEEPINTVL' => 'Available since PHP 5.5.0 and cURL 7.25.0', +'CURLOPT_TCP_NODELAY' => 'Available since PHP 5.2.1', +'CURLOPT_TFTP_NO_OPTIONS' => 'Available since PHP 7.0.7 and cURL 7.48.0', +'CURLOPT_TIMEVALUE_LARGE' => 'Available since PHP 7.3.0 and cURL 7.59.0', +'CURLOPT_TLS13_CIPHERS' => 'Available since PHP 7.3.0 and cURL 7.61.0', +'CURLOPT_UNIX_SOCKET_PATH' => 'Available since PHP 7.0.7 and cURL 7.40.0', +'CURLOPT_USERNAME' => 'Available since PHP 5.5.0 and cURL 7.19.1', +'CURLOPT_XOAUTH2_BEARER' => 'Available since PHP 7.0.7 and cURL 7.33.0', +'CURLPAUSE_ALL' => 'Available since PHP 5.5.0 and cURL 7.18.0.', +'CURLPAUSE_CONT' => 'Available since PHP 5.5.0 and cURL 7.18.0.', +'CURLPAUSE_RECV' => 'Available since PHP 5.5.0 and cURL 7.18.0.', +'CURLPAUSE_RECV_CONT' => 'Available since PHP 5.5.0 and cURL 7.18.0.', +'CURLPAUSE_SEND' => 'Available since PHP 5.5.0 and cURL 7.18.0.', +'CURLPAUSE_SEND_CONT' => 'Available since PHP 5.5.0 and cURL 7.18.0.', +'CURLPIPE_HTTP1' => 'Available since PHP 7.0.0 and cURL 7.43.0.', +'CURLPIPE_MULTIPLEX' => 'Available since PHP 7.0.0 and cURL 7.43.0.', +'CURLPIPE_NOTHING' => 'Available since PHP 7.0.0 and cURL 7.43.0.', +'CURLPROTO_SMB' => 'Available since PHP 7.0.7 and cURL 7.40.0.', +'CURLPROTO_SMBS' => 'Available since PHP 7.0.7 and cURL 7.40.0.', +'CURLPROXY_HTTP' => 'Available since cURL 7.10.', +'CURLPROXY_HTTP_1_0' => 'Available since PHP 7.0.7 and cURL 7.19.3', +'CURLPROXY_HTTPS' => 'Available since PHP 7.3.0 and cURL 7.52.0', +'CURLPROXY_SOCKS4' => 'Available since PHP 5.2.10 and cURL 7.10.', +'CURLPROXY_SOCKS4A' => 'Available since PHP 5.5.23 and PHP 5.6.7 and cURL 7.18.0.', +'CURLPROXY_SOCKS5' => 'Available since cURL 7.10.', +'CURLPROXY_SOCKS5_HOSTNAME' => 'Available since PHP 5.5.23 and PHP 5.6.7 and cURL 7.18.0.', +'CURLSSH_AUTH_AGENT' => 'Available since PHP 7.0.7 and cURL 7.28.0', +'CURLSSH_AUTH_GSSAPI' => 'Available since PHP 7.3.0 and cURL 7.58.0', +'CURLSSLOPT_ALLOW_BEAST' => 'Available since PHP 5.5.0 and cURL 7.25.0', +'CURLSSLOPT_NO_REVOKE' => 'Available since PHP 7.0.7 and cURL 7.44.0', +'DB2_AUTOCOMMIT_OFF' => 'Specifies that autocommit should be turned off.', +'DB2_AUTOCOMMIT_ON' => 'Specifies that autocommit should be turned on.', +'DB2_BINARY' => 'Specifies that binary data shall be returned as is. This is the default mode.', +'DB2_CASE_LOWER' => 'Specifies that column names will be returned in lower case.', +'DB2_CASE_NATURAL' => 'Specifies that column names will be returned in their natural case.', +'DB2_CASE_UPPER' => 'Specifies that column names will be returned in upper case.', +'DB2_CHAR' => 'Specifies that the variable should be bound as a CHAR or VARCHAR data type.', +'DB2_CONVERT' => 'Specifies that binary data shall be converted to a hexadecimal encoding and returned as an ASCII string.', +'DB2_DEFERRED_PREPARE_OFF' => 'Specifies that deferred prepare should be turned off for the specified statement resource.', +'DB2_DEFERRED_PREPARE_ON' => 'Specifies that deferred prepare should be turned on for the specified statement resource.', +'DB2_DOUBLE' => 'Specifies that the variable should be bound as a DOUBLE, FLOAT, or REAL data type.', +'DB2_FORWARD_ONLY' => 'Specifies a forward-only cursor for a statement resource. This is the default cursor type and is supported on all database servers.', +'DB2_LONG' => 'Specifies that the variable should be bound as a SMALLINT, INTEGER, or BIGINT data type.', +'DB2_PARAM_FILE' => 'Specifies that the column should be bound directly to a file for input.', +'DB2_PARAM_IN' => 'Specifies the PHP variable should be bound as an IN parameter for a stored procedure.', +'DB2_PARAM_INOUT' => 'Specifies the PHP variable should be bound as an INOUT parameter for a stored procedure.', +'DB2_PARAM_OUT' => 'Specifies the PHP variable should be bound as an OUT parameter for a stored procedure.', +'DB2_PASSTHRU' => 'Specifies that binary data shall be converted to a `null` value.', +'DB2_SCROLLABLE' => 'Specifies a scrollable cursor for a statement resource. This mode enables random access to rows in a result set, but currently is supported only by IBM DB2 Universal Database.', +'DBASE_RDONLY' => 'Open database for reading only. Used with `dbase_open`. (Available as of dbase 7.0.0)', +'DBASE_RDWR' => 'Open database for reading and writing. Used with `dbase_open`. (Available as of dbase 7.0.0)', +'DBASE_TYPE_DBASE' => 'Create dBASE style database. Used with `dbase_create`. (Available as of dbase 7.0.0)', +'DBASE_TYPE_FOXPRO' => 'Create FoxPro style database. Used with `dbase_create`. (Available as of dbase 7.0.0)', +'DBASE_VERSION' => 'The extension version. (Available as of dbase 7.0.0)', +'E_ALL' => 'Error reporting constant', +'E_COMPILE_ERROR' => 'Error reporting constant', +'E_COMPILE_WARNING' => 'Error reporting constant', +'E_CORE_ERROR' => 'Error reporting constant', +'E_CORE_WARNING' => 'Error reporting constant', +'E_DEPRECATED' => 'Error reporting constant.', +'E_ERROR' => 'Error reporting constant', +'E_NOTICE' => 'Error reporting constant', +'E_PARSE' => 'Error reporting constant', +'E_RECOVERABLE_ERROR' => 'Error reporting constant.', +'E_STRICT' => 'Error reporting constant', +'E_USER_DEPRECATED' => 'Error reporting constant.', +'E_USER_ERROR' => 'Error reporting constant', +'E_USER_NOTICE' => 'Error reporting constant', +'E_USER_WARNING' => 'Error reporting constant', +'E_WARNING' => 'Error reporting constant', +'EIO_DT_BLK' => 'Node type', +'EIO_DT_CHR' => 'Node type', +'EIO_DT_CMP' => 'HP-UX network special node type', +'EIO_DT_DIR' => 'Directory node type', +'EIO_DT_DOOR' => 'Solaris door node type', +'EIO_DT_FIFO' => 'FIFO node type', +'EIO_DT_LNK' => 'Link node type', +'EIO_DT_MAX' => 'Highest node type value', +'EIO_DT_MPB' => 'Multiplexed block device (v7+coherent)', +'EIO_DT_MPC' => 'Multiplexed char device (v7+coherent) node type', +'EIO_DT_NAM' => 'Xenix special named file node type', +'EIO_DT_REG' => 'Node type', +'EIO_DT_SOCK' => 'Socket node type', +'EIO_DT_UNKNOWN' => 'Unknown node type(very common). Further `stat` needed.', +'EIO_DT_WHT' => 'Node type', +'EIO_PRI_DEFAULT' => 'Request default prioriry', +'EIO_PRI_MAX' => 'Request maximal prioriry', +'EIO_PRI_MIN' => 'Request minimal prioriry', +'EIO_READDIR_DENTS' => '`eio_readdir` flag. If specified, the result argument of the callback becomes an array with the following keys: `\'names\'` - array of directory names `\'dents\'` - array of `struct eio_dirent`-like arrays having the following keys each: `\'name\'` - the directory name; `\'type\'` - one of *EIO_DT_** constants; `\'inode\'` - the inode number, if available, otherwise unspecified;', +'EIO_READDIR_DIRS_FIRST' => 'When this flag is specified, the names will be returned in an order where likely directories come first, in optimal stat order.', +'EIO_READDIR_STAT_ORDER' => 'When this flag is specified, then the names will be returned in an order suitable for `stat`\'ing each one. When planning to `stat` all files in the given directory, the returned order will likely be fastest.', +'EIO_SEEK_CUR' => 'The offset is set to its current location plus offset bytes.', +'EIO_SEEK_END' => 'The offset is set to the size of the file plus offset bytes.', +'EIO_SEEK_SET' => 'The offset is set to specified number of bytes(offset).', +'ENC7BIT' => 'Body encoding: 7 bit SMTP semantic data', +'ENC8BIT' => 'Body encoding: 8 bit SMTP semantic data', +'ENCBASE64' => 'Body encoding: base-64 encoded data', +'ENCBINARY' => 'Body encoding: 8 bit binary data', +'ENCHANT_ISPELL' => 'Dictionary type for Ispell. Used with `enchant_broker_get_dict_path` and `enchant_broker_set_dict_path`.', +'ENCHANT_MYSPELL' => 'Dictionary type for MySpell. Used with `enchant_broker_get_dict_path` and `enchant_broker_set_dict_path`.', +'ENCOTHER' => 'Body encoding: unknown', +'ENCQUOTEDPRINTABLE' => 'Body encoding: human-readable 8-as-7 bit data', +'Ev::BACKEND_ALL' => 'Try all backends(even corrupted ones). It\'s not recommended to use it explicitly. Bitwise operators should be +applied here(e.g. Ev::BACKEND_ALL & ~ Ev::BACKEND_KQUEUE ) Use Ev::recommendedBackends() , or don\'t specify any +backends at all.', +'Ev::BACKEND_DEVPOLL' => 'Solaris 8 backend. This is not implemented yet.', +'Ev::BACKEND_EPOLL' => 'Linux-specific epoll(7) backend for both pre- and post-2.6.9 kernels', +'Ev::BACKEND_KQUEUE' => 'kqueue backend used on most BSD systems. EvEmbed watcher could be used to embed one loop(with kqueue backend) +into another. For instance, one can try to create an event loop with kqueue backend and use it for sockets only.', +'Ev::BACKEND_MASK' => 'Not a backend, but a mask to select all backend bits from flags value to mask out any backends(e.g. when +modifying the LIBEV_FLAGS environment variable).', +'Ev::BACKEND_POLL' => 'poll(2) backend', +'Ev::BACKEND_PORT' => 'Solaris 10 event port mechanism with a good scaling.', +'Ev::BACKEND_SELECT' => 'select(2) backend', +'Ev::BREAK_ALL' => 'Flag passed to Ev::stop() or EvLoop::stop(): Makes all nested Ev::run() or EvLoop::run() calls return.', +'Ev::BREAK_CANCEL' => 'Flag passed to Ev::stop() or EvLoop::stop(): Cancel the break operation.', +'Ev::BREAK_ONE' => 'Flag passed to Ev::stop() or EvLoop::stop(): Makes the innermost Ev::run() or EvLoop::run() call return.', +'Ev::CHECK' => 'Event bitmask: All EvCheck watchers are queued just after Ev::run() has gathered the new events, but before it +queues any callbacks for any received events. Thus, EvCheck watchers will be invoked before any other watchers +of the same or lower priority within an event loop iteration.', +'Ev::CHILD' => 'Event bitmask: The pid specified in EvChild::__construct() has received a status change.', +'Ev::CUSTOM' => 'Event bitmask: Not ever sent(or otherwise used) by libev itself, but can be freely used by libev users to signal +watchers (e.g. via EvWatcher::feed() ).', +'Ev::EMBED' => 'Event bitmask: The embedded event loop specified in the EvEmbed watcher needs attention.', +'Ev::ERROR' => 'Event bitmask: An unspecified error has occurred, the watcher has been stopped. This might happen because the +watcher could not be properly started because libev ran out of memory, a file descriptor was found to be closed +or any other problem. Libev considers these application bugs.', +'Ev::FLAG_AUTO' => 'Flag passed to create a loop: The default flags value', +'Ev::FLAG_FORKCHECK' => 'Flag passed to create a loop: Makes libev check for a fork in each iteration, instead of calling EvLoop::fork() +manually. This works by calling getpid() on every iteration of the loop, and thus this might slow down the event +loop with lots of loop iterations, but usually is not noticeable. This flag setting cannot be overridden or +specified in the LIBEV_FLAGS environment variable.', +'Ev::FLAG_NOENV' => 'Flag passed to create a loop: If this flag used(or the program runs setuid or setgid), libev won\'t look at the +environment variable LIBEV_FLAGS. Otherwise(by default), LIBEV_FLAGS will override the flags completely if it is +found. Useful for performance tests and searching for bugs.', +'Ev::FLAG_NOINOTIFY' => 'Flag passed to create a loop: When this flag is specified, libev won\'t attempt to use the inotify API for its +ev_stat watchers. The flag can be useful to conserve inotify file descriptors, as otherwise each loop using +ev_stat watchers consumes one inotify handle.', +'Ev::FLAG_NOSIGMASK' => 'Flag passed to create a loop: When this flag is specified, libev will avoid to modify the signal mask. +Specifically, this means having to make sure signals are unblocked before receiving them. + +This behaviour is useful for custom signal handling, or handling signals only in specific threads.', +'Ev::FLAG_SIGNALFD' => 'Flag passed to create a loop: When this flag is specified, libev will attempt to use the signalfd API for its +ev_signal (and ev_child ) watchers. This API delivers signals synchronously, which makes it both faster and might +make it possible to get the queued signal data. It can also simplify signal handling with threads, as long as +signals are properly blocked in threads. Signalfd will not be used by default.', +'Ev::IDLE' => 'Event bitmask: EvIdle watcher works when there is nothing to do with other watchers.', +'Ev::MAXPRI' => 'Highest allowed watcher priority.', +'Ev::MINPRI' => 'Lowest allowed watcher priority.', +'Ev::PERIODIC' => 'Event bitmask: EvPeriodic watcher has been timed out.', +'Ev::PREPARE' => 'Event bitmask: All EvPrepare watchers are invoked just before Ev::run() starts. Thus, EvPrepare watchers are the +last watchers invoked before the event loop sleeps or polls for new events.', +'Ev::READ' => 'Event bitmask: The file descriptor in the EvIo watcher has become readable.', +'Ev::RUN_NOWAIT' => 'Flag passed to Ev::run() or EvLoop::run(): Means that event loop will look for new events, will handle those +events and any already outstanding ones, but will not wait and block the process in case there are no events and +will return after one iteration of the loop. This is sometimes useful to poll and handle new events while doing +lengthy calculations, to keep the program responsive.', +'Ev::RUN_ONCE' => 'Flag passed to Ev::run() or EvLoop::run(): Means that event loop will look for new events (waiting if necessary) +and will handle those and any already outstanding ones. It will block the process until at least one new event +arrives (which could be an event internal to libev itself, so there is no guarantee that a user-registered +callback will be called), and will return after one iteration of the loop.', +'Ev::SIGNAL' => 'Event bitmask: A signal specified in EvSignal::__construct() has been received.', +'Ev::STAT' => 'Event bitmask: The path specified in EvStat watcher changed its attributes.', +'Ev::TIMER' => 'Event bitmask: EvTimer watcher has been timed out.', +'Ev::WRITE' => 'Event bitmask: The file descriptor in the EvIo watcher has become writable.', +'EXIF_USE_MBSTRING' => 'This constant have a value of `1` if the mbstring is enabled, otherwise the value is `0`.', +'EXP_EOF' => 'Value, returned by `expect_expectl`, when EOF is reached.', +'EXP_EXACT' => 'Indicates that the pattern is an exact string.', +'EXP_FULLBUFFER' => 'Value, returned by `expect_expectl` if no pattern have been matched.', +'EXP_GLOB' => 'Indicates that the pattern is a glob-style string pattern.', +'EXP_REGEXP' => 'Indicates that the pattern is a regexp-style string pattern.', +'EXP_TIMEOUT' => 'Value, returned by `expect_expectl` upon timeout of seconds, specified in value of expect.timeout', +'FANN_COS' => 'Periodical cosinus activation function.', +'FANN_COS_SYMMETRIC' => 'Periodical cosinus activation function.', +'FANN_E_CANT_ALLOCATE_MEM' => 'Unable to allocate memory.', +'FANN_E_CANT_OPEN_CONFIG_R' => 'Unable to open configuration file for reading.', +'FANN_E_CANT_OPEN_CONFIG_W' => 'Unable to open configuration file for writing.', +'FANN_E_CANT_OPEN_TD_R' => 'Unable to open train data file for reading.', +'FANN_E_CANT_OPEN_TD_W' => 'Unable to open train data file for writing.', +'FANN_E_CANT_READ_CONFIG' => 'Error reading info from configuration file.', +'FANN_E_CANT_READ_CONNECTIONS' => 'Error reading connections from configuration file.', +'FANN_E_CANT_READ_NEURON' => 'Error reading neuron info from configuration file.', +'FANN_E_CANT_READ_TD' => 'Error reading training data from file.', +'FANN_E_CANT_TRAIN_ACTIVATION' => 'Unable to train with the selected activation function.', +'FANN_E_CANT_USE_ACTIVATION' => 'Unable to use the selected activation function.', +'FANN_E_CANT_USE_TRAIN_ALG' => 'Unable to use the selected training algorithm.', +'FANN_E_INDEX_OUT_OF_BOUND' => 'Index is out of bound.', +'FANN_E_INPUT_NO_MATCH' => 'The number of input neurons in the ann data do not match', +'FANN_E_NO_ERROR' => 'No error.', +'FANN_E_OUTPUT_NO_MATCH' => 'The number of output neurons in the ann and data do not match.', +'FANN_E_SCALE_NOT_PRESENT' => 'Scaling parameters not present.', +'FANN_E_TRAIN_DATA_MISMATCH' => 'Irreconcilable differences between two struct fann_train_data structures.', +'FANN_E_TRAIN_DATA_SUBSET' => 'Trying to take subset which is not within the training set.', +'FANN_E_WRONG_CONFIG_VERSION' => 'Wrong version of configuration file.', +'FANN_E_WRONG_NUM_CONNECTIONS' => 'Number of connections not equal to the number expected.', +'FANN_ELLIOT' => 'Fast (sigmoid like) activation function defined by David Elliott.', +'FANN_ELLIOT_SYMMETRIC' => 'Fast (symmetric sigmoid like) activation function defined by David Elliott.', +'FANN_ERRORFUNC_LINEAR' => 'Standard linear error function.', +'FANN_ERRORFUNC_TANH' => 'Tanh error function, usually better but can require a lower learning rate. This error function aggressively targets outputs that differ much from the desired, while not targeting outputs that only differ a little that much. This activation function is not recommended for cascade training and incremental training.', +'FANN_GAUSSIAN' => 'Gaussian activation function.', +'FANN_GAUSSIAN_STEPWISE' => 'Stepwise gaussian activation function.', +'FANN_GAUSSIAN_SYMMETRIC' => 'Symmetric gaussian activation function.', +'FANN_LINEAR' => 'Linear activation function.', +'FANN_LINEAR_PIECE' => 'Bounded linear activation function.', +'FANN_LINEAR_PIECE_SYMMETRIC' => 'Bounded linear activation function.', +'FANN_NETTYPE_LAYER' => 'Each layer only has connections to the next layer.', +'FANN_NETTYPE_SHORTCUT' => 'Each layer has connections to all following layers', +'FANN_SIGMOID' => 'Sigmoid activation function.', +'FANN_SIGMOID_STEPWISE' => 'Stepwise linear approximation to sigmoid.', +'FANN_SIGMOID_SYMMETRIC' => 'Symmetric sigmoid activation function, aka. tanh.', +'FANN_SIGMOID_SYMMETRIC_STEPWISE' => 'Stepwise linear approximation to symmetric sigmoid', +'FANN_SIN' => 'Periodical sinus activation function.', +'FANN_SIN_SYMMETRIC' => 'Periodical sinus activation function.', +'FANN_STOPFUNC_BIT' => 'Stop criteria is number of bits that fail. The number of bits means the number of output neurons which differs more than the bit fail limit (see fann_get_bit_fail_limit, fann_set_bit_fail_limit). The bits are counted in all of the training data, so this number can be higher than the number of training data.', +'FANN_STOPFUNC_MSE' => 'Stop criteria is Mean Square Error (MSE) value.', +'FANN_THRESHOLD' => 'Threshold activation function.', +'FANN_THRESHOLD_SYMMETRIC' => 'Threshold activation function.', +'FANN_TRAIN_BATCH' => 'Standard backpropagation algorithm, where the weights are updated after calculating the mean square error for the whole training set. This means that the weights are only updated once during a epoch. For this reason some problems, will train slower with this algorithm. But since the mean square error is calculated more correctly than in incremental training, some problems will reach a better solutions with this algorithm.', +'FANN_TRAIN_INCREMENTAL' => 'Standard backpropagation algorithm, where the weights are updated after each training pattern. This means that the weights are updated many times during a single epoch. For this reason some problems, will train very fast with this algorithm, while other more advanced problems will not train very well.', +'FANN_TRAIN_QUICKPROP' => 'A more advanced batch training algorithm which achieves good results for many problems. The quickprop training algorithm uses the learning_rate parameter along with other more advanced parameters, but it is only recommended to change these advanced parameters, for users with insight in how the quickprop training algorithm works. The quickprop training algorithm is described by [Fahlman, 1988].', +'FANN_TRAIN_RPROP' => 'A more advanced batch training algorithm which achieves good results for many problems. The RPROP training algorithm is adaptive, and does therefore not use the learning_rate. Some other parameters can however be set to change the way the RPROP algorithm works, but it is only recommended for users with insight in how the RPROP training algorithm works. The RPROP training algorithm is described by [Riedmiller and Braun, 1993], but the actual learning algorithm used here is the iRPROP- training algorithm which is described by [Igel and Husken, 2000] which is an variety of the standard RPROP training algorithm.', +'FANN_TRAIN_SARPROP' => 'Even more advance training algorithm. Only for version 2.2', +'FILE_APPEND' => 'Append content to existing file.', +'FILE_BINARY' => 'Binary mode (since PHP 5.2.7). This constant has no effect, and is only available for `forward compatibility`.', +'FILE_IGNORE_NEW_LINES' => 'Strip EOL characters.', +'FILE_SKIP_EMPTY_LINES' => 'Skip empty lines.', +'FILE_TEXT' => 'Text mode (since PHP 5.2.7). This constant has no effect, and is only available for `forward compatibility`.', +'FILE_USE_INCLUDE_PATH' => 'Search for filename in include_path.', +'FILEINFO_COMPRESS' => 'Decompress compressed files. + +Disabled since PHP 5.3.0 due to thread safety issues.', +'FILEINFO_CONTINUE' => 'Return all matches, not just the first.', +'FILEINFO_DEVICES' => 'Look at the contents of blocks or character special devices.', +'FILEINFO_EXTENSION' => 'Returns the file extension appropriate for a the MIME type detected in the file. + +For types that commonly have multiple file extensions, such as `JPEG` images, then the return value is multiple extensions separated by a forward slash e.g.: `"jpeg/jpg/jpe/jfif"`. For unknown types not available in the magic.mime database, then return value is `"???"`. + +Available since PHP 7.2.0.', +'FILEINFO_MIME' => 'Return the mime type and mime encoding as defined by RFC 2045.', +'FILEINFO_MIME_ENCODING' => 'Return the mime encoding of the file.', +'FILEINFO_MIME_TYPE' => 'Return the mime type.', +'FILEINFO_NONE' => 'No special handling.', +'FILEINFO_PRESERVE_ATIME' => 'If possible preserve the original access time.', +'FILEINFO_RAW' => 'Don\'t translate unprintable characters to a `\ooo` octal representation.', +'FILEINFO_SYMLINK' => 'Follow symlinks.', +'FILTER_CALLBACK' => 'ID of "callback" filter.', +'FILTER_DEFAULT' => 'ID of default ("unsafe_raw") filter. This is equivalent to `FILTER_UNSAFE_RAW`.', +'FILTER_FLAG_ALLOW_FRACTION' => 'Allow fractional part in "number_float" filter.', +'FILTER_FLAG_ALLOW_HEX' => 'Allow hex notation (`0x[0-9a-fA-F]+`) in "int" filter.', +'FILTER_FLAG_ALLOW_OCTAL' => 'Allow octal notation (`0[0-7]+`) in "int" filter.', +'FILTER_FLAG_ALLOW_SCIENTIFIC' => 'Allow scientific notation (`e`, `E`) in "number_float" filter.', +'FILTER_FLAG_ALLOW_THOUSAND' => 'Allow thousand separator (`,`) in "number_float" filter.', +'FILTER_FLAG_EMAIL_UNICODE' => 'Accepts Unicode characters in the local part in "validate_email" filter. (Available as of PHP 7.1.0)', +'FILTER_FLAG_EMPTY_STRING_NULL' => '(No use for now.)', +'FILTER_FLAG_ENCODE_AMP' => 'Encode `&`.', +'FILTER_FLAG_ENCODE_HIGH' => 'Encode characters with ASCII value greater than 127.', +'FILTER_FLAG_ENCODE_LOW' => 'Encode characters with ASCII value less than 32.', +'FILTER_FLAG_IPV4' => 'Allow only IPv4 address in "validate_ip" filter.', +'FILTER_FLAG_IPV6' => 'Allow only IPv6 address in "validate_ip" filter.', +'FILTER_FLAG_NO_ENCODE_QUOTES' => 'Don\'t encode `\'` and `"`.', +'FILTER_FLAG_NO_PRIV_RANGE' => 'Deny private addresses in "validate_ip" filter.', +'FILTER_FLAG_NO_RES_RANGE' => 'Deny reserved addresses in "validate_ip" filter.', +'FILTER_FLAG_NONE' => 'No flags.', +'FILTER_FLAG_PATH_REQUIRED' => 'Require path in "validate_url" filter.', +'FILTER_FLAG_QUERY_REQUIRED' => 'Require query in "validate_url" filter.', +'FILTER_FLAG_STRIP_HIGH' => 'Strip characters with ASCII value greater than 127.', +'FILTER_FLAG_STRIP_LOW' => 'Strip characters with ASCII value less than 32.', +'FILTER_FORCE_ARRAY' => 'Always returns an array.', +'FILTER_NULL_ON_FAILURE' => 'Use NULL instead of FALSE on failure.', +'FILTER_REQUIRE_ARRAY' => 'Require an array as input.', +'FILTER_REQUIRE_SCALAR' => 'Flag used to require scalar as input', +'FILTER_SANITIZE_EMAIL' => 'ID of "email" filter.', +'FILTER_SANITIZE_ENCODED' => 'ID of "encoded" filter.', +'FILTER_SANITIZE_MAGIC_QUOTES' => 'ID of "magic_quotes" filter.', +'FILTER_SANITIZE_NUMBER_FLOAT' => 'ID of "number_float" filter.', +'FILTER_SANITIZE_NUMBER_INT' => 'ID of "number_int" filter.', +'FILTER_SANITIZE_SPECIAL_CHARS' => 'ID of "special_chars" filter.', +'FILTER_SANITIZE_STRING' => 'ID of "string" filter.', +'FILTER_SANITIZE_STRIPPED' => 'ID of "stripped" filter.', +'FILTER_SANITIZE_URL' => 'ID of "url" filter.', +'FILTER_UNSAFE_RAW' => 'ID of "unsafe_raw" filter.', +'FILTER_VALIDATE_BOOLEAN' => 'ID of "boolean" filter.', +'FILTER_VALIDATE_EMAIL' => 'ID of "validate_email" filter.', +'FILTER_VALIDATE_FLOAT' => 'ID of "float" filter.', +'FILTER_VALIDATE_INT' => 'ID of "int" filter.', +'FILTER_VALIDATE_IP' => 'ID of "validate_ip" filter.', +'FILTER_VALIDATE_MAC' => 'ID of "validate_mac_address" filter. (Available as of PHP 5.5.0)', +'FILTER_VALIDATE_REGEXP' => 'ID of "validate_regexp" filter.', +'FILTER_VALIDATE_URL' => 'ID of "validate_url" filter.', +'FNM_CASEFOLD' => 'Caseless match. Part of the GNU extension.', +'FNM_NOESCAPE' => 'Disable backslash escaping.', +'FNM_PATHNAME' => 'Slash in string only matches slash in the given pattern.', +'FNM_PERIOD' => 'Leading period in string must be exactly matched by period in the given pattern.', +'FPE_FLTDIV' => 'Available since PHP 5.3.0.', +'FPE_FLTINV' => 'Available since PHP 5.3.0.', +'FPE_FLTOVF' => 'Available since PHP 5.3.0.', +'FPE_FLTRES' => 'Available since PHP 5.3.0.', +'FPE_FLTSUB' => 'Available since PHP 5.3.0.', +'FPE_FLTUND' => 'Available since PHP 5.3.0.', +'FPE_INTDIV' => 'Available since PHP 5.3.0.', +'FPE_INTOVF' => 'Available since PHP 5.3.0.', +'FRIBIDI_AUTO' => 'Autodetect the base direction', +'FRIBIDI_CHARSET_8859_6' => 'Arabic', +'FRIBIDI_CHARSET_8859_8' => 'Hebrew', +'FRIBIDI_CHARSET_CAP_RTL' => 'Used for test purposes, will treat CAPS as non-English letters', +'FRIBIDI_CHARSET_CP1255' => 'Hebrew/Yiddish', +'FRIBIDI_CHARSET_CP1256' => 'Arabic', +'FRIBIDI_CHARSET_ISIRI_3342' => 'Persian', +'FRIBIDI_CHARSET_UTF8' => 'Unicode', +'FRIBIDI_LTR' => 'Left to right', +'FRIBIDI_RTL' => 'Right to left', +'FT_INTERNAL' => 'The return string is in internal format, will not canonicalize to CRLF.', +'FT_PEEK' => 'Do not set the \Seen flag if not already set', +'FT_UID' => 'The parameter is a UID', +'FTP_AUTORESUME' => 'Automatically determine resume position and start position for GET and PUT requests (only works if FTP_AUTOSEEK is enabled)', +'FTP_AUTOSEEK' => 'See `ftp_set_option` for information.', +'FTP_FAILED' => 'Asynchronous transfer has failed', +'FTP_FINISHED' => 'Asynchronous transfer has finished', +'FTP_IMAGE' => 'Alias of `FTP_BINARY`.', +'FTP_MOREDATA' => 'Asynchronous transfer is still active', +'FTP_TEXT' => 'Alias of `FTP_ASCII`.', +'FTP_TIMEOUT_SEC' => 'See `ftp_set_option` for information.', +'FTP_USEPASVADDRESS' => 'See `ftp_set_option` for information. Available as of PHP 5.6.0.', +'GD_BUNDLED' => 'When the bundled version of GD is used this is 1 otherwise its set to 0.', +'GD_EXTRA_VERSION' => 'The GD "extra" version (beta/rc..) PHP was compiled against. (Available as of PHP 5.2.4)', +'GD_MAJOR_VERSION' => 'The GD major version PHP was compiled against. (Available as of PHP 5.2.4)', +'GD_MINOR_VERSION' => 'The GD minor version PHP was compiled against. (Available as of PHP 5.2.4)', +'GD_RELEASE_VERSION' => 'The GD release version PHP was compiled against. (Available as of PHP 5.2.4)', +'GD_VERSION' => 'The GD version PHP was compiled against. (Available as of PHP 5.2.4)', +'GEARMAN_CLIENT_FREE_TASKS' => 'Automatically free task objects once they are complete. This is the default setting in this extension to prevent memory leaks.', +'GEARMAN_CLIENT_GENERATE_UNIQUE' => 'Generate a unique id (UUID) for each task.', +'GEARMAN_CLIENT_NON_BLOCKING' => 'Run the cient in a non-blocking mode.', +'GEARMAN_CLIENT_UNBUFFERED_RESULT' => 'Allow the client to read data in chunks rather than have the library buffer the entire data result and pass that back.', +'GEARMAN_COULD_NOT_CONNECT' => 'Failed to connect to servers.', +'GEARMAN_ECHO_DATA_CORRUPTION' => 'After `GearmanClient::echo` or `GearmanWorker::echo` the data returned doesn\'t match the data sent.', +'GEARMAN_ERRNO' => 'A system error. Check `GearmanClient::errno` or `GearmanWorker::errno` for the system error code that was returned.', +'GEARMAN_GETADDRINFO' => 'DNS resolution failed (invalid host, port, etc).', +'GEARMAN_INVALID_FUNCTION_NAME' => 'Trying to register a function name of NULL or using the callback interface without specifying callbacks.', +'GEARMAN_INVALID_WORKER_FUNCTION' => 'Trying to register a function with a NULL callback function.', +'GEARMAN_IO_WAIT' => 'When in non-blocking mode, an event is hit that would have blocked.', +'GEARMAN_LOST_CONNECTION' => 'Lost a connection during a request.', +'GEARMAN_MEMORY_ALLOCATION_FAILURE' => 'Memory allocation failed (ran out of memory).', +'GEARMAN_NEED_WORKLOAD_FN' => 'When the client opted to stream the workload of a task, but did not specify a workload callback function.', +'GEARMAN_NO_ACTIVE_FDS' => '`GearmanClient::wait` or `GearmanWorker` was called with no connections.', +'GEARMAN_NO_JOBS' => 'For a non-blocking worker, when `GearmanWorker::work` does not have any active jobs.', +'GEARMAN_NO_REGISTERED_FUNCTIONS' => 'When a worker gets a job for a function it did not register.', +'GEARMAN_NO_SERVERS' => 'Did not call `GearmanClient::addServer` before submitting jobs or tasks.', +'GEARMAN_PAUSE' => 'For the non-blocking client task interface, can be returned from the task callback to "pause" the call and return from `GearmanClient::runTasks`. Call `GearmanClient::runTasks` again to continue.', +'GEARMAN_SEND_BUFFER_TOO_SMALL' => 'Internal error: trying to flush more data in one atomic chunk than is possible due to hard-coded buffer sizes.', +'GEARMAN_SERVER_ERROR' => 'Something went wrong in the Gearman server and it could not handle the request gracefully.', +'GEARMAN_SUCCESS' => 'Whatever action was taken was successful.', +'GEARMAN_TIMEOUT' => 'Hit the timeout limit set by the client/worker.', +'GEARMAN_UNEXPECTED_PACKET' => 'Indicates something going very wrong in gearmand. Applies only to `GearmanWorker`.', +'GEARMAN_UNKNOWN_STATE' => 'Internal client/worker state error.', +'GEARMAN_WORK_DATA' => 'Notice return code obtained with `GearmanClient::returnCode` when using `GearmanClient::do`. Sent to update the client with data from a running job. A worker uses this when it needs to send updates, send partial results, or flush data during long running jobs.', +'GEARMAN_WORK_EXCEPTION' => 'Notice return code obtained with `GearmanClient::returnCode` when using `GearmanClient::do`. Indicates that a job failed with a given exception.', +'GEARMAN_WORK_FAIL' => 'Notice return code obtained with `GearmanClient::returnCode` when using `GearmanClient::do`. Indicates that the job failed.', +'GEARMAN_WORK_STATUS' => 'Notice return code obtained with `GearmanClient::returnCode` when using `GearmanClient::do`. Sent to update the status of a long running job. Use `GearmanClient::doStatus` to obtain the percentage complete of the task.', +'GEARMAN_WORK_WARNING' => 'Notice return code obtained with `GearmanClient::returnCode` when using `GearmanClient::do`. Updates the client with a warning. The behavior is just like `GEARMAN_WORK_DATA`, but should be treated as a warning instead of normal response data.', +'GEARMAN_WORKER_GRAB_UNIQ' => 'Return the client assigned unique ID in addition to the job handle.', +'GEARMAN_WORKER_NON_BLOCKING' => 'Run the worker in non-blocking mode.', +'Gmagick::COLOR_ALPHA' => 'Alpha', +'Gmagick::COLOR_BLACK' => 'Black', +'Gmagick::COLOR_BLUE' => 'Blue', +'Gmagick::COLOR_CYAN' => 'Cyan', +'Gmagick::COLOR_FUZZ' => 'Fuzz', +'Gmagick::COLOR_GREEN' => 'Green', +'Gmagick::COLOR_MAGENTA' => 'Magenta', +'Gmagick::COLOR_OPACITY' => 'Opacity', +'Gmagick::COLOR_RED' => 'Red', +'Gmagick::COLOR_YELLOW' => 'Yellow', +'Gmagick::COMPOSITE_ADD' => 'The result of image + image', +'Gmagick::COMPOSITE_ATOP' => 'The result is the same shape as image, with composite image obscuring image where the image shapes overlap', +'Gmagick::COMPOSITE_BLEND' => 'Blends the image', +'Gmagick::COMPOSITE_BUMPMAP' => 'The same as COMPOSITE_MULTIPLY, except the source is converted to grayscale first.', +'Gmagick::COMPOSITE_CLEAR' => 'Makes the target image transparent', +'Gmagick::COMPOSITE_COLORBURN' => 'Darkens the destination image to reflect the source image', +'Gmagick::COMPOSITE_COLORDODGE' => 'Brightens the destination image to reflect the source image', +'Gmagick::COMPOSITE_COLORIZE' => 'Colorizes the target image using the composite image', +'Gmagick::COMPOSITE_COPY' => 'Copies the source image on the target image', +'Gmagick::COMPOSITE_COPYBLACK' => 'Copies black from the source to target', +'Gmagick::COMPOSITE_COPYBLUE' => 'Copies blue from the source to target', +'Gmagick::COMPOSITE_COPYCYAN' => 'Copies cyan from the source to target', +'Gmagick::COMPOSITE_COPYGREEN' => 'Copies green from the source to target', +'Gmagick::COMPOSITE_COPYMAGENTA' => 'Copies magenta from the source to target', +'Gmagick::COMPOSITE_COPYOPACITY' => 'Copies opacity from the source to target', +'Gmagick::COMPOSITE_COPYRED' => 'Copies red from the source to target', +'Gmagick::COMPOSITE_COPYYELLOW' => 'Copies yellow from the source to target', +'Gmagick::COMPOSITE_DARKEN' => 'Darkens the target image', +'Gmagick::COMPOSITE_DEFAULT' => 'The default composite operator', +'Gmagick::COMPOSITE_DIFFERENCE' => 'Subtracts the darker of the two constituent colors from the lighter', +'Gmagick::COMPOSITE_DISPLACE' => 'Shifts target image pixels as defined by the source', +'Gmagick::COMPOSITE_DISSOLVE' => 'Dissolves the source in to the target', +'Gmagick::COMPOSITE_DST' => 'The target is left untouched', +'Gmagick::COMPOSITE_DSTATOP' => 'The part of the destination lying inside of the source is composited over the source and replaces the destination', +'Gmagick::COMPOSITE_DSTIN' => 'The parts inside the source replace the target', +'Gmagick::COMPOSITE_DSTOUT' => 'The parts outside the source replace the target', +'Gmagick::COMPOSITE_DSTOVER' => 'Target replaces the source', +'Gmagick::COMPOSITE_EXCLUSION' => 'Produces an effect similar to that of Gmagick::COMPOSITE_DIFFERENCE, but appears as lower contrast', +'Gmagick::COMPOSITE_HARDLIGHT' => 'Multiplies or screens the colors, dependent on the source color value', +'Gmagick::COMPOSITE_HUE' => 'Modifies the hue of the target as defined by source', +'Gmagick::COMPOSITE_IN' => 'Composites source into the target', +'Gmagick::COMPOSITE_LIGHTEN' => 'Lightens the target as defined by source', +'Gmagick::COMPOSITE_LUMINIZE' => 'Luminizes the target as defined by source', +'Gmagick::COMPOSITE_MINUS' => 'Subtracts the source from the target', +'Gmagick::COMPOSITE_MODULATE' => 'Modulates the target brightness, saturation and hue as defined by source', +'Gmagick::COMPOSITE_MULTIPLY' => 'Multiplies the target to the source', +'Gmagick::COMPOSITE_NO' => 'No composite operator defined', +'Gmagick::COMPOSITE_OUT' => 'Composites outer parts of the source on the target', +'Gmagick::COMPOSITE_OVER' => 'Composites source over the target', +'Gmagick::COMPOSITE_OVERLAY' => 'Overlays the source on the target', +'Gmagick::COMPOSITE_PLUS' => 'Adds the source to the target', +'Gmagick::COMPOSITE_REPLACE' => 'Replaces the target with the source', +'Gmagick::COMPOSITE_SATURATE' => 'Saturates the target as defined by the source', +'Gmagick::COMPOSITE_SCREEN' => 'The source and destination are complemented and then multiplied and then replace the destination', +'Gmagick::COMPOSITE_SOFTLIGHT' => 'Darkens or lightens the colors, dependent on the source', +'Gmagick::COMPOSITE_SRC' => 'The source is copied to the destination', +'Gmagick::COMPOSITE_SRCATOP' => 'The part of the source lying inside of the destination is composited onto the destination', +'Gmagick::COMPOSITE_SRCIN' => 'The part of the source lying inside of the destination replaces the destination', +'Gmagick::COMPOSITE_SRCOUT' => 'The part of the source lying outside of the destination replaces the destination', +'Gmagick::COMPOSITE_SRCOVER' => 'The source replaces the destination', +'Gmagick::COMPOSITE_SUBTRACT' => 'Subtract the colors in the source image from the destination image', +'Gmagick::COMPOSITE_THRESHOLD' => 'The source is composited on the target as defined by source threshold', +'Gmagick::COMPOSITE_UNDEFINED' => 'Undefined composite operator', +'Gmagick::COMPOSITE_XOR' => 'The part of the source that lies outside of the destination is combined with the part of the destination that lies outside of the source', +'GMP_VERSION' => 'The GMP library version', +'Grpc\CALL_ERROR' => 'something failed, we don\'t know what', +'Grpc\CALL_ERROR_ALREADY_ACCEPTED' => 'this method must be called before server_accept', +'Grpc\CALL_ERROR_ALREADY_FINISHED' => 'this call is already finished +(writes_done or write_status has already been called)', +'Grpc\CALL_ERROR_ALREADY_INVOKED' => 'this method must be called before invoke', +'Grpc\CALL_ERROR_BATCH_TOO_BIG' => 'this batch of operations leads to more operations than allowed', +'Grpc\CALL_ERROR_INVALID_FLAGS' => 'the flags value was illegal for this call', +'Grpc\CALL_ERROR_INVALID_MESSAGE' => 'invalid message was passed to this call', +'Grpc\CALL_ERROR_INVALID_METADATA' => 'invalid metadata was passed to this call', +'Grpc\CALL_ERROR_NOT_INVOKED' => 'this method must be called after invoke', +'Grpc\CALL_ERROR_NOT_ON_CLIENT' => 'this method is not available on the client', +'Grpc\CALL_ERROR_NOT_ON_SERVER' => 'this method is not available on the server', +'Grpc\CALL_ERROR_NOT_SERVER_COMPLETION_QUEUE' => 'completion queue for notification has not been registered with the +server', +'Grpc\CALL_ERROR_PAYLOAD_TYPE_MISMATCH' => 'payload type requested is not the type registered', +'Grpc\CALL_ERROR_TOO_MANY_OPERATIONS' => 'there is already an outstanding read/write operation on the call', +'Grpc\CALL_OK' => 'everything went ok', +'Grpc\CHANNEL_CONNECTING' => 'channel is connecting', +'Grpc\CHANNEL_IDLE' => 'channel is idle', +'Grpc\CHANNEL_READY' => 'channel is ready for work', +'Grpc\CHANNEL_SHUTDOWN' => 'channel has seen a failure that it cannot recover from', +'Grpc\CHANNEL_TRANSIENT_FAILURE' => 'channel has seen a failure but expects to recover', +'Grpc\OP_RECV_CLOSE_ON_SERVER' => 'Receive close on the server: one and only one must be made on the +server. +This op completes after the close has been received by the server. +This operation always succeeds, meaning ops paired with this operation +will also appear to succeed, even though they may not have.', +'Grpc\OP_RECV_INITIAL_METADATA' => 'Receive initial metadata: one and only one MUST be made on the client, +must not be made on the server. +This op completes after all initial metadata has been read from the +peer.', +'Grpc\OP_RECV_MESSAGE' => 'Receive a message: 0 or more of these operations can occur for each call. +This op completes after all bytes of the received message have been +read, or after a half-close has been received on this call.', +'Grpc\OP_RECV_STATUS_ON_CLIENT' => 'Receive status on the client: one and only one must be made on the client. +This operation always succeeds, meaning ops paired with this operation +will also appear to succeed, even though they may not have. In that case +the status will indicate some failure. +This op completes after all activity on the call has completed.', +'Grpc\OP_SEND_CLOSE_FROM_CLIENT' => 'Send a close from the client: one and only one instance MUST be sent from +the client, unless the call was cancelled - in which case this can be +skipped. +This op completes after all bytes for the call (including the close) +have passed outgoing flow control.', +'Grpc\OP_SEND_INITIAL_METADATA' => 'Send initial metadata: one and only one instance MUST be sent for each +call, unless the call was cancelled - in which case this can be skipped. +This op completes after all bytes of metadata have been accepted by +outgoing flow control.', +'Grpc\OP_SEND_MESSAGE' => 'Send a message: 0 or more of these operations can occur for each call. +This op completes after all bytes for the message have been accepted by +outgoing flow control.', +'Grpc\OP_SEND_STATUS_FROM_SERVER' => 'Send status from the server: one and only one instance MUST be sent from +the server unless the call was cancelled - in which case this can be +skipped. +This op completes after all bytes for the call (including the status) +have passed outgoing flow control.', +'Grpc\STATUS_ABORTED' => 'The operation was aborted, typically due to a concurrency issue +like sequencer check failures, transaction aborts, etc. + +See litmus test above for deciding between FAILED_PRECONDITION, +ABORTED, and UNAVAILABLE.', +'Grpc\STATUS_CANCELLED' => 'The operation was cancelled (typically by the caller).', +'Grpc\STATUS_DATA_LOSS' => 'Unrecoverable data loss or corruption.', +'Grpc\STATUS_DEADLINE_EXCEEDED' => 'Deadline expired before operation could complete. For operations +that change the state of the system, this error may be returned +even if the operation has completed successfully. For example, a +successful response from a server could have been delayed long +enough for the deadline to expire.', +'Grpc\STATUS_FAILED_PRECONDITION' => 'Operation was rejected because the system is not in a state +required for the operation\'s execution. For example, directory +to be deleted may be non-empty, an rmdir operation is applied to +a non-directory, etc. + +A litmus test that may help a service implementor in deciding +between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: + (a) Use UNAVAILABLE if the client can retry just the failing call. + (b) Use ABORTED if the client should retry at a higher-level + (e.g., restarting a read-modify-write sequence). + (c) Use FAILED_PRECONDITION if the client should not retry until + the system state has been explicitly fixed. E.g., if an "rmdir" + fails because the directory is non-empty, FAILED_PRECONDITION + should be returned since the client should not retry unless + they have first fixed up the directory by deleting files from it. + (d) Use FAILED_PRECONDITION if the client performs conditional + REST Get/Update/Delete on a resource and the resource on the + server does not match the condition. E.g., conflicting + read-modify-write on the same resource.', +'Grpc\STATUS_INTERNAL' => 'Internal errors. Means some invariants expected by underlying + system has been broken. If you see one of these errors, + something is very broken.', +'Grpc\STATUS_INVALID_ARGUMENT' => 'Client specified an invalid argument. Note that this differs +from FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments +that are problematic regardless of the state of the system +(e.g., a malformed file name).', +'Grpc\STATUS_NOT_FOUND' => 'Some requested entity (e.g., file or directory) was not found.', +'Grpc\STATUS_OK' => 'Not an error; returned on success', +'Grpc\STATUS_OUT_OF_RANGE' => 'Operation was attempted past the valid range. E.g., seeking or +reading past end of file. + +Unlike INVALID_ARGUMENT, this error indicates a problem that may +be fixed if the system state changes. For example, a 32-bit file +system will generate INVALID_ARGUMENT if asked to read at an +offset that is not in the range [0,2^32-1], but it will generate +OUT_OF_RANGE if asked to read from an offset past the current +file size. + +There is a fair bit of overlap between FAILED_PRECONDITION and +OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific +error) when it applies so that callers who are iterating through +a space can easily look for an OUT_OF_RANGE error to detect when +they are done.', +'Grpc\STATUS_PERMISSION_DENIED' => 'The caller does not have permission to execute the specified +operation. PERMISSION_DENIED must not be used for rejections +caused by exhausting some resource (use RESOURCE_EXHAUSTED +instead for those errors). PERMISSION_DENIED must not be +used if the caller can not be identified (use UNAUTHENTICATED +instead for those errors).', +'Grpc\STATUS_RESOURCE_EXHAUSTED' => 'Some resource has been exhausted, perhaps a per-user quota, or +perhaps the entire file system is out of space.', +'Grpc\STATUS_UNAUTHENTICATED' => 'The request does not have valid authentication credentials for the +operation.', +'Grpc\STATUS_UNAVAILABLE' => 'The service is currently unavailable. This is a most likely a +transient condition and may be corrected by retrying with +a backoff. + +See litmus test above for deciding between FAILED_PRECONDITION, +ABORTED, and UNAVAILABLE.', +'Grpc\STATUS_UNIMPLEMENTED' => 'Operation is not implemented or not supported/enabled in this service.', +'Grpc\STATUS_UNKNOWN' => 'Unknown error. An example of where this error may be returned is +if a Status value received from another address space belongs to +an error-space that is not known in this address space. Also +errors raised by APIs that do not return enough error information +may be converted to this error.', +'Grpc\WRITE_BUFFER_HINT' => 'Hint that the write may be buffered and need not go out on the wire +immediately. GRPC is free to buffer the message until the next non-buffered +write, or until writes_done, but it need not buffer completely or at all.', +'Grpc\WRITE_NO_COMPRESS' => 'Force compression to be disabled for a particular write +(start_write/add_metadata). Illegal on invoke/accept.', +'GSLC_SSL_NO_AUTH' => 'SSL Authentication Mode - No authentication required. (Only for Oracle LDAP)', +'GSLC_SSL_ONEWAY_AUTH' => 'SSL Authentication Mode - Only server authentication required. (Only for Oracle LDAP)', +'GSLC_SSL_TWOWAY_AUTH' => 'SSL Authentication Mode - Both server and client authentication required. (Only for Oracle LDAP)', +'HASH_HMAC' => 'Optional flag for `hash_init`. Indicates that the HMAC digest-keying algorithm should be applied to the current hashing context.', +'http\Client::DEBUG_BODY' => 'Debug callback\'s $data contains a body part.', +'http\Client::DEBUG_HEADER' => 'Debug callback\'s $data contains headers.', +'http\Client::DEBUG_IN' => 'Debug callback\'s $data contains data received.', +'http\Client::DEBUG_INFO' => 'Debug callback\'s $data contains human readable text.', +'http\Client::DEBUG_OUT' => 'Debug callback\'s $data contains data sent.', +'http\Client::DEBUG_SSL' => 'Debug callback\'s $data contains SSL data.', +'http\Client\Curl\AUTH_ANY' => 'Use any authentication.', +'http\Client\Curl\AUTH_BASIC' => 'Use Basic authentication.', +'http\Client\Curl\AUTH_DIGEST' => 'Use Digest authentication.', +'http\Client\Curl\AUTH_DIGEST_IE' => 'Use IE (lower v7) quirks with Digest authentication. Available if libcurl is v7.19.3 or more recent.', +'http\Client\Curl\AUTH_GSSNEG' => 'Use GSS-Negotiate authentication.', +'http\Client\Curl\AUTH_NTLM' => 'Use NTLM authentication.', +'http\Client\Curl\AUTH_SPNEGO' => 'Use HTTP Netgotiate authentication (SPNEGO, RFC4559). Available if libcurl is v7.38.0 or more recent.', +'http\Client\Curl\FEATURES' => 'Bitmask of available libcurl features. + See http\Client\Curl\Features namespace.', +'http\Client\Curl\Features\ASYNCHDNS' => 'Whether libcurl supports asynchronous domain name resolution.', +'http\Client\Curl\Features\GSSAPI' => 'Whether libcurl supports the Generic Security Services Application Program Interface. Available if libcurl is v7.38.0 or more recent.', +'http\Client\Curl\Features\GSSNEGOTIATE' => 'Whether libcurl supports HTTP Generic Security Services negotiation.', +'http\Client\Curl\Features\HTTP2' => 'Whether libcurl supports the HTTP/2 protocol. Available if libcurl is v7.33.0 or more recent.', +'http\Client\Curl\Features\IDN' => 'Whether libcurl supports international domain names.', +'http\Client\Curl\Features\IPV6' => 'Whether libcurl supports IPv6.', +'http\Client\Curl\Features\KERBEROS4' => 'Whether libcurl supports the old Kerberos protocol.', +'http\Client\Curl\Features\KERBEROS5' => 'Whether libcurl supports the more recent Kerberos v5 protocol. Available if libcurl is v7.40.0 or more recent.', +'http\Client\Curl\Features\LARGEFILE' => 'Whether libcurl supports large files.', +'http\Client\Curl\Features\LIBZ' => 'Whether libcurl supports gzip/deflate compression.', +'http\Client\Curl\Features\NTLM' => 'Whether libcurl supports the NT Lan Manager authentication.', +'http\Client\Curl\Features\NTLM_WB' => 'Whether libcurl supports NTLM delegation to a winbind helper. Available if libcurl is v7.22.0 or more recent.', +'http\Client\Curl\Features\PSL' => 'Whether libcurl supports the Public Suffix List for cookie host handling. Available if libcurl is v7.47.0 or more recent.', +'http\Client\Curl\Features\SPNEGO' => 'Whether libcurl supports the Simple and Protected GSSAPI Negotiation Mechanism.', +'http\Client\Curl\Features\SSL' => 'Whether libcurl supports SSL/TLS protocols.', +'http\Client\Curl\Features\SSPI' => 'Whether libcurl supports the Security Support Provider Interface.', +'http\Client\Curl\Features\TLSAUTH_SRP' => 'Whether libcurl supports TLS Secure Remote Password authentication. Available if libcurl is v7.21.4 or more recent.', +'http\Client\Curl\Features\UNIX_SOCKETS' => 'Whether libcurl supports connections to unix sockets. Available if libcurl is v7.40.0 or more recent.', +'http\Client\Curl\HTTP_VERSION_1_0' => 'Use HTTP/1.0 protocol version.', +'http\Client\Curl\HTTP_VERSION_1_1' => 'Use HTTP/1.1 protocol version.', +'http\Client\Curl\HTTP_VERSION_2_0' => 'Attempt to use HTTP/2 protocol version. Available if libcurl is v7.33.0 or more recent and was built with nghttp2 support.', +'http\Client\Curl\HTTP_VERSION_2TLS' => 'Attempt to use version 2 for HTTPS, version 1.1 for HTTP. Available if libcurl is v7.47.0 or more recent and was built with nghttp2 support.', +'http\Client\Curl\HTTP_VERSION_ANY' => 'Use any HTTP protocol version.', +'http\Client\Curl\IPRESOLVE_ANY' => 'Use any resolver.', +'http\Client\Curl\IPRESOLVE_V4' => 'Use IPv4 resolver.', +'http\Client\Curl\IPRESOLVE_V6' => 'Use IPv6 resolver.', +'http\Client\Curl\POSTREDIR_301' => 'Keep POSTing on 301 redirects. Available if libcurl is v7.19.1 or more recent.', +'http\Client\Curl\POSTREDIR_302' => 'Keep POSTing on 302 redirects. Available if libcurl is v7.19.1 or more recent.', +'http\Client\Curl\POSTREDIR_303' => 'Keep POSTing on 303 redirects. Available if libcurl is v7.19.1 or more recent.', +'http\Client\Curl\POSTREDIR_ALL' => 'Keep POSTing on any redirect. Available if libcurl is v7.19.1 or more recent.', +'http\Client\Curl\PROXY_HTTP' => 'Use HTTP/1.1 proxy protocol.', +'http\Client\Curl\PROXY_HTTP_1_0' => 'Use HTTP/1.0 proxy protocol. Available if libcurl is v7.19.4 or more recent.', +'http\Client\Curl\PROXY_SOCKS4' => 'Use SOCKSv4 proxy protocol.', +'http\Client\Curl\PROXY_SOCKS4A' => 'Use SOCKSv4a proxy protocol.', +'http\Client\Curl\PROXY_SOCKS5' => 'Use SOCKS5 proxy protoccol.', +'http\Client\Curl\PROXY_SOCKS5_HOSTNAME' => 'Use SOCKS5h proxy protocol.', +'http\Client\Curl\SSL_VERSION_ANY' => 'Use any encryption.', +'http\Client\Curl\SSL_VERSION_SSLv2' => 'Use SSL v2 encryption.', +'http\Client\Curl\SSL_VERSION_SSLv3' => 'Use SSL v3 encryption.', +'http\Client\Curl\SSL_VERSION_TLSv1' => 'Use any TLS v1 encryption.', +'http\Client\Curl\SSL_VERSION_TLSv1_0' => 'Use TLS v1.0 encryption.', +'http\Client\Curl\SSL_VERSION_TLSv1_1' => 'Use TLS v1.1 encryption.', +'http\Client\Curl\SSL_VERSION_TLSv1_2' => 'Use TLS v1.2 encryption.', +'http\Client\Curl\TLSAUTH_SRP' => 'Use TLS SRP authentication. Available if libcurl is v7.21.4 or more recent and was built with gnutls or openssl with TLS-SRP support.', +'http\Client\Curl\User::POLL_IN' => 'Poll for read readiness.', +'http\Client\Curl\User::POLL_INOUT' => 'Poll for read/write readiness.', +'http\Client\Curl\User::POLL_NONE' => 'No action.', +'http\Client\Curl\User::POLL_OUT' => 'Poll for write readiness.', +'http\Client\Curl\User::POLL_REMOVE' => 'Stop polling for activity on this descriptor.', +'http\Client\Curl\VERSIONS' => 'List of library versions of or linked into libcurl, + e.g. "libcurl/7.50.0 OpenSSL/1.0.2h zlib/1.2.8 libidn/1.32 nghttp2/1.12.0". + See http\Client\Curl\Versions namespace.', +'http\Client\Curl\Versions\ARES' => 'Version string of the c-ares library, e.g. "1.11.0".', +'http\Client\Curl\Versions\CURL' => 'Version string of libcurl, e.g. "7.50.0".', +'http\Client\Curl\Versions\IDN' => 'Version string of the IDN library, e.g. "1.32".', +'http\Client\Curl\Versions\LIBZ' => 'Version string of the zlib compression library, e.g. "1.2.8".', +'http\Client\Curl\Versions\SSL' => 'Version string of the SSL/TLS library, e.g. "OpenSSL/1.0.2h".', +'http\Client\Request::TYPE_NONE' => 'No specific type of message.', +'http\Client\Request::TYPE_REQUEST' => 'A request message.', +'http\Client\Request::TYPE_RESPONSE' => 'A response message.', +'http\Client\Response::TYPE_NONE' => 'No specific type of message.', +'http\Client\Response::TYPE_REQUEST' => 'A request message.', +'http\Client\Response::TYPE_RESPONSE' => 'A response message.', +'http\Cookie::HTTPONLY' => 'The cookies\' flags have the httpOnly attribute set.', +'http\Cookie::PARSE_RAW' => 'Do not decode cookie contents.', +'http\Cookie::SECURE' => 'The cookies\' flags have the secure attribute set.', +'http\Encoding\Stream::FLUSH_FULL' => 'Flush at each IO operation.', +'http\Encoding\Stream::FLUSH_NONE' => 'Do no intermittent flushes.', +'http\Encoding\Stream::FLUSH_SYNC' => 'Flush at appropriate transfer points.', +'http\Encoding\Stream\Debrotli::FLUSH_FULL' => 'Flush at each IO operation.', +'http\Encoding\Stream\Debrotli::FLUSH_NONE' => 'Do no intermittent flushes.', +'http\Encoding\Stream\Debrotli::FLUSH_SYNC' => 'Flush at appropriate transfer points.', +'http\Encoding\Stream\Dechunk::FLUSH_FULL' => 'Flush at each IO operation.', +'http\Encoding\Stream\Dechunk::FLUSH_NONE' => 'Do no intermittent flushes.', +'http\Encoding\Stream\Dechunk::FLUSH_SYNC' => 'Flush at appropriate transfer points.', +'http\Encoding\Stream\Deflate::FLUSH_FULL' => 'Flush at each IO operation.', +'http\Encoding\Stream\Deflate::FLUSH_NONE' => 'Do no intermittent flushes.', +'http\Encoding\Stream\Deflate::FLUSH_SYNC' => 'Flush at appropriate transfer points.', +'http\Encoding\Stream\Deflate::LEVEL_DEF' => 'Default compression level.', +'http\Encoding\Stream\Deflate::LEVEL_MAX' => 'Greatest compression level.', +'http\Encoding\Stream\Deflate::LEVEL_MIN' => 'Least compression level.', +'http\Encoding\Stream\Deflate::STRATEGY_DEF' => 'Default compression strategy.', +'http\Encoding\Stream\Deflate::STRATEGY_FILT' => 'Filtered compression strategy.', +'http\Encoding\Stream\Deflate::STRATEGY_FIXED' => 'Encoding with fixed Huffman codes only. + +> **A note on the compression strategy:** +> +> The strategy parameter is used to tune the compression algorithm. +> +> Use the value DEFAULT_STRATEGY for normal data, FILTERED for data produced by a filter (or predictor), HUFFMAN_ONLY to force Huffman encoding only (no string match), or RLE to limit match distances to one (run-length encoding). +> +> Filtered data consists mostly of small values with a somewhat random distribution. In this case, the compression algorithm is tuned to compress them better. The effect of FILTERED is to force more Huffman coding and less string matching; it is somewhat intermediate between DEFAULT_STRATEGY and HUFFMAN_ONLY. +> +> RLE is designed to be almost as fast as HUFFMAN_ONLY, but give better compression for PNG image data. +> +> FIXED prevents the use of dynamic Huffman codes, allowing for a simpler decoder for special applications. +> +> The strategy parameter only affects the compression ratio but not the correctness of the compressed output even if it is not set appropriately. +> +>_Source: [zlib Manual](http://www.zlib.net/manual.html)_', +'http\Encoding\Stream\Deflate::STRATEGY_HUFF' => 'Huffman strategy only.', +'http\Encoding\Stream\Deflate::STRATEGY_RLE' => 'Run-length encoding strategy.', +'http\Encoding\Stream\Deflate::TYPE_GZIP' => 'Gzip encoding. RFC1952', +'http\Encoding\Stream\Deflate::TYPE_RAW' => 'Deflate encoding. RFC1951', +'http\Encoding\Stream\Deflate::TYPE_ZLIB' => 'Zlib encoding. RFC1950', +'http\Encoding\Stream\Enbrotli::FLUSH_FULL' => 'Flush at each IO operation.', +'http\Encoding\Stream\Enbrotli::FLUSH_NONE' => 'Do no intermittent flushes.', +'http\Encoding\Stream\Enbrotli::FLUSH_SYNC' => 'Flush at appropriate transfer points.', +'http\Encoding\Stream\Enbrotli::LEVEL_DEF' => 'Default compression level.', +'http\Encoding\Stream\Enbrotli::LEVEL_MAX' => 'Greatest compression level.', +'http\Encoding\Stream\Enbrotli::LEVEL_MIN' => 'Least compression level.', +'http\Encoding\Stream\Enbrotli::MODE_FONT' => 'Compression mode used in WOFF 2.0.', +'http\Encoding\Stream\Enbrotli::MODE_GENERIC' => 'Default compression mode.', +'http\Encoding\Stream\Enbrotli::MODE_TEXT' => 'Compression mode for UTF-8 formatted text.', +'http\Encoding\Stream\Enbrotli::WBITS_DEF' => 'Default window bits.', +'http\Encoding\Stream\Enbrotli::WBITS_MAX' => 'Maximum window bits.', +'http\Encoding\Stream\Enbrotli::WBITS_MIN' => 'Minimum window bits.', +'http\Encoding\Stream\Inflate::FLUSH_FULL' => 'Flush at each IO operation.', +'http\Encoding\Stream\Inflate::FLUSH_NONE' => 'Do no intermittent flushes.', +'http\Encoding\Stream\Inflate::FLUSH_SYNC' => 'Flush at appropriate transfer points.', +'http\Env\Request::TYPE_NONE' => 'No specific type of message.', +'http\Env\Request::TYPE_REQUEST' => 'A request message.', +'http\Env\Request::TYPE_RESPONSE' => 'A response message.', +'http\Env\Response::CACHE_HIT' => 'The cache was hit.', +'http\Env\Response::CACHE_MISS' => 'The cache was missed.', +'http\Env\Response::CACHE_NO' => 'No caching info available.', +'http\Env\Response::CONTENT_ENCODING_GZIP' => 'Support "Accept-Encoding" requests with gzip and deflate encoding.', +'http\Env\Response::CONTENT_ENCODING_NONE' => 'Do not use content encoding.', +'http\Env\Response::TYPE_NONE' => 'No specific type of message.', +'http\Env\Response::TYPE_REQUEST' => 'A request message.', +'http\Env\Response::TYPE_RESPONSE' => 'A response message.', +'http\Header::MATCH_CASE' => 'Perform case sensitive matching.', +'http\Header::MATCH_FULL' => 'Match the complete string.', +'http\Header::MATCH_LOOSE' => 'None of the following match constraints applies.', +'http\Header::MATCH_STRICT' => 'Case sensitively match the full string (same as MATCH_CASE|MATCH_FULL).', +'http\Header::MATCH_WORD' => 'Match only on word boundaries (according by CType alpha-numeric).', +'http\Header\Parser::CLEANUP' => 'Finish up parser at end of (incomplete) input.', +'http\Header\Parser::STATE_DONE' => 'Finished parsing the headers. + +> ***NOTE:*** +> Most of this states won\'t be returned to the user, because the parser immediately jumps to the next expected state.', +'http\Header\Parser::STATE_FAILURE' => 'Parse failure.', +'http\Header\Parser::STATE_HEADER_DONE' => 'A header was completed.', +'http\Header\Parser::STATE_KEY' => 'Expecting a key or already parsing a key.', +'http\Header\Parser::STATE_START' => 'Expecting HTTP info (request/response line) or headers.', +'http\Header\Parser::STATE_VALUE' => 'Expecting a value or already parsing the value.', +'http\Header\Parser::STATE_VALUE_EX' => 'At EOL of an header, checking whether a folded header line follows.', +'http\Message::TYPE_NONE' => 'No specific type of message.', +'http\Message::TYPE_REQUEST' => 'A request message.', +'http\Message::TYPE_RESPONSE' => 'A response message.', +'http\Message\Parser::CLEANUP' => 'Finish up parser at end of (incomplete) input.', +'http\Message\Parser::DUMB_BODIES' => 'Soak up the rest of input if no entity length is deducible.', +'http\Message\Parser::EMPTY_REDIRECTS' => 'Redirect messages do not contain any body despite of indication of such.', +'http\Message\Parser::GREEDY' => 'Continue parsing while input is available.', +'http\Message\Parser::STATE_BODY' => 'Parsing the body.', +'http\Message\Parser::STATE_BODY_CHUNKED' => 'Parsing `chunked` encoded body.', +'http\Message\Parser::STATE_BODY_DONE' => 'Finished parsing the body.', +'http\Message\Parser::STATE_BODY_DUMB' => 'Soaking up all input as body.', +'http\Message\Parser::STATE_BODY_LENGTH' => 'Reading body as indicated by `Content-Length` or `Content-Range`.', +'http\Message\Parser::STATE_DONE' => 'Finished parsing the message. + +> ***NOTE:*** +> Most of this states won\'t be returned to the user, because the parser immediately jumps to the next expected state.', +'http\Message\Parser::STATE_FAILURE' => 'Parse failure.', +'http\Message\Parser::STATE_HEADER' => 'Parsing headers.', +'http\Message\Parser::STATE_HEADER_DONE' => 'Completed parsing headers.', +'http\Message\Parser::STATE_START' => 'Expecting HTTP info (request/response line) or headers.', +'http\Message\Parser::STATE_UPDATE_CL' => 'Updating Content-Length based on body size.', +'http\Params::COOKIE_PARAM_SEP' => 'Empty param separator to parse cookies.', +'http\Params::DEF_ARG_SEP' => 'The default argument separator (";").', +'http\Params::DEF_PARAM_SEP' => 'The default parameter separator (",").', +'http\Params::DEF_VAL_SEP' => 'The default value separator ("=").', +'http\Params::PARSE_DEFAULT' => 'Interpret input as default formatted parameters.', +'http\Params::PARSE_DIMENSION' => 'Parse sub dimensions indicated by square brackets.', +'http\Params::PARSE_ESCAPED' => 'Parse backslash escaped (quoted) strings.', +'http\Params::PARSE_QUERY' => 'Parse URL querystring (same as http\Params::PARSE_URLENCODED|http\Params::PARSE_DIMENSION).', +'http\Params::PARSE_RAW' => 'Do not interpret the parsed parameters.', +'http\Params::PARSE_RFC5987' => 'Parse [RFC5987](http://tools.ietf.org/html/rfc5987) style encoded character set and language information embedded in HTTP header params.', +'http\Params::PARSE_RFC5988' => 'Parse [RFC5988](http://tools.ietf.org/html/rfc5988) (Web Linking) tags of Link headers.', +'http\Params::PARSE_URLENCODED' => 'Urldecode single units of parameters, arguments and values.', +'http\QueryString::TYPE_ARRAY' => 'Cast requested value to an array.', +'http\QueryString::TYPE_BOOL' => 'Cast requested value to bool.', +'http\QueryString::TYPE_FLOAT' => 'Cast requested value to float.', +'http\QueryString::TYPE_INT' => 'Cast requested value to int.', +'http\QueryString::TYPE_OBJECT' => 'Cast requested value to an object.', +'http\QueryString::TYPE_STRING' => 'Cast requested value to string.', +'http\Url::FROM_ENV' => 'Import initial URL parts from the SAPI environment.', +'http\Url::IGNORE_ERRORS' => 'Continue parsing when encountering errors.', +'http\Url::JOIN_PATH' => 'Whether a relative path should be joined into the old path.', +'http\Url::JOIN_QUERY' => 'Whether the querystrings should be joined.', +'http\Url::PARSE_MBLOC' => 'Parse locale encoded multibyte sequences (on systems with wide character support).', +'http\Url::PARSE_MBUTF8' => 'Parse UTF-8 encoded multibyte sequences.', +'http\Url::PARSE_TOIDN' => 'Parse and convert multibyte hostnames according to IDNA (with IDNA support).', +'http\Url::PARSE_TOIDN_2003' => 'Explicitly request IDNA2003 implementation if available (libidn, idnkit or ICU).', +'http\Url::PARSE_TOIDN_2008' => 'Explicitly request IDNA2008 implementation if available (libidn2, idnkit2 or ICU).', +'http\Url::PARSE_TOPCT' => 'Percent encode multibyte sequences in the userinfo, path, query and fragment parts of the URL.', +'http\Url::REPLACE' => 'Replace parts of the old URL with parts of the new.', +'http\Url::SANITIZE_PATH' => 'Whether to sanitize the URL path (consolidate double slashes, directory jumps etc.)', +'http\Url::SILENT_ERRORS' => 'Suppress errors/exceptions.', +'http\Url::STDFLAGS' => 'Standard flags used by default internally for e.g. http\Message::setRequestUrl(). + Enables joining path and query, sanitizing path, multibyte/unicode, international domain names and transient percent encoding.', +'http\Url::STRIP_ALL' => 'Strip everything except scheme and host information.', +'http\Url::STRIP_AUTH' => 'Strip user and password information from URL (same as STRIP_USER|STRIP_PASS).', +'http\Url::STRIP_FRAGMENT' => 'Strip the fragment (hash) from the URL.', +'http\Url::STRIP_PASS' => 'Strip the password from the URL.', +'http\Url::STRIP_PATH' => 'Do not include the URL path.', +'http\Url::STRIP_PORT' => 'Do not include the port.', +'http\Url::STRIP_QUERY' => 'Do not include the URL querystring.', +'http\Url::STRIP_USER' => 'Strip the user information from the URL.', +'IBASE_BKP_CONVERT' => 'Options to `ibase_backup`', +'IBASE_BKP_IGNORE_CHECKSUMS' => 'Options to `ibase_backup`', +'IBASE_BKP_IGNORE_LIMBO' => 'Options to `ibase_backup`', +'IBASE_BKP_METADATA_ONLY' => 'Options to `ibase_backup`', +'IBASE_BKP_NO_GARBAGE_COLLECT' => 'Options to `ibase_backup`', +'IBASE_BKP_NON_TRANSPORTABLE' => 'Options to `ibase_backup`', +'IBASE_BKP_OLD_DESCRIPTIONS' => 'Options to `ibase_backup`', +'IBASE_RES_CREATE' => 'Options to `ibase_restore`', +'IBASE_RES_DEACTIVATE_IDX' => 'Options to `ibase_restore`', +'IBASE_RES_NO_SHADOW' => 'Options to `ibase_restore`', +'IBASE_RES_NO_VALIDITY' => 'Options to `ibase_restore`', +'IBASE_RES_ONE_AT_A_TIME' => 'Options to `ibase_restore`', +'IBASE_RES_USE_ALL_SPACE' => 'Options to `ibase_restore`', +'IBASE_RPR_SWEEP_DB' => 'Options to `ibase_maintain_db`', +'IBASE_STS_SYS_RELATIONS' => 'Options to `ibase_db_info`', +'IBASE_SVC_GET_ENV' => 'Options to `ibase_server_info`', +'IBASE_SVC_GET_USERS' => 'Options to `ibase_server_info`', +'IBASE_SVC_IMPLEMENTATION' => 'Options to `ibase_server_info`', +'IBASE_SVC_SERVER_VERSION' => 'Options to `ibase_server_info`', +'ID3_BEST' => '`ID3_BEST` is used if would like to let the id3 functions determine which tag version should be used.', +'ID3_V1_0' => '`ID3_V1_0` is used if you are working with ID3 V1.0 tags. These tags may contain the fields title, artist, album, genre, year and comment.', +'ID3_V1_1' => '`ID3_V1_1` is used if you are working with ID3 V1.1 tags. These tags may all information contained in v1.0 tags plus the track number.', +'ID3_V2_1' => '`ID3_V2_1` is used if you are working with ID3 V2.1 tags.', +'ID3_V2_2' => '`ID3_V2_2` is used if you are working with ID3 V2.2 tags.', +'ID3_V2_3' => '`ID3_V2_3` is used if you are working with ID3 V2.3 tags.', +'ID3_V2_4' => '`ID3_V2_4` is used if you are working with ID3 V2.4 tags.', +'IDNA_ALLOW_UNASSIGNED' => 'Allow processing of unassigned codepoints in the input for IDN functions.', +'IDNA_CHECK_BIDI' => 'Check whether the input conforms to the BiDi rules. Ignored by the IDNA2003 implementation, which always performs this check.', +'IDNA_CHECK_CONTEXTJ' => 'Check whether the input conforms to the CONTEXTJ rules. Ignored by the IDNA2003 implementation, as this check is new in IDNA2008.', +'IDNA_DEFAULT' => 'Prohibit processing of unassigned codepoints in the input for IDN functions and do not check if the input conforms to domain name ASCII rules.', +'IDNA_ERROR_EMPTY_LABEL' => 'Errors reported in a bitset returned by the UTS #46 algorithm in `idn_to_utf8` and `idn_to_ascii`.', +'IDNA_NONTRANSITIONAL_TO_ASCII' => 'Option for nontransitional processing in `idn_to_ascii`. Transitional processing is activated by default. This option is ignored by the IDNA2003 implementation.', +'IDNA_NONTRANSITIONAL_TO_UNICODE' => 'Option for nontransitional processing in `idn_to_utf8`. Transitional processing is activated by default. This option is ignored by the IDNA2003 implementation.', +'IDNA_USE_STD3_RULES' => 'Check if the input for IDN functions conforms to domain name ASCII rules.', +'ILL_BADSTK' => 'Available since PHP 5.3.0.', +'ILL_COPROC' => 'Available since PHP 5.3.0.', +'ILL_ILLADR' => 'Available since PHP 5.3.0.', +'ILL_ILLOPC' => 'Available since PHP 5.3.0.', +'ILL_ILLOPN' => 'Available since PHP 5.3.0.', +'ILL_ILLTRP' => 'Available since PHP 5.3.0.', +'ILL_PRVOPC' => 'Available since PHP 5.3.0.', +'ILL_PRVREG' => 'Available since PHP 5.3.0.', +'IMAGETYPE_ICO' => '(Available as of PHP 5.3.0)', +'IMAGETYPE_WEBP' => '(Available as of PHP 7.1.0)', +'imagick::ALPHACHANNEL_ACTIVATE' => '`imagick.constant.available` 6.3.8 or higher.', +'imagick::ALPHACHANNEL_COPY' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::ALPHACHANNEL_DEACTIVATE' => '`imagick.constant.available` 6.3.8 or higher.', +'imagick::ALPHACHANNEL_EXTRACT' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::ALPHACHANNEL_OPAQUE' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::ALPHACHANNEL_RESET' => '`imagick.constant.available` 6.3.8 or higher.', +'imagick::ALPHACHANNEL_SET' => '`imagick.constant.available` 6.3.8 or higher.', +'imagick::ALPHACHANNEL_SHAPE' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::ALPHACHANNEL_TRANSPARENT' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::ALPHACHANNEL_UNDEFINED' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::COLOR_ALPHA' => 'Color\'s alpha', +'imagick::COLOR_BLACK' => 'Black color', +'imagick::COLOR_BLUE' => 'Blue color', +'imagick::COLOR_CYAN' => 'Cyan color', +'imagick::COLOR_FUZZ' => 'Color\'s fuzz', +'imagick::COLOR_GREEN' => 'Green color', +'imagick::COLOR_MAGENTA' => 'Magenta color', +'imagick::COLOR_OPACITY' => 'Color\'s opacity', +'imagick::COLOR_RED' => 'Red color', +'imagick::COLOR_YELLOW' => 'Yellow color', +'imagick::COLORSPACE_CMY' => '`imagick.constant.available` 6.4.2 or higher.', +'imagick::COMPOSITE_ADD' => 'The result of image + image', +'imagick::COMPOSITE_ATOP' => 'The result is the same shape as image, with composite image obscuring image where the image shapes overlap', +'imagick::COMPOSITE_BLEND' => 'Blends the image', +'imagick::COMPOSITE_BUMPMAP' => 'The same as COMPOSITE_MULTIPLY, except the source is converted to grayscale first.', +'imagick::COMPOSITE_CLEAR' => 'Makes the target image transparent', +'imagick::COMPOSITE_COLORBURN' => 'Darkens the destination image to reflect the source image', +'imagick::COMPOSITE_COLORDODGE' => 'Brightens the destination image to reflect the source image', +'imagick::COMPOSITE_COLORIZE' => 'Colorizes the target image using the composite image', +'imagick::COMPOSITE_COPY' => 'Copies the source image on the target image', +'imagick::COMPOSITE_COPYBLACK' => 'Copies black from the source to target', +'imagick::COMPOSITE_COPYBLUE' => 'Copies blue from the source to target', +'imagick::COMPOSITE_COPYCYAN' => 'Copies cyan from the source to target', +'imagick::COMPOSITE_COPYGREEN' => 'Copies green from the source to target', +'imagick::COMPOSITE_COPYMAGENTA' => 'Copies magenta from the source to target', +'imagick::COMPOSITE_COPYOPACITY' => 'Copies opacity from the source to target', +'imagick::COMPOSITE_COPYRED' => 'Copies red from the source to target', +'imagick::COMPOSITE_COPYYELLOW' => 'Copies yellow from the source to target', +'imagick::COMPOSITE_DARKEN' => 'Darkens the target image', +'imagick::COMPOSITE_DEFAULT' => 'The default composite operator', +'imagick::COMPOSITE_DIFFERENCE' => 'Subtracts the darker of the two constituent colors from the lighter', +'imagick::COMPOSITE_DISPLACE' => 'Shifts target image pixels as defined by the source', +'imagick::COMPOSITE_DISSOLVE' => 'Dissolves the source in to the target', +'imagick::COMPOSITE_DST' => 'The target is left untouched', +'imagick::COMPOSITE_DSTATOP' => 'The part of the destination lying inside of the source is composited over the source and replaces the destination', +'imagick::COMPOSITE_DSTIN' => 'The parts inside the source replace the target', +'imagick::COMPOSITE_DSTOUT' => 'The parts outside the source replace the target', +'imagick::COMPOSITE_DSTOVER' => 'Target replaces the source', +'imagick::COMPOSITE_EXCLUSION' => 'Produces an effect similar to that of imagick::COMPOSITE_DIFFERENCE, but appears as lower contrast', +'imagick::COMPOSITE_HARDLIGHT' => 'Multiplies or screens the colors, dependent on the source color value', +'imagick::COMPOSITE_HUE' => 'Modifies the hue of the target as defined by source', +'imagick::COMPOSITE_IN' => 'Composites source into the target', +'imagick::COMPOSITE_LIGHTEN' => 'Lightens the target as defined by source', +'imagick::COMPOSITE_LUMINIZE' => 'Luminizes the target as defined by source', +'imagick::COMPOSITE_MINUS' => 'Subtracts the source from the target', +'imagick::COMPOSITE_MODULATE' => 'Modulates the target brightness, saturation and hue as defined by source', +'imagick::COMPOSITE_MULTIPLY' => 'Multiplies the target to the source', +'imagick::COMPOSITE_NO' => 'No composite operator defined', +'imagick::COMPOSITE_OUT' => 'Composites outer parts of the source on the target', +'imagick::COMPOSITE_OVER' => 'Composites source over the target', +'imagick::COMPOSITE_OVERLAY' => 'Overlays the source on the target', +'imagick::COMPOSITE_PLUS' => 'Adds the source to the target', +'imagick::COMPOSITE_REPLACE' => 'Replaces the target with the source', +'imagick::COMPOSITE_SATURATE' => 'Saturates the target as defined by the source', +'imagick::COMPOSITE_SCREEN' => 'The source and destination are complemented and then multiplied and then replace the destination', +'imagick::COMPOSITE_SOFTLIGHT' => 'Darkens or lightens the colors, dependent on the source', +'imagick::COMPOSITE_SRC' => 'The source is copied to the destination', +'imagick::COMPOSITE_SRCATOP' => 'The part of the source lying inside of the destination is composited onto the destination', +'imagick::COMPOSITE_SRCIN' => 'The part of the source lying inside of the destination replaces the destination', +'imagick::COMPOSITE_SRCOUT' => 'The part of the source lying outside of the destination replaces the destination', +'imagick::COMPOSITE_SRCOVER' => 'The source replaces the destination', +'imagick::COMPOSITE_SUBTRACT' => 'Subtract the colors in the source image from the destination image', +'imagick::COMPOSITE_THRESHOLD' => 'The source is composited on the target as defined by source threshold', +'imagick::COMPOSITE_UNDEFINED' => 'Undefined composite operator', +'imagick::COMPOSITE_XOR' => 'The part of the source that lies outside of the destination is combined with the part of the destination that lies outside of the source', +'imagick::COMPRESSION_DXT1' => '`imagick.constant.available` 6.4.0 or higher.', +'imagick::COMPRESSION_DXT3' => '`imagick.constant.available` 6.4.0 or higher.', +'imagick::COMPRESSION_DXT5' => '`imagick.constant.available` 6.4.0 or higher.', +'imagick::DISPOSE_BACKGROUND' => 'Dispose background', +'imagick::DISPOSE_NONE' => 'No dispose type defined', +'imagick::DISPOSE_PREVIOUS' => 'Dispose previous', +'imagick::DISPOSE_UNDEFINED' => 'Undefined dispose type', +'imagick::DISPOSE_UNRECOGNIZED' => 'Unrecognized dispose type', +'imagick::DISTORTION_AFFINE' => '`imagick.constant.available` 6.3.6 or higher.', +'imagick::DISTORTION_AFFINEPROJECTION' => '`imagick.constant.available` 6.3.6 or higher.', +'imagick::DISTORTION_ARC' => '`imagick.constant.available` 6.3.6 or higher.', +'imagick::DISTORTION_BARREL' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DISTORTION_BARRELINVERSE' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DISTORTION_BILINEAR' => '`imagick.constant.available` 6.3.6 or higher.', +'imagick::DISTORTION_DEPOLAR' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DISTORTION_PERSPECTIVE' => '`imagick.constant.available` 6.3.6 or higher.', +'imagick::DISTORTION_PERSPECTIVEPROJECTION' => '`imagick.constant.available` 6.3.6 or higher.', +'imagick::DISTORTION_POLAR' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DISTORTION_POLYNOMIAL' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DISTORTION_SCALEROTATETRANSLATE' => '`imagick.constant.available` 6.3.6 or higher.', +'imagick::DISTORTION_SENTINEL' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DISTORTION_SHEPARDS' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DISTORTION_UNDEFINED' => '`imagick.constant.available` 6.3.6 or higher.', +'imagick::DITHERMETHOD_FLOYDSTEINBERG' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DITHERMETHOD_NO' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DITHERMETHOD_RIEMERSMA' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::DITHERMETHOD_UNDEFINED' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::EVALUATE_ADDMODULUS' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_COSINE' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_GAUSSIANNOISE' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_IMPULSENOISE' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_LAPLACIANNOISE' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_LOG' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_MULTIPLICATIVENOISE' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_POISSONNOISE' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_POW' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_SINE' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_THRESHOLD' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_THRESHOLDBLACK' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_THRESHOLDWHITE' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::EVALUATE_UNIFORMNOISE' => '`imagick.constant.available` 6.4.4 or higher.', +'imagick::FUNCTION_POLYNOMIAL' => '`imagick.constant.available` 6.4.9 or higher.', +'imagick::FUNCTION_SINUSOID' => '`imagick.constant.available` 6.4.9 or higher.', +'imagick::FUNCTION_UNDEFINED' => '`imagick.constant.available` 6.4.9 or higher.', +'imagick::INTERLACE_GIF' => '`imagick.constant.available` 6.3.4 or higher.', +'imagick::INTERPOLATE_AVERAGE' => '`imagick.constant.available` 6.3.2 or higher.', +'imagick::INTERPOLATE_BICUBIC' => '`imagick.constant.available` 6.3.2 or higher.', +'imagick::INTERPOLATE_BILINEAR' => '`imagick.constant.available` 6.3.2 or higher.', +'imagick::INTERPOLATE_FILTER' => '`imagick.constant.available` 6.3.2 or higher.', +'imagick::INTERPOLATE_INTEGER' => '`imagick.constant.available` 6.3.2 or higher.', +'imagick::INTERPOLATE_MESH' => '`imagick.constant.available` 6.3.2 or higher.', +'imagick::INTERPOLATE_NEARESTNEIGHBOR' => '`imagick.constant.available` 6.3.2 or higher.', +'imagick::INTERPOLATE_SPLINE' => '`imagick.constant.available` 6.3.4 or higher.', +'imagick::INTERPOLATE_UNDEFINED' => '`imagick.constant.available` 6.3.2 or higher.', +'imagick::LAYERMETHOD_COALESCE' => '`imagick.constant.available` 6.2.9 or higher.', +'imagick::LAYERMETHOD_COMPAREANY' => '`imagick.constant.available` 6.2.9 or higher.', +'imagick::LAYERMETHOD_COMPARECLEAR' => '`imagick.constant.available` 6.2.9 or higher.', +'imagick::LAYERMETHOD_COMPAREOVERLAY' => '`imagick.constant.available` 6.2.9 or higher.', +'imagick::LAYERMETHOD_COMPOSITE' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::LAYERMETHOD_DISPOSE' => '`imagick.constant.available` 6.2.9 or higher.', +'imagick::LAYERMETHOD_FLATTEN' => '`imagick.constant.available` 6.3.7 or higher.', +'imagick::LAYERMETHOD_MERGE' => '`imagick.constant.available` 6.3.7 or higher.', +'imagick::LAYERMETHOD_MOSAIC' => '`imagick.constant.available` 6.3.7 or higher.', +'imagick::LAYERMETHOD_OPTIMIZE' => '`imagick.constant.available` 6.2.9 or higher.', +'imagick::LAYERMETHOD_OPTIMIZEIMAGE' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::LAYERMETHOD_OPTIMIZEPLUS' => '`imagick.constant.available` 6.2.9 or higher.', +'imagick::LAYERMETHOD_OPTIMIZETRANS' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::LAYERMETHOD_REMOVEDUPS' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::LAYERMETHOD_REMOVEZERO' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::LAYERMETHOD_UNDEFINED' => '`imagick.constant.available` 6.2.9 or higher.', +'imagick::NOISE_RANDOM' => '`imagick.constant.available` 6.3.6 or higher.', +'imagick::ORIENTATION_BOTTOMLEFT' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::ORIENTATION_BOTTOMRIGHT' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::ORIENTATION_LEFTBOTTOM' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::ORIENTATION_LEFTTOP' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::ORIENTATION_RIGHTBOTTOM' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::ORIENTATION_RIGHTTOP' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::ORIENTATION_TOPLEFT' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::ORIENTATION_TOPRIGHT' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::ORIENTATION_UNDEFINED' => '`imagick.constant.available` 6.3.0 or higher.', +'imagick::PIXEL_INTEGER' => 'Only available for ImageMagick < 7.', +'imagick::RESOURCETYPE_AREA' => 'Set the maximum width * height of an image that can reside in the pixel cache memory.', +'imagick::RESOURCETYPE_DISK' => 'Set maximum amount of disk space in bytes permitted for use by the pixel cache.', +'imagick::RESOURCETYPE_FILE' => 'Set maximum number of open pixel cache files.', +'imagick::RESOURCETYPE_MAP' => 'Set maximum amount of memory map in bytes to allocate for the pixel cache.', +'imagick::RESOURCETYPE_MEMORY' => 'Set maximum amount of memory in bytes to allocate for the pixel cache from the heap.', +'imagick::RESOURCETYPE_THREAD' => 'Set maximum parallel threads. `imagick.constant.available` 6.7.8 or higher.', +'imagick::SPARSECOLORMETHOD_BARYCENTRIC' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::SPARSECOLORMETHOD_BILINEAR' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::SPARSECOLORMETHOD_POLYNOMIAL' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::SPARSECOLORMETHOD_SPEPARDS' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::SPARSECOLORMETHOD_UNDEFINED' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::SPARSECOLORMETHOD_VORONOI' => '`imagick.constant.available` 6.4.6 or higher.', +'imagick::VIRTUALPIXELMETHOD_BLACK' => '`imagick.constant.available` 6.4.2 or higher.', +'imagick::VIRTUALPIXELMETHOD_GRAY' => '`imagick.constant.available` 6.4.2 or higher.', +'imagick::VIRTUALPIXELMETHOD_HORIZONTALTILE' => '`imagick.constant.available` 6.4.3 or higher.', +'imagick::VIRTUALPIXELMETHOD_MASK' => '`imagick.constant.available` 6.4.2 or higher.', +'imagick::VIRTUALPIXELMETHOD_VERTICALTILE' => '`imagick.constant.available` 6.4.3 or higher.', +'imagick::VIRTUALPIXELMETHOD_WHITE' => '`imagick.constant.available` 6.4.2 or higher.', +'IMAP_GC_ELT' => 'Garbage collector, clear message cache elements.', +'IMAP_GC_ENV' => 'Garbage collector, clear envelopes and bodies.', +'IMAP_GC_TEXTS' => 'Garbage collector, clear texts.', +'IMG_ARC_ROUNDED' => 'This constant has the same value as `IMG_ARC_PIE`', +'IMG_FILTER_PIXELATE' => '(Available as of PHP 5.3.0)', +'IMG_FILTER_SCATTER' => '(Available as of PHP 7.4.0)', +'IMG_JPEG' => 'This constant has the same value as `IMG_JPG`', +'IMG_WEBP' => 'Available as of PHP 5.6.25 and PHP 7.0.10, respectively.', +'IN_ACCESS' => 'File was accessed (read) (*)', +'IN_ALL_EVENTS' => 'Bitmask of all the above constants', +'IN_ATTRIB' => 'Metadata changed (e.g. permissions, mtime, etc.) (*)', +'IN_CLOSE' => 'Equals to IN_CLOSE_WRITE | IN_CLOSE_NOWRITE', +'IN_CLOSE_NOWRITE' => 'File not opened for writing was closed (*)', +'IN_CLOSE_WRITE' => 'File opened for writing was closed (*)', +'IN_CREATE' => 'File or directory created in watched directory (*)', +'IN_DELETE' => 'File or directory deleted in watched directory (*)', +'IN_DELETE_SELF' => 'Watched file or directory was deleted', +'IN_DONT_FOLLOW' => 'Do not dereference pathname if it is a symlink (Since Linux 2.6.15)', +'IN_IGNORED' => 'Watch was removed (explicitly by `inotify_rm_watch` or because file was removed or filesystem unmounted', +'IN_ISDIR' => 'Subject of this event is a directory', +'IN_MASK_ADD' => 'Add events to watch mask for this pathname if it already exists (instead of replacing mask).', +'IN_MODIFY' => 'File was modified (*)', +'IN_MOVE' => 'Equals to IN_MOVED_FROM | IN_MOVED_TO', +'IN_MOVE_SELF' => 'Watch file or directory was moved', +'IN_MOVED_FROM' => 'File moved out of watched directory (*)', +'IN_MOVED_TO' => 'File moved into watched directory (*)', +'IN_ONESHOT' => 'Monitor pathname for one event, then remove from watch list.', +'IN_ONLYDIR' => 'Only watch pathname if it is a directory (Since Linux 2.6.15)', +'IN_OPEN' => 'File was opened (*)', +'IN_Q_OVERFLOW' => 'Event queue overflowed (wd is -1 for this event)', +'IN_UNMOUNT' => 'File system containing watched object was unmounted', +'INGRES_API_VERSION' => 'Specifies the version of Ingres OpenAPI that the extension was built against. Available since version 1.2.0 of the PECL extension.', +'INGRES_ASSOC' => 'Columns are returned into the array having the fieldname as the array index. Used with `ingres_fetch_array`.', +'INGRES_BOTH' => 'Columns are returned into the array having both a numerical index and the fieldname as the array index. Used with `ingres_fetch_array`.', +'INGRES_CURSOR_READONLY' => 'Specifies that Ingres cursors should be opened in "readonly" mode. Available since version 1.2.0 of the PECL extension. Used with ingres.cursor_mode.', +'INGRES_CURSOR_UPDATE' => 'Specifies that Ingres cursors should be opened "for update." Available since version 1.2.0 of the PECL extension. Used with ingres.cursor_mode.', +'INGRES_DATE_DMY' => 'Equivalent to the II_DATE_FORMAT setting of DMY. Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_DATE_FINNISH' => 'Equivalent to the II_DATE_FORMAT setting of FINNISH. Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_DATE_GERMAN' => 'Equivalent to the II_DATE_FORMAT setting of GERMAN. Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_DATE_ISO' => 'Equivalent to the II_DATE_FORMAT setting of ISO. Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_DATE_ISO4' => 'Equivalent to the II_DATE_FORMAT setting of ISO4. Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_DATE_MDY' => 'Equivalent to the II_DATE_FORMAT setting of MDY. Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_DATE_MULTINATIONAL' => 'Equivalent to the II_DATE_FORMAT setting of MULTINATIONAL. Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_DATE_MULTINATIONAL4' => 'Equivalent to the II_DATE_FORMAT setting of MULTINATIONAL4. Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_DATE_YMD' => 'Equivalent to the II_DATE_FORMAT setting of YMD. Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_EXT_VERSION' => 'Specifies the version of the Ingres Extension. Available since version 1.2.0 of the PECL extension.', +'INGRES_MONEY_LEADING' => 'Specifies the currency character that should be placed at the start of a money value. Equivalent to setting II_MONEY_FORMAT to "L:". Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_MONEY_TRAILING' => 'Specifies the currency character that should be placed at the end of a money value. Equivalent to setting II_MONEY_FORMAT to "T:". Available since version 1.2.0 of the PECL extension. Used with `ingres_connect`, `ingres_pconnect` and `ingres_set_environment`. See options in `ingres_set_environment`.', +'INGRES_NUM' => 'Columns are returned into the array having a numerical index to the fields. By default this index starts at 1, the first field in the result. To change this value, see ingres.array_index_start. Used with `ingres_fetch_array`.', +'INGRES_STRUCTURE_BTREE' => 'Specifies the default table or index structure to BTREE when used in combination with the options or index_structure option when connecting. Available since version 1.4.0 of the PECL extension. Used with `ingres_connect` and `ingres_pconnect`. See options in `ingres_connect`.', +'INGRES_STRUCTURE_CBTREE' => 'Specifies the default table or index structure to COMPRESSED BTREE when used in combination with the options or index_structure option when connecting. Available since version 1.4.0 of the PECL extension. Used with `ingres_connect` and `ingres_pconnect`. See options in `ingres_connect`.', +'INGRES_STRUCTURE_CHASH' => 'Specifies the default table or index structure to COMPRESSED HASH when used in combination with the options or index_structure option when connecting. Available since version 1.4.0 of the PECL extension. Used with `ingres_connect` and `ingres_pconnect`. See options in `ingres_connect`.', +'INGRES_STRUCTURE_CHEAP' => 'Specifies the default table structure to COMPRESSED HEAP when used in combination with the options option when connecting. Available since version 1.4.0 of the PECL extension. Used with `ingres_connect` and `ingres_pconnect`. See options in `ingres_connect`.', +'INGRES_STRUCTURE_CISAM' => 'Specifies the default table or index structure to COMPRESSED ISAM when used in combination with the options or index_structure option when connecting. Available since version 1.4.0 of the PECL extension. Used with `ingres_connect` and `ingres_pconnect`. See options in `ingres_connect`.', +'INGRES_STRUCTURE_HASH' => 'Specifies the default table or index structure to HASH when used in combination with the options or index_structure option when connecting. Available since version 1.4.0 of the PECL extension. Used with `ingres_connect` and `ingres_pconnect`. See options in `ingres_connect`.', +'INGRES_STRUCTURE_HEAP' => 'Specifies the default table structure to HEAP when used in combination with the options option when connecting. Available since version 1.4.0 of the PECL extension. Used with `ingres_connect` and `ingres_pconnect`. See options in `ingres_connect`.', +'INGRES_STRUCTURE_ISAM' => 'Specifies the default table or index structure to ISAM when used in combination with the options or index_structure option when connecting. Available since version 1.4.0 of the PECL extension. Used with `ingres_connect` and `ingres_pconnect`. See options in `ingres_connect`.', +'INI_SCANNER_NORMAL' => 'Normal INI scanner mode (since PHP 5.3).', +'INI_SCANNER_RAW' => 'Raw INI scanner mode (since PHP 5.3).', +'INI_SCANNER_TYPED' => 'Typed INI scanner mode (since PHP 5.6.1).', +'INPUT_COOKIE' => 'COOKIE variables.', +'INPUT_ENV' => 'ENV variables.', +'INPUT_GET' => 'GET variables.', +'INPUT_POST' => 'POST variables.', +'INPUT_REQUEST' => 'REQUEST variables. (not implemented yet)', +'INPUT_SERVER' => 'SERVER variables.', +'INPUT_SESSION' => 'SESSION variables. (not implemented yet)', +'INTL_IDNA_VARIANT_2003' => 'Use IDNA 2003 algorithm in `idn_to_utf8` and `idn_to_ascii`. This is the default. This constant and using the default has been deprecated as of PHP 7.2.0.', +'INTL_IDNA_VARIANT_UTS46' => 'Use UTS #46 algorithm in `idn_to_utf8` and `idn_to_ascii`. Available as of ICU 4.6.', +'INTL_MAX_LOCALE_LEN' => 'Limit on locale length, set to 80 in PHP code. Locale names longer than this limit will not be accepted.', +'JSON_BIGINT_AS_STRING' => 'Decodes large integers as their original string value. Available since PHP 5.4.0.', +'JSON_ERROR_CTRL_CHAR' => 'Control character error, possibly incorrectly encoded.', +'JSON_ERROR_DEPTH' => 'The maximum stack depth has been exceeded.', +'JSON_ERROR_INF_OR_NAN' => 'The value passed to `json_encode` includes either `NAN` or `INF`. If the `JSON_PARTIAL_OUTPUT_ON_ERROR` option was given, `0` will be encoded in the place of these special numbers. Available since PHP 5.5.0.', +'JSON_ERROR_INVALID_PROPERTY_NAME' => 'A key starting with \u0000 character was in the string passed to `json_decode` when decoding a JSON object into a PHP object. Available since PHP 7.0.0.', +'JSON_ERROR_NONE' => 'No error has occurred.', +'JSON_ERROR_RECURSION' => 'The object or array passed to `json_encode` include recursive references and cannot be encoded. If the `JSON_PARTIAL_OUTPUT_ON_ERROR` option was given, `null` will be encoded in the place of the recursive reference. Available since PHP 5.5.0.', +'JSON_ERROR_STATE_MISMATCH' => 'Occurs with underflow or with the modes mismatch.', +'JSON_ERROR_SYNTAX' => 'Syntax error.', +'JSON_ERROR_UNSUPPORTED_TYPE' => 'A value of an unsupported type was given to `json_encode`, such as a `resource`. If the `JSON_PARTIAL_OUTPUT_ON_ERROR` option was given, `null` will be encoded in the place of the unsupported value. Available since PHP 5.5.0.', +'JSON_ERROR_UTF16' => 'Single unpaired UTF-16 surrogate in unicode escape contained in the JSON string passed to `json_encode`. Available since PHP 7.0.0.', +'JSON_ERROR_UTF8' => 'Malformed UTF-8 characters, possibly incorrectly encoded.', +'JSON_FORCE_OBJECT' => 'Outputs an object rather than an array when a non-associative array is used. Especially useful when the recipient of the output is expecting an object and the array is empty.', +'JSON_HEX_AMP' => 'All &s are converted to \u0026.', +'JSON_HEX_APOS' => 'All \' are converted to \u0027.', +'JSON_HEX_QUOT' => 'All " are converted to \u0022.', +'JSON_HEX_TAG' => 'All < and > are converted to \u003C and \u003E.', +'JSON_INVALID_UTF8_IGNORE' => 'Ignore invalid UTF-8 characters. Available as of PHP 7.2.0.', +'JSON_INVALID_UTF8_SUBSTITUTE' => 'Convert invalid UTF-8 characters to \0xfffd (Unicode Character \'REPLACEMENT CHARACTER\') Available as of PHP 7.2.0.', +'JSON_NUMERIC_CHECK' => 'Encodes numeric strings as numbers.', +'JSON_OBJECT_AS_ARRAY' => 'Decodes JSON objects as PHP array. This option can be added automatically by calling `json_decode` with the second parameter equal to `true`. Available since PHP 5.4.0.', +'JSON_PARTIAL_OUTPUT_ON_ERROR' => 'Substitute some unencodable values instead of failing. Available since PHP 5.5.0.', +'JSON_PRESERVE_ZERO_FRACTION' => 'Ensures that `float` values are always encoded as a float value. Available since PHP 5.6.6.', +'JSON_PRETTY_PRINT' => 'Use whitespace in returned data to format it. Available since PHP 5.4.0.', +'JSON_THROW_ON_ERROR' => 'Throws `JsonException` if an error occurs instead of setting the global error state that is retrieved with `json_last_error`. `JSON_PARTIAL_OUTPUT_ON_ERROR` takes precedence over `JSON_THROW_ON_ERROR`. Available since PHP 7.3.0.', +'JSON_UNESCAPED_LINE_TERMINATORS' => 'The line terminators are kept unescaped when `JSON_UNESCAPED_UNICODE` is supplied. It uses the same behaviour as it was before PHP 7.1 without this constant. Available since PHP 7.1.0.', +'JSON_UNESCAPED_SLASHES' => 'Don\'t escape `/`. Available since PHP 5.4.0.', +'JSON_UNESCAPED_UNICODE' => 'Encode multibyte Unicode characters literally (default is to escape as \uXXXX). Available since PHP 5.4.0.', +'Judy::BITSET' => 'Define the Judy Array as a Bitset with keys as Integer and Values as a Boolean.', +'Judy::INT_TO_INT' => 'Define the Judy Array with key/values as Integer, and Integer only.', +'Judy::INT_TO_MIXED' => 'Define the Judy Array with keys as Integer and Values of any type.', +'Judy::STRING_TO_INT' => 'Define the Judy Array with keys as a String and Values as Integer, and Integer only.', +'Judy::STRING_TO_MIXED' => 'Define the Judy Array with keys as a String and Values of any type.', +'KTaglib_ID3v2_AttachedPictureFrame::Artist' => 'Picture type Artist', +'KTaglib_ID3v2_AttachedPictureFrame::BackCover' => 'Picture type BackCover', +'KTaglib_ID3v2_AttachedPictureFrame::Band' => 'Picture type Band', +'KTaglib_ID3v2_AttachedPictureFrame::BandLogo' => 'Picture type BandLogo', +'KTaglib_ID3v2_AttachedPictureFrame::ColouredFish' => 'Picture type ColouredFish', +'KTaglib_ID3v2_AttachedPictureFrame::Composer' => 'Picture type Composer', +'KTaglib_ID3v2_AttachedPictureFrame::Conductor' => 'Picture type Conductor', +'KTaglib_ID3v2_AttachedPictureFrame::DuringPerformance' => 'Picture type DuringPerformance', +'KTaglib_ID3v2_AttachedPictureFrame::DuringRecording' => 'Picture type DuringRecording', +'KTaglib_ID3v2_AttachedPictureFrame::FileIcon' => 'Picture type FileIcon', +'KTaglib_ID3v2_AttachedPictureFrame::FrontCover' => 'Picture type FrontCover', +'KTaglib_ID3v2_AttachedPictureFrame::Illustration' => 'Picture type Illustration', +'KTaglib_ID3v2_AttachedPictureFrame::LeadArtist' => 'Picture type LeadArtist', +'KTaglib_ID3v2_AttachedPictureFrame::LeafletPage' => 'Picture type LeafletPage', +'KTaglib_ID3v2_AttachedPictureFrame::Lyricist' => 'Picture type Lyricist', +'KTaglib_ID3v2_AttachedPictureFrame::Media' => 'Picture type Media', +'KTaglib_ID3v2_AttachedPictureFrame::MovieScreenCapture' => 'Picture type MovieScreenCapture', +'KTaglib_ID3v2_AttachedPictureFrame::Other' => 'Picture type Other', +'KTaglib_ID3v2_AttachedPictureFrame::OtherFileIcon' => 'Picture type OtherFileIcon', +'KTaglib_ID3v2_AttachedPictureFrame::RecordingLocation' => 'Picture type RecordingLocation', +'KTaglib_MPEG_Header::Version1' => 'ID3 version is 1.0', +'KTaglib_MPEG_Header::Version2' => 'ID3 version is 2.0', +'KTaglib_MPEG_Header::Version2_5' => 'ID3 version is 2.5', +'LATT_HASCHILDREN' => 'This mailbox has selectable inferiors.', +'LATT_HASNOCHILDREN' => 'This mailbox has no selectable inferiors.', +'LATT_MARKED' => 'This mailbox is marked. Only used by UW-IMAPD.', +'LATT_NOINFERIORS' => 'This mailbox has no "children" (there are no mailboxes below this one).', +'LATT_NOSELECT' => 'This is only a container, not a mailbox - you cannot open it.', +'LATT_REFERRAL' => 'This container has a referral to a remote mailbox.', +'LATT_UNMARKED' => 'This mailbox is not marked. Only used by UW-IMAPD.', +'LDAP_CONTROL_ASSERT' => 'Control Constant - Assertion (RFC 4528). Available as of PHP 7.3.0.', +'LDAP_CONTROL_AUTHZID_REQUEST' => 'Control Constant - Authorization Identity Request (RFC 3829). Available as of PHP 7.3.0.', +'LDAP_CONTROL_AUTHZID_RESPONSE' => 'Control Constant - Authorization Identity Response (RFC 3829). Available as of PHP 7.3.0.', +'LDAP_CONTROL_DONTUSECOPY' => 'Control Constant - Don\'t Use Copy (RFC 6171). Available as of PHP 7.3.0.', +'LDAP_CONTROL_MANAGEDSAIT' => 'Control Constant - Manage DSA IT (RFC 3296). Available as of PHP 7.3.0.', +'LDAP_CONTROL_PAGEDRESULTS' => 'Control Constant - Paged results (RFC 2696). Available as of PHP 7.3.0.', +'LDAP_CONTROL_PASSWORDPOLICYREQUEST' => 'Control Constant - Password Policy Request. Available as of PHP 7.3.0.', +'LDAP_CONTROL_PASSWORDPOLICYRESPONSE' => 'Control Constant - Password Policy Response. Available as of PHP 7.3.0.', +'LDAP_CONTROL_POST_READ' => 'Control Constant - Post read (RFC 4527). Available as of PHP 7.3.0.', +'LDAP_CONTROL_PRE_READ' => 'Control Constant - Pre read (RFC 4527). Available as of PHP 7.3.0.', +'LDAP_CONTROL_PROXY_AUTHZ' => 'Control Constant - Proxied Authorization (RFC 4370). Available as of PHP 7.3.0.', +'LDAP_CONTROL_SORTREQUEST' => 'Control Constant - Sort request (RFC 2891). Available as of PHP 7.3.0.', +'LDAP_CONTROL_SORTRESPONSE' => 'Control Constant - Sort response (RFC 2891). Available as of PHP 7.3.0.', +'LDAP_CONTROL_SUBENTRIES' => 'Control Constant - Subentries (RFC 3672). Available as of PHP 7.3.0.', +'LDAP_CONTROL_SYNC' => 'Control Constant - Content Synchronization Operation (RFC 4533). Available as of PHP 7.3.0.', +'LDAP_CONTROL_SYNC_DONE' => 'Control Constant - Content Synchronization Operation Done (RFC 4533). Available as of PHP 7.3.0.', +'LDAP_CONTROL_SYNC_STATE' => 'Control Constant - Content Synchronization Operation State (RFC 4533). Available as of PHP 7.3.0.', +'LDAP_CONTROL_VALUESRETURNFILTER' => 'Control Constant - Filter returned values (RFC 3876). Available as of PHP 7.3.0.', +'LDAP_CONTROL_VLVREQUEST' => 'Control Constant - Virtual List View Request. Available as of PHP 7.3.0.', +'LDAP_CONTROL_VLVRESPONSE' => 'Control Constant - Virtual List View Response. Available as of PHP 7.3.0.', +'LDAP_CONTROL_X_DOMAIN_SCOPE' => 'Control Constant - Active Directory Domain Scope. Available as of PHP 7.3.0.', +'LDAP_CONTROL_X_EXTENDED_DN' => 'Control Constant - Active Directory Extended DN. Available as of PHP 7.3.0.', +'LDAP_CONTROL_X_INCREMENTAL_VALUES' => 'Control Constant - Active Directory Incremental Values. Available as of PHP 7.3.0.', +'LDAP_CONTROL_X_PERMISSIVE_MODIFY' => 'Control Constant - Active Directory Permissive Modify. Available as of PHP 7.3.0.', +'LDAP_CONTROL_X_SEARCH_OPTIONS' => 'Control Constant - Active Directory Search Options. Available as of PHP 7.3.0.', +'LDAP_CONTROL_X_TREE_DELETE' => 'Control Constant - Active Directory Tree Delete. Available as of PHP 7.3.0.', +'LDAP_DEREF_ALWAYS' => 'Alias dereferencing rule - Always.', +'LDAP_DEREF_FINDING' => 'Alias dereferencing rule - Finding.', +'LDAP_DEREF_NEVER' => 'Alias dereferencing rule - Never.', +'LDAP_DEREF_SEARCHING' => 'Alias dereferencing rule - Searching.', +'LDAP_EXOP_MODIFY_PASSWD' => 'Extended Operation constant - Modify password (RFC 3062).', +'LDAP_EXOP_REFRESH' => 'Extended Operation Constant - Refresh (RFC 2589).', +'LDAP_EXOP_START_TLS' => 'Extended Operation constant - Start TLS (RFC 4511).', +'LDAP_EXOP_TURN' => 'Extended Operation Constant - Turn (RFC 4531).', +'LDAP_EXOP_WHO_AM_I' => 'Extended Operation Constant - WHOAMI (RFC 4532).', +'LDAP_OPT_CLIENT_CONTROLS' => 'Specifies a default list of client controls to be processed with each request.', +'LDAP_OPT_DEBUG_LEVEL' => 'Specifies a bitwise level for debug traces.', +'LDAP_OPT_DEREF' => 'Specifies alternative rules for following aliases at the server.', +'LDAP_OPT_DIAGNOSTIC_MESSAGE' => 'Gets the latest session error message.', +'LDAP_OPT_ERROR_NUMBER' => 'Latest session error number.', +'LDAP_OPT_ERROR_STRING' => 'Alias of `LDAP_OPT_DIAGNOSTIC_MESSAGE`.', +'LDAP_OPT_HOST_NAME' => 'Sets/gets a space-separated of hosts when trying to connect.', +'LDAP_OPT_MATCHED_DN' => 'Sets/gets the matched DN associated with the connection.', +'LDAP_OPT_NETWORK_TIMEOUT' => 'Option for `ldap_set_option` to allow setting network timeout. (Available as of PHP 5.3.0)', +'LDAP_OPT_PROTOCOL_VERSION' => 'Specifies the LDAP protocol to be used (V2 or V3).', +'LDAP_OPT_REFERRALS' => 'Specifies whether to automatically follow referrals returned by the LDAP server.', +'LDAP_OPT_RESTART' => 'Determines whether or not the connection should be implicitly restarted.', +'LDAP_OPT_SERVER_CONTROLS' => 'Specifies a default list of server controls to be sent with each request.', +'LDAP_OPT_SIZELIMIT' => 'Specifies the maximum number of entries that can be returned on a search operation. + +The actual size limit for operations is also bounded by the server\'s configured maximum number of return entries. The lesser of these two settings is the actual size limit.', +'LDAP_OPT_TIMELIMIT' => 'Specifies the number of seconds to wait for search results.', +'LDAP_OPT_X_KEEPALIVE_IDLE' => 'Specifies the number of seconds a connection needs to remain idle before TCP starts sending keepalive probes.', +'LDAP_OPT_X_KEEPALIVE_INTERVAL' => 'Specifies the interval in seconds between individual keepalive probes.', +'LDAP_OPT_X_KEEPALIVE_PROBES' => 'Specifies the maximum number of keepalive probes TCP should send before dropping the connection.', +'LDAP_OPT_X_TLS_CACERTDIR' => 'Specifies the path of the directory containing CA certificates.', +'LDAP_OPT_X_TLS_CACERTFILE' => 'Specifies the full-path of the CA certificate file.', +'LDAP_OPT_X_TLS_CERTFILE' => 'Specifies the full-path of the certificate file.', +'LDAP_OPT_X_TLS_CIPHER_SUITE' => 'Specifies the allowed cipher suite.', +'LDAP_OPT_X_TLS_CRLCHECK' => 'Specifies the CRL evaluation strategy. This must be one of: `LDAP_OPT_X_TLS_CRL_NONE`,`LDAP_OPT_X_TLS_CRL_PEER`, `LDAP_OPT_X_TLS_CRL_ALL`.', +'LDAP_OPT_X_TLS_CRLFILE' => 'Specifies the full-path of the CRL file.', +'LDAP_OPT_X_TLS_DHFILE' => 'Specifies the full-path of the file containing the parameters for Diffie-Hellman ephemeral key exchange.', +'LDAP_OPT_X_TLS_KEYFILE' => 'Specifies the full-path of the certificate key file.', +'LDAP_OPT_X_TLS_PROTOCOL_MIN' => 'Specifies the minimum protocol version. This can be one of: `LDAP_OPT_X_TLS_PROTOCOL_SSL2`,`LDAP_OPT_X_TLS_PROTOCOL_SSL3`, `LDAP_OPT_X_TLS_PROTOCOL_TLS1_0`, `LDAP_OPT_X_TLS_PROTOCOL_TLS1_1`, `LDAP_OPT_X_TLS_PROTOCOL_TLS1_2`', +'LDAP_OPT_X_TLS_RANDOM_FILE' => 'Sets/gets the random file when one of the system default ones are not available.', +'LDAP_OPT_X_TLS_REQUIRE_CERT' => 'Specifies the certificate checking checking strategy. This must be one of: `LDAP_OPT_X_TLS_NEVER`,`LDAP_OPT_X_TLS_HARD`, `LDAP_OPT_X_TLS_DEMAND`, `LDAP_OPT_X_TLS_ALLOW`, `LDAP_OPT_X_TLS_TRY`. (Available as of PHP 7.0.0)', +'LIBEXSLT_DOTTED_VERSION' => 'libexslt version like 1.1.17. Available as of PHP 5.1.2.', +'LIBEXSLT_VERSION' => 'libexslt version like 813. Available as of PHP 5.1.2.', +'LIBXML_BIGLINES' => 'Allows line numbers greater than 65535 to be reported correctly.', +'LIBXML_COMPACT' => 'Activate small nodes allocation optimization. This may speed up your application without needing to change the code.', +'LIBXML_DOTTED_VERSION' => 'libxml version like 2.6.5 or 2.6.17', +'LIBXML_DTDATTR' => 'Default DTD attributes', +'LIBXML_DTDLOAD' => 'Load the external subset', +'LIBXML_DTDVALID' => 'Validate with the DTD', +'LIBXML_ERR_ERROR' => 'A recoverable error', +'LIBXML_ERR_FATAL' => 'A fatal error', +'LIBXML_ERR_NONE' => 'No errors', +'LIBXML_ERR_WARNING' => 'A simple warning', +'LIBXML_HTML_NODEFDTD' => 'Sets HTML_PARSE_NODEFDTD flag, which prevents a default doctype being added when one is not found.', +'LIBXML_HTML_NOIMPLIED' => 'Sets HTML_PARSE_NOIMPLIED flag, which turns off the automatic adding of implied html/body... elements.', +'LIBXML_NOBLANKS' => 'Remove blank nodes', +'LIBXML_NOCDATA' => 'Merge CDATA as text nodes', +'LIBXML_NOEMPTYTAG' => 'Expand empty tags (e.g. `<br/>` to `<br></br>`)', +'LIBXML_NOENT' => 'Substitute entities', +'LIBXML_NOERROR' => 'Suppress error reports', +'LIBXML_NONET' => 'Disable network access when loading documents', +'LIBXML_NOWARNING' => 'Suppress warning reports', +'LIBXML_NOXMLDECL' => 'Drop the XML declaration when saving a document', +'LIBXML_NSCLEAN' => 'Remove redundant namespace declarations', +'LIBXML_PARSEHUGE' => 'Sets XML_PARSE_HUGE flag, which relaxes any hardcoded limit from the parser. This affects limits like maximum depth of a document or the entity recursion, as well as limits of the size of text nodes.', +'LIBXML_PEDANTIC' => 'Sets XML_PARSE_PEDANTIC flag, which enables pedantic error reporting.', +'LIBXML_SCHEMA_CREATE' => 'Create default/fixed value nodes during XSD schema validation', +'LIBXML_VERSION' => 'libxml version like 20605 or 20617', +'LIBXML_XINCLUDE' => 'Implement XInclude substitution', +'LIBXSLT_DOTTED_VERSION' => 'libxslt version like 1.1.17. Available as of PHP 5.1.2.', +'LIBXSLT_VERSION' => 'libxslt version like 10117. Available as of PHP 5.1.2.', +'MB_CASE_FOLD' => 'Available since PHP 7.3.', +'MB_CASE_FOLD_SIMPLE' => 'Used by case-insensitive operations. Available since PHP 7.3.', +'MB_CASE_LOWER' => 'Performs a full lower-case folding. This may change the length of the string. This is the mode used by mb_strtolower().', +'MB_CASE_LOWER_SIMPLE' => 'Available since PHP 7.3.', +'MB_CASE_TITLE' => 'Performs a full title-case conversion based on the Cased and CaseIgnorable derived Unicode properties. In particular this improves handling of quotes and apostrophes. This may change the length of the string.', +'MB_CASE_TITLE_SIMPLE' => 'Available since PHP 7.3.', +'MB_CASE_UPPER' => 'Performs a full upper-case folding. This may change the length of the string. This is the mode used by mb_strtoupper().', +'MB_CASE_UPPER_SIMPLE' => 'Performs simple upper-case fold conversion. This does not change the length of the string. Available as of PHP 7.3.', +'MB_ONIGURUMA_VERSION' => 'The Oniguruma version, e.g. `6.9.4`. Available as of PHP 7.4.', +'Memcached::DISTRIBUTION_CONSISTENT' => '

Consistent hashing key distribution algorithm (based on libketama).

', +'Memcached::DISTRIBUTION_MODULA' => '

Modulo-based key distribution algorithm.

', +'Memcached::GET_EXTENDED' => 'A flag for `Memcached::get`, `Memcached::getMulti` and `Memcached::getMultiByKey` to ensure that the CAS token values are returned as well.', +'Memcached::GET_PRESERVE_ORDER' => '

A flag for Memcached::getMulti and +Memcached::getMultiByKey to ensure that the keys are +returned in the same order as they were requested in. Non-existing keys +get a default value of NULL.

', +'Memcached::HASH_CRC' => '

CRC item key hashing algorithm.

', +'Memcached::HASH_DEFAULT' => '

The default (Jenkins one-at-a-time) item key hashing algorithm.

', +'Memcached::HASH_FNV1_32' => '

FNV1_32 item key hashing algorithm.

', +'Memcached::HASH_FNV1_64' => '

FNV1_64 item key hashing algorithm.

', +'Memcached::HASH_FNV1A_32' => '

FNV1_32A item key hashing algorithm.

', +'Memcached::HASH_FNV1A_64' => '

FNV1_64A item key hashing algorithm.

', +'Memcached::HASH_HSIEH' => '

Hsieh item key hashing algorithm.

', +'Memcached::HASH_MD5' => '

MD5 item key hashing algorithm.

', +'Memcached::HASH_MURMUR' => '

Murmur item key hashing algorithm.

', +'Memcached::HAVE_IGBINARY' => 'Indicates whether igbinary serializer support is available.', +'Memcached::HAVE_JSON' => 'Indicates whether JSON serializer support is available.', +'Memcached::HAVE_MSGPACK' => 'Indicates whether msgpack serializer support is available.', +'Memcached::HAVE_SASL' => 'Whether SASL is available for authentication', +'Memcached::HAVE_SESSION' => 'Whether memcached can be used for storing session data', +'Memcached::OPT_BINARY_PROTOCOL' => '

Enable the use of the binary protocol. Please note that you cannot +toggle this option on an open connection.

+

Type: boolean, default: FALSE.

', +'Memcached::OPT_BUFFER_WRITES' => '

Enables or disables buffered I/O. Enabling buffered I/O causes +storage commands to "buffer" instead of being sent. Any action that +retrieves data causes this buffer to be sent to the remote connection. +Quitting the connection or closing down the connection will also cause +the buffered data to be pushed to the remote connection.

+

Type: boolean, default: FALSE.

', +'Memcached::OPT_CACHE_LOOKUPS' => '

Enables or disables caching of DNS lookups.

+

Type: boolean, default: FALSE.

', +'Memcached::OPT_COMPRESSION' => '

Enables or disables payload compression. When enabled, +item values longer than a certain threshold (currently 100 bytes) will be +compressed during storage and decompressed during retrieval +transparently.

+

Type: boolean, default: TRUE.

', +'Memcached::OPT_CONNECT_TIMEOUT' => '

In non-blocking mode this set the value of the timeout during socket +connection, in milliseconds.

+

Type: integer, default: 1000.

', +'Memcached::OPT_DISTRIBUTION' => '

Specifies the method of distributing item keys to the servers. +Currently supported methods are modulo and consistent hashing. Consistent +hashing delivers better distribution and allows servers to be added to +the cluster with minimal cache losses.

+

Type: integer, default: Memcached::DISTRIBUTION_MODULA.

', +'Memcached::OPT_HASH' => '

Specifies the hashing algorithm used for the item keys. The valid +values are supplied via Memcached::HASH_* constants. +Each hash algorithm has its advantages and its disadvantages. Go with the +default if you don\'t know or don\'t care.

+

Type: integer, default: Memcached::HASH_DEFAULT

', +'Memcached::OPT_LIBKETAMA_COMPATIBLE' => '

Enables or disables compatibility with libketama-like behavior. When +enabled, the item key hashing algorithm is set to MD5 and distribution is +set to be weighted consistent hashing distribution. This is useful +because other libketama-based clients (Python, Ruby, etc.) with the same +server configuration will be able to access the keys transparently. +

+

+It is highly recommended to enable this option if you want to use +consistent hashing, and it may be enabled by default in future +releases. +

+

Type: boolean, default: FALSE.

', +'Memcached::OPT_NO_BLOCK' => '

Enables or disables asynchronous I/O. This is the fastest transport +available for storage functions.

+

Type: boolean, default: FALSE.

', +'Memcached::OPT_POLL_TIMEOUT' => '

Timeout for connection polling, in milliseconds.

+

Type: integer, default: 1000.

', +'Memcached::OPT_PREFIX_KEY' => '

This can be used to create a "domain" for your item keys. The value +specified here will be prefixed to each of the keys. It cannot be +longer than 128 characters and will reduce the +maximum available key size. The prefix is applied only to the item keys, +not to the server keys.

+

Type: string, default: "".

', +'Memcached::OPT_RECV_TIMEOUT' => '

Socket reading timeout, in microseconds. In cases where you cannot +use non-blocking I/O this will allow you to still have timeouts on the +reading of data.

+

Type: integer, default: 0.

', +'Memcached::OPT_RETRY_TIMEOUT' => '

The amount of time, in seconds, to wait until retrying a failed +connection attempt.

+

Type: integer, default: 0.

', +'Memcached::OPT_SEND_TIMEOUT' => '

Socket sending timeout, in microseconds. In cases where you cannot +use non-blocking I/O this will allow you to still have timeouts on the +sending of data.

+

Type: integer, default: 0.

', +'Memcached::OPT_SERIALIZER' => '

+Specifies the serializer to use for serializing non-scalar values. +The valid serializers are Memcached::SERIALIZER_PHP +or Memcached::SERIALIZER_IGBINARY. The latter is +supported only when memcached is configured with +--enable-memcached-igbinary option and the +igbinary extension is loaded. +

+

Type: integer, default: Memcached::SERIALIZER_PHP.

', +'Memcached::OPT_SERVER_FAILURE_LIMIT' => '

Specifies the failure limit for server connection attempts. The +server will be removed after this many continuous connection +failures.

+

Type: integer, default: 0.

', +'Memcached::OPT_SOCKET_RECV_SIZE' => '

The maximum socket receive buffer in bytes.

+

Type: integer, default: varies by platform/kernel +configuration.

', +'Memcached::OPT_SOCKET_SEND_SIZE' => '

The maximum socket send buffer in bytes.

+

Type: integer, default: varies by platform/kernel +configuration.

', +'Memcached::OPT_TCP_NODELAY' => '

Enables or disables the no-delay feature for connecting sockets (may +be faster in some environments).

+

Type: boolean, default: FALSE.

', +'Memcached::RES_AUTH_CONTINUE' => 'Available as of Memcached 3.0.0.', +'Memcached::RES_AUTH_FAILURE' => 'Available as of Memcached 3.0.0.', +'Memcached::RES_AUTH_PROBLEM' => 'Available as of Memcached 3.0.0.', +'Memcached::RES_BAD_KEY_PROVIDED' => '

Bad key.

', +'Memcached::RES_BUFFERED' => '

The operation was buffered.

', +'Memcached::RES_CLIENT_ERROR' => '

Error on the client side.

', +'Memcached::RES_CONNECTION_SOCKET_CREATE_FAILURE' => '

Failed to create network socket.

', +'Memcached::RES_DATA_EXISTS' => '

Failed to do compare-and-swap: item you are trying to store has been +modified since you last fetched it.

', +'Memcached::RES_E2BIG' => 'Available as of Memcached 3.0.0.', +'Memcached::RES_END' => '

End of result set.

', +'Memcached::RES_ERRNO' => '

System error.

', +'Memcached::RES_FAILURE' => '

The operation failed in some fashion.

', +'Memcached::RES_HOST_LOOKUP_FAILURE' => '

DNS lookup failed.

', +'Memcached::RES_KEY_TOO_BIG' => 'Available as of Memcached 3.0.0.', +'Memcached::RES_NO_SERVERS' => '

Server list is empty.

', +'Memcached::RES_NOTFOUND' => '

Item with this key was not found (with "get" operation or "case" +operations).

', +'Memcached::RES_NOTSTORED' => '

Item was not stored: but not because of an error. This normally +means that either the condition for an "add" or a "replace" command +wasn\'t met, or that the item is in a delete queue.

', +'Memcached::RES_PARTIAL_READ' => '

Partial network data read error.

', +'Memcached::RES_PAYLOAD_FAILURE' => '

Payload failure: could not compress/decompress or serialize/unserialize the value.

', +'Memcached::RES_PROTOCOL_ERROR' => '

Bad command in memcached protocol.

', +'Memcached::RES_SERVER_ERROR' => '

Error on the server side.

', +'Memcached::RES_SERVER_MEMORY_ALLOCATION_FAILURE' => 'Available as of Memcached 3.0.0.', +'Memcached::RES_SERVER_TEMPORARILY_DISABLED' => 'Available as of Memcached 3.0.0.', +'Memcached::RES_SOME_ERRORS' => '

Some errors occurred during multi-get.

', +'Memcached::RES_SUCCESS' => '

The operation was successful.

', +'Memcached::RES_TIMEOUT' => '

The operation timed out.

', +'Memcached::RES_UNKNOWN_READ_FAILURE' => '

Failed to read network data.

', +'Memcached::RES_WRITE_FAILURE' => '

Failed to write network data.

', +'Memcached::SERIALIZER_IGBINARY' => '

The igbinary serializer. +Instead of textual representation it stores PHP data structures in a +compact binary form, resulting in space and time gains.

', +'Memcached::SERIALIZER_JSON' => '

The JSON serializer. Requires PHP 5.2.10+.

', +'Memcached::SERIALIZER_PHP' => '

The default PHP serializer.

', +'MONGO_STREAMS' => 'Alias of `MONGO_SUPPORTS_STREAMS`', +'MONGO_SUPPORTS_AUTH_MECHANISM_GSSAPI' => '1 when GSSAPI authentication is compiled in.', +'MONGO_SUPPORTS_AUTH_MECHANISM_MONGODB_CR' => '1 when MongoDB-Challenge/Response authentication is compiled in.', +'MONGO_SUPPORTS_AUTH_MECHANISM_MONGODB_X509' => '1 when x.509 authentication is compiled in.', +'MONGO_SUPPORTS_AUTH_MECHANISM_PLAIN' => '1 when PLAIN authentication is compiled in.', +'MONGO_SUPPORTS_SSL' => '1 when the PHP manual\'s section on book.openssl is enabled and available.', +'MONGO_SUPPORTS_STREAMS' => '1 when compiled against PHP Streams (default since 1.4.0).', +'MongoBinData::BYTE_ARRAY' => 'Generic binary data (deprecated in favor of MongoBinData::GENERIC)', +'MongoBinData::CUSTOM' => 'User-defined type', +'MongoBinData::FUNC' => 'Function', +'MongoBinData::GENERIC' => 'Generic binary data.', +'MongoBinData::MD5' => 'MD5', +'MongoBinData::UUID' => 'Universally unique identifier (deprecated in favor of MongoBinData::UUID_RFC4122)', +'MongoBinData::UUID_RFC4122' => 'Universally unique identifier (according to » RFC 4122)', +'MongoDB::PROFILING_OFF' => 'Profiling is off.', +'MongoDB::PROFILING_ON' => 'Profiling is on for all operations.', +'MongoDB::PROFILING_SLOW' => 'Profiling is on for slow operations (>100 ms).', +'MongoDB\Driver\WriteConcern::MAJORITY' => 'Majority of all the members in the set; arbiters, non-voting members, passive members, hidden members and delayed members are all included in the definition of majority write concern.', +'MONGODB_STABILITY' => 'Current stability (alpha/beta/stable)', +'MONGODB_VERSION' => 'x.y.z style version number of the extension', +'MS_TRUE' => 'Mapscript extension (version 7.0.*) +Parsed from documentation +Generated at 2017-08-24 16:06:54', +'MSG_EOF' => 'Not available on Windows platforms.', +'MSG_EOR' => 'Not available on Windows platforms.', +'MSSQL_ASSOC' => 'Return an associative array. Used on `mssql_fetch_array`\'s `result_type` parameter.', +'MSSQL_BOTH' => 'Return an array with both numeric keys and keys with their field name. This is the default value for `mssql_fetch_array`\'s `result_type` parameter.', +'MSSQL_NUM' => 'Return an array with numeric keys. Used on `mssql_fetch_array`\'s `result_type` parameter.', +'MYSQLI_ASSOC' => 'Columns are returned into the array having the fieldname as the array index.', +'MYSQLI_AUTO_INCREMENT_FLAG' => 'Field is defined as `AUTO_INCREMENT`', +'MYSQLI_BINARY_FLAG' => 'Field is defined as `BINARY`.', +'MYSQLI_BLOB_FLAG' => 'Field is defined as `BLOB`', +'MYSQLI_BOTH' => 'Columns are returned into the array having both a numerical index and the fieldname as the associative index.', +'MYSQLI_CLIENT_COMPRESS' => 'Use compression protocol', +'MYSQLI_CLIENT_IGNORE_SPACE' => 'Allow spaces after function names. Makes all functions names reserved words.', +'MYSQLI_CLIENT_INTERACTIVE' => 'Allow `interactive_timeout` seconds (instead of `wait_timeout` seconds) of inactivity before closing the connection. The client\'s session `wait_timeout` variable will be set to the value of the session `interactive_timeout` variable.', +'MYSQLI_CLIENT_MULTI_QUERIES' => 'Allows multiple semicolon-delimited queries in a single `mysqli_query` call.', +'MYSQLI_CLIENT_NO_SCHEMA' => 'Don\'t allow the `db_name.tbl_name.col_name` syntax.', +'MYSQLI_CLIENT_SSL' => 'Use SSL (encrypted protocol). This option should not be set by application programs; it is set internally in the MySQL client library', +'MYSQLI_DATA_TRUNCATED' => 'Data truncation occurred. Available since PHP 5.1.0 and MySQL 5.0.5.', +'MYSQLI_DEBUG_TRACE_ENABLED' => 'Is set to 1 if `mysqli_debug` functionality is enabled.', +'MYSQLI_ENUM_FLAG' => 'Field is defined as `ENUM`.', +'MYSQLI_GROUP_FLAG' => 'Field is part of `GROUP BY`', +'MYSQLI_INIT_COMMAND' => 'Command to execute when connecting to MySQL server. Will automatically be re-executed when reconnecting.', +'MYSQLI_MULTIPLE_KEY_FLAG' => 'Field is part of an index.', +'MYSQLI_NEED_DATA' => 'More data available for bind variable', +'MYSQLI_NO_DATA' => 'No more data available for bind variable', +'MYSQLI_NOT_NULL_FLAG' => 'Indicates that a field is defined as `NOT NULL`', +'MYSQLI_NUM' => 'Columns are returned into the array having an enumerated index.', +'MYSQLI_NUM_FLAG' => 'Field is defined as `NUMERIC`', +'MYSQLI_OPT_CONNECT_TIMEOUT' => 'Connect timeout in seconds', +'MYSQLI_OPT_LOCAL_INFILE' => 'Enables command `LOAD LOCAL INFILE`', +'MYSQLI_PART_KEY_FLAG' => 'Field is part of an multi-index', +'MYSQLI_PRI_KEY_FLAG' => 'Field is part of a primary index', +'MYSQLI_READ_DEFAULT_FILE' => 'Read options from the named option file instead of from my.cnf', +'MYSQLI_READ_DEFAULT_GROUP' => 'Read options from the named group from my.cnf or the file specified with `MYSQLI_READ_DEFAULT_FILE`', +'MYSQLI_REFRESH_GRANT' => 'Refreshes the grant tables.', +'MYSQLI_REFRESH_HOSTS' => 'Flushes the host cache, like executing the `FLUSH HOSTS` SQL statement.', +'MYSQLI_REFRESH_LOG' => 'Flushes the logs, like executing the `FLUSH LOGS` SQL statement.', +'MYSQLI_REFRESH_MASTER' => 'On a master replication server: removes the binary log files listed in the binary log index, and truncates the index file. Like executing the `RESET MASTER` SQL statement.', +'MYSQLI_REFRESH_SLAVE' => 'On a slave replication server: resets the master server information, and restarts the slave. Like executing the `RESET SLAVE` SQL statement.', +'MYSQLI_REFRESH_STATUS' => 'Reset the status variables, like executing the `FLUSH STATUS` SQL statement.', +'MYSQLI_REFRESH_TABLES' => 'Flushes the table cache, like executing the `FLUSH TABLES` SQL statement.', +'MYSQLI_REFRESH_THREADS' => 'Flushes the thread cache.', +'MYSQLI_REPORT_ALL' => 'Set all options on (report all).', +'MYSQLI_REPORT_ERROR' => 'Report errors from mysqli function calls.', +'MYSQLI_REPORT_INDEX' => 'Report if no index or bad index was used in a query.', +'MYSQLI_REPORT_OFF' => 'Turns reporting off.', +'MYSQLI_REPORT_STRICT' => 'Throw a `mysqli_sql_exception` for errors instead of warnings.', +'MYSQLI_SET_FLAG' => 'Field is defined as `SET`', +'MYSQLI_STORE_RESULT' => 'For using buffered resultsets', +'MYSQLI_TIMESTAMP_FLAG' => 'Field is defined as `TIMESTAMP`', +'MYSQLI_TRANS_COR_AND_CHAIN' => 'Appends "AND CHAIN" to `mysqli_commit` or `mysqli_rollback`.', +'MYSQLI_TRANS_COR_AND_NO_CHAIN' => 'Appends "AND NO CHAIN" to `mysqli_commit` or `mysqli_rollback`.', +'MYSQLI_TRANS_COR_NO_RELEASE' => 'Appends "NO RELEASE" to `mysqli_commit` or `mysqli_rollback`.', +'MYSQLI_TRANS_COR_RELEASE' => 'Appends "RELEASE" to `mysqli_commit` or `mysqli_rollback`.', +'MYSQLI_TRANS_START_CONSISTENT_SNAPSHOT' => 'Start the transaction as "START TRANSACTION WITH CONSISTENT SNAPSHOT" with `mysqli_begin_transaction`.', +'MYSQLI_TRANS_START_READ_ONLY' => 'Start the transaction as "START TRANSACTION READ ONLY" with `mysqli_begin_transaction`.', +'MYSQLI_TRANS_START_READ_WRITE' => 'Start the transaction as "START TRANSACTION READ WRITE" with `mysqli_begin_transaction`.', +'MYSQLI_TYPE_BIT' => 'Field is defined as `BIT` (MySQL 5.0.3 and up)', +'MYSQLI_TYPE_BLOB' => 'Field is defined as `BLOB`', +'MYSQLI_TYPE_CHAR' => 'Field is defined as `TINYINT`. For `CHAR`, see `MYSQLI_TYPE_STRING`', +'MYSQLI_TYPE_DATE' => 'Field is defined as `DATE`', +'MYSQLI_TYPE_DATETIME' => 'Field is defined as `DATETIME`', +'MYSQLI_TYPE_DECIMAL' => 'Field is defined as `DECIMAL`', +'MYSQLI_TYPE_DOUBLE' => 'Field is defined as `DOUBLE`', +'MYSQLI_TYPE_ENUM' => 'Field is defined as `ENUM`', +'MYSQLI_TYPE_FLOAT' => 'Field is defined as `FLOAT`', +'MYSQLI_TYPE_GEOMETRY' => 'Field is defined as `GEOMETRY`', +'MYSQLI_TYPE_INT24' => 'Field is defined as `MEDIUMINT`', +'MYSQLI_TYPE_INTERVAL' => 'Field is defined as `INTERVAL`', +'MYSQLI_TYPE_LONG' => 'Field is defined as `INT`', +'MYSQLI_TYPE_LONG_BLOB' => 'Field is defined as `LONGBLOB`', +'MYSQLI_TYPE_LONGLONG' => 'Field is defined as `BIGINT`', +'MYSQLI_TYPE_MEDIUM_BLOB' => 'Field is defined as `MEDIUMBLOB`', +'MYSQLI_TYPE_NEWDATE' => 'Field is defined as `DATE`', +'MYSQLI_TYPE_NEWDECIMAL' => 'Precision math `DECIMAL` or `NUMERIC` field (MySQL 5.0.3 and up)', +'MYSQLI_TYPE_NULL' => 'Field is defined as `DEFAULT NULL`', +'MYSQLI_TYPE_SET' => 'Field is defined as `SET`', +'MYSQLI_TYPE_SHORT' => 'Field is defined as `SMALLINT`', +'MYSQLI_TYPE_STRING' => 'Field is defined as `CHAR` or `BINARY`', +'MYSQLI_TYPE_TIME' => 'Field is defined as `TIME`', +'MYSQLI_TYPE_TIMESTAMP' => 'Field is defined as `TIMESTAMP`', +'MYSQLI_TYPE_TINY' => 'Field is defined as `TINYINT`', +'MYSQLI_TYPE_TINY_BLOB' => 'Field is defined as `TINYBLOB`', +'MYSQLI_TYPE_VAR_STRING' => 'Field is defined as `VARCHAR`', +'MYSQLI_TYPE_YEAR' => 'Field is defined as `YEAR`', +'MYSQLI_UNIQUE_KEY_FLAG' => 'Field is part of a unique index.', +'MYSQLI_UNSIGNED_FLAG' => 'Field is defined as `UNSIGNED`', +'MYSQLI_USE_RESULT' => 'For using unbuffered resultsets', +'MYSQLI_ZEROFILL_FLAG' => 'Field is defined as `ZEROFILL`', +'MYSQLND_MEMCACHE_DEFAULT_REGEXP' => 'Default regular expression (PCRE style) used for matching `SELECT` statements that will be mapped into a MySQL Memcache Plugin access point, if possible. + +It is also possible to use `mysqlnd_memcache_set`, but the default approach is using this regular expression for pattern matching.', +'MYSQLND_MEMCACHE_VERSION' => 'Plugin version string, for example, 1.0.0-alpha.', +'MYSQLND_MEMCACHE_VERSION_ID' => 'Plugin version number, for example, 10000.', +'MYSQLND_MS_LAST_USED_SWITCH' => 'SQL hint used to send a query to the last used MySQL server. The last used MySQL server can either be a master or a slave server in a MySQL replication setup.', +'MYSQLND_MS_MASTER_SWITCH' => 'SQL hint used to send a query to the MySQL replication master server.', +'MYSQLND_MS_QOS_CONSISTENCY_EVENTUAL' => 'Use to request the service level eventual consistency from the `mysqlnd_ms_set_qos`. Eventual consistency is the default quality of service when reading from an asynchronous MySQL replication slave. Data returned in this service level may or may not be stale, depending on whether the selected slaves happen to have replicated the latest changes from the MySQL replication master or not.', +'MYSQLND_MS_QOS_CONSISTENCY_SESSION' => 'Use to request the service level session consistency from the `mysqlnd_ms_set_qos`. Session consistency is defined as read your writes. The client is guaranteed to see his latest changes.', +'MYSQLND_MS_QOS_CONSISTENCY_STRONG' => 'Use to request the service level strong consistency from the `mysqlnd_ms_set_qos`. Strong consistency is used to ensure all clients see each others changes.', +'MYSQLND_MS_QOS_OPTION_AGE' => 'Used as a service level option with `mysqlnd_ms_set_qos` to parameterize eventual consistency.', +'MYSQLND_MS_QOS_OPTION_GTID' => 'Used as a service level option with `mysqlnd_ms_set_qos` to parameterize session consistency.', +'MYSQLND_MS_QUERY_USE_LAST_USED' => 'If `mysqlnd_ms_is_select` returns `MYSQLND_MS_QUERY_USE_LAST_USED` for a given query, the built-in read/write split mechanism recommends sending the query to the last used server.', +'MYSQLND_MS_QUERY_USE_MASTER' => 'If `mysqlnd_ms_is_select` returns `MYSQLND_MS_QUERY_USE_MASTER` for a given query, the built-in read/write split mechanism recommends sending the query to a MySQL replication master server.', +'MYSQLND_MS_QUERY_USE_SLAVE' => 'If `mysqlnd_ms_is_select` returns `MYSQLND_MS_QUERY_USE_SLAVE` for a given query, the built-in read/write split mechanism recommends sending the query to a MySQL replication slave server.', +'MYSQLND_MS_SLAVE_SWITCH' => 'SQL hint used to send a query to one of the MySQL replication slave servers.', +'MYSQLND_MS_VERSION' => 'Plugin version string, for example, 1.0.0-prototype.', +'MYSQLND_MS_VERSION_ID' => 'Plugin version number, for example, 10000.', +'MYSQLND_MUX_VERSION' => 'Plugin version string, for example, 1.0.0-prototype.', +'MYSQLND_MUX_VERSION_ID' => 'Plugin version number, for example, 10000.', +'MYSQLND_QC_CONDITION_META_SCHEMA_PATTERN' => 'Used as a parameter of `mysqlnd_qc_set_cache_condition` to set conditions for schema based automatic caching.', +'MYSQLND_QC_DISABLE_SWITCH' => 'SQL hint used to disable caching of a query if `mysqlnd_qc.cache_by_default = 1`.', +'MYSQLND_QC_ENABLE_SWITCH' => 'SQL hint used to enable caching of a query.', +'MYSQLND_QC_SERVER_ID_SWITCH' => 'This SQL hint should not be used in general. + +It is needed by PECL/mysqlnd_ms to group cache entries for one statement but originating from different physical connections. If the hint is used connection settings such as user, hostname and charset are not considered for generating a cache key of a query. Instead the given value and the query string are used as input to the hashing function that generates the key. + +PECL/mysqlnd_ms may, if instructed, cache results from MySQL Replication slaves. Because it can hold many connections to the slave the cache key shall not be formed from the user, hostname or other settings that may vary for the various slave connections. Instead, PECL/mysqlnd_ms provides an identifier which refers to the group of slave connections that shall be enabled to share cache entries no matter which physical slave connection was to generate the cache entry. + +Use of this feature outside of PECL/mysqlnd_ms is not recommended.', +'MYSQLND_QC_TTL_SWITCH' => 'SQL hint used to set the TTL of a result set.', +'MYSQLND_QC_VERSION' => 'Plugin version string, for example, 1.0.0-prototype.', +'MYSQLND_QC_VERSION_ID' => 'Plugin version number, for example, 10000.', +'MYSQLND_UH_MYSQLND_CHG_USER_RESP_PACKET' => 'MySQL Client Server protocol packet: change user response.', +'MYSQLND_UH_MYSQLND_CLOSE_DISCONNECTED' => 'Connection error.', +'MYSQLND_UH_MYSQLND_CLOSE_EXPLICIT' => 'User has called mysqlnd to close the connection.', +'MYSQLND_UH_MYSQLND_CLOSE_IMPLICIT' => 'Implicitly closed, for example, during garbage connection.', +'MYSQLND_UH_MYSQLND_CLOSE_LAST' => 'No practical meaning. Last entry marker of internal C data structure list.', +'MYSQLND_UH_MYSQLND_COM_BINLOG_DUMP' => 'MySQL Client Server protocol command: COM_BINLOG_DUMP.', +'MYSQLND_UH_MYSQLND_COM_CHANGE_USER' => 'MySQL Client Server protocol command: COM_CHANGE_USER.', +'MYSQLND_UH_MYSQLND_COM_CONNECT' => 'MySQL Client Server protocol command: COM_CONNECT.', +'MYSQLND_UH_MYSQLND_COM_CONNECT_OUT' => 'MySQL Client Server protocol command: COM_CONNECT_OUT.', +'MYSQLND_UH_MYSQLND_COM_CREATE_DB' => 'MySQL Client Server protocol command: COM_CREATE_DB.', +'MYSQLND_UH_MYSQLND_COM_DAEMON' => 'MySQL Client Server protocol command: COM_DAEMON.', +'MYSQLND_UH_MYSQLND_COM_DEBUG' => 'MySQL Client Server protocol command: COM_DEBUG.', +'MYSQLND_UH_MYSQLND_COM_DELAYED_INSERT' => 'MySQL Client Server protocol command: COM_DELAYED_INSERT.', +'MYSQLND_UH_MYSQLND_COM_DROP_DB' => 'MySQL Client Server protocol command: COM_DROP_DB.', +'MYSQLND_UH_MYSQLND_COM_END' => 'MySQL Client Server protocol command: COM_END.', +'MYSQLND_UH_MYSQLND_COM_FIELD_LIST' => 'MySQL Client Server protocol command: COM_FIELD_LIST.', +'MYSQLND_UH_MYSQLND_COM_INIT_DB' => 'MySQL Client Server protocol command: COM_INIT_DB.', +'MYSQLND_UH_MYSQLND_COM_PING' => 'MySQL Client Server protocol command: COM_PING.', +'MYSQLND_UH_MYSQLND_COM_PROCESS_INFO' => 'MySQL Client Server protocol command: COM_PROCESS_INFO.', +'MYSQLND_UH_MYSQLND_COM_PROCESS_KILL' => 'MySQL Client Server protocol command: COM_PROCESS_KILL.', +'MYSQLND_UH_MYSQLND_COM_QUERY' => 'MySQL Client Server protocol command: COM_QUERY.', +'MYSQLND_UH_MYSQLND_COM_QUIT' => 'MySQL Client Server protocol command: COM_QUIT.', +'MYSQLND_UH_MYSQLND_COM_REFRESH' => 'MySQL Client Server protocol command: COM_REFRESH.', +'MYSQLND_UH_MYSQLND_COM_REGISTER_SLAVED' => 'MySQL Client Server protocol command: COM_REGISTER_SLAVED.', +'MYSQLND_UH_MYSQLND_COM_SET_OPTION' => 'MySQL Client Server protocol command: COM_SET_OPTION.', +'MYSQLND_UH_MYSQLND_COM_SHUTDOWN' => 'MySQL Client Server protocol command: COM_SHUTDOWN.', +'MYSQLND_UH_MYSQLND_COM_SLEEP' => 'MySQL Client Server protocol command: COM_SLEEP.', +'MYSQLND_UH_MYSQLND_COM_STATISTICS' => 'MySQL Client Server protocol command: COM_STATISTICS.', +'MYSQLND_UH_MYSQLND_COM_STMT_CLOSE' => 'MySQL Client Server protocol command: COM_STMT_CLOSE.', +'MYSQLND_UH_MYSQLND_COM_STMT_EXECUTE' => 'MySQL Client Server protocol command: COM_STMT_EXECUTE.', +'MYSQLND_UH_MYSQLND_COM_STMT_FETCH' => 'MySQL Client Server protocol command: COM_STMT_FETCH.', +'MYSQLND_UH_MYSQLND_COM_STMT_PREPARE' => 'MySQL Client Server protocol command: COM_STMT_PREPARE.', +'MYSQLND_UH_MYSQLND_COM_STMT_RESET' => 'MySQL Client Server protocol command: COM_STMT_RESET.', +'MYSQLND_UH_MYSQLND_COM_STMT_SEND_LONG_DATA' => 'MySQL Client Server protocol command: COM_STMT_SEND_LONG_DATA.', +'MYSQLND_UH_MYSQLND_COM_TABLE_DUMP' => 'MySQL Client Server protocol command: COM_TABLE_DUMP.', +'MYSQLND_UH_MYSQLND_COM_TIME' => 'MySQL Client Server protocol command: COM_TIME.', +'MYSQLND_UH_MYSQLND_OPT_AUTH_PROTOCOL' => 'Option: TODO. Available as of `PHP 5.4.0`.', +'MYSQLND_UH_MYSQLND_OPT_GUESS_CONNECTION' => 'TODO', +'MYSQLND_UH_MYSQLND_OPT_INT_AND_FLOAT_NATIVE' => 'Option: make mysqlnd return integer and float columns as long even when using the MySQL Client Server text protocol. Only available with a custom build of mysqlnd.', +'MYSQLND_UH_MYSQLND_OPT_LOCAL_INFILE' => 'Option: Whether to allow `LOAD DATA LOCAL INFILE` use.', +'MYSQLND_UH_MYSQLND_OPT_MAX_ALLOWED_PACKET' => 'Option: maximum allowed packet size. Available as of `PHP 5.4.0`.', +'MYSQLND_UH_MYSQLND_OPT_NET_CMD_BUFFER_SIZE' => 'Option: mysqlnd network buffer size for commands.', +'MYSQLND_UH_MYSQLND_OPT_NET_READ_BUFFER_SIZE' => 'Option: mysqlnd network buffer size for reading from the server.', +'MYSQLND_UH_MYSQLND_OPT_PROTOCOL' => 'Option: supported protocol version.', +'MYSQLND_UH_MYSQLND_OPT_READ_TIMEOUT' => 'Option: connection read timeout.', +'MYSQLND_UH_MYSQLND_OPT_RECONNECT' => 'Option: Whether to reconnect automatically.', +'MYSQLND_UH_MYSQLND_OPT_SSL_CA' => 'Option: SSL CA.', +'MYSQLND_UH_MYSQLND_OPT_SSL_CAPATH' => 'Option: Path to SSL CA.', +'MYSQLND_UH_MYSQLND_OPT_SSL_CERT' => 'Option: SSL certificate.', +'MYSQLND_UH_MYSQLND_OPT_SSL_CIPHER' => 'Option: SSL cipher.', +'MYSQLND_UH_MYSQLND_OPT_SSL_KEY' => 'Option: SSL key.', +'MYSQLND_UH_MYSQLND_OPT_SSL_PASSPHRASE' => 'Option: SSL passphrase.', +'MYSQLND_UH_MYSQLND_OPT_SSL_VERIFY_SERVER_CERT' => 'Option: TODO', +'MYSQLND_UH_MYSQLND_OPT_USE_EMBEDDED_CONNECTION' => 'Embedded server related.', +'MYSQLND_UH_MYSQLND_OPT_USE_REMOTE_CONNECTION' => 'Embedded server related.', +'MYSQLND_UH_MYSQLND_OPT_USE_RESULT' => 'Option: unbuffered result sets.', +'MYSQLND_UH_MYSQLND_OPT_WRITE_TIMEOUT' => 'Option: connection write timeout.', +'MYSQLND_UH_MYSQLND_OPTION_INIT_COMMAND' => 'Option: init command to execute upon connect.', +'MYSQLND_UH_MYSQLND_OPTION_OPT_COMPRESS' => 'Option: whether the MySQL compressed protocol is to be used.', +'MYSQLND_UH_MYSQLND_OPTION_OPT_CONNECT_TIMEOUT' => 'Option: connection timeout.', +'MYSQLND_UH_MYSQLND_OPTION_OPT_NAMED_PIPE' => 'Option: named pipe to use for connection (Windows).', +'MYSQLND_UH_MYSQLND_PREPARE_RESP_PACKET' => 'MySQL Client Server protocol packet: prepare response.', +'MYSQLND_UH_MYSQLND_PROT_AUTH_PACKET' => 'MySQL Client Server protocol packet: authentication.', +'MYSQLND_UH_MYSQLND_PROT_CMD_PACKET' => 'MySQL Client Server protocol packet: command.', +'MYSQLND_UH_MYSQLND_PROT_EOF_PACKET' => 'MySQL Client Server protocol packet: EOF.', +'MYSQLND_UH_MYSQLND_PROT_GREET_PACKET' => 'MySQL Client Server protocol packet: greeting.', +'MYSQLND_UH_MYSQLND_PROT_LAST' => 'No practical meaning. Last entry marker of internal C data structure list.', +'MYSQLND_UH_MYSQLND_PROT_OK_PACKET' => 'MySQL Client Server protocol packet: OK.', +'MYSQLND_UH_MYSQLND_PROT_ROW_PACKET' => 'MySQL Client Server protocol packet: row.', +'MYSQLND_UH_MYSQLND_PROT_RSET_FLD_PACKET' => 'MySQL Client Server protocol packet: resultset field.', +'MYSQLND_UH_MYSQLND_PROT_RSET_HEADER_PACKET' => 'MySQL Client Server protocol packet: result set header.', +'MYSQLND_UH_MYSQLND_PROT_STATS_PACKET' => 'MySQL Client Server protocol packet: stats.', +'MYSQLND_UH_MYSQLND_READ_DEFAULT_FILE' => 'Option: MySQL server default file to read upon connect.', +'MYSQLND_UH_MYSQLND_READ_DEFAULT_GROUP' => 'Option: MySQL server default file group to read upon connect.', +'MYSQLND_UH_MYSQLND_REPORT_DATA_TRUNCATION' => 'Option: Whether to report data truncation.', +'MYSQLND_UH_MYSQLND_SECURE_AUTH' => 'TODO', +'MYSQLND_UH_MYSQLND_SET_CHARSET_DIR' => 'Option: charset description files directory.', +'MYSQLND_UH_MYSQLND_SET_CHARSET_NAME' => 'Option: charset name.', +'MYSQLND_UH_MYSQLND_SET_CLIENT_IP' => 'TODO', +'MYSQLND_UH_MYSQLND_SHARED_MEMORY_BASE_NAME' => 'Option: shared memory base name for shared memory connections.', +'MYSQLND_UH_SERVER_OPTION_DEFAULT_AUTH' => 'Option: default authentication method.', +'MYSQLND_UH_SERVER_OPTION_MULTI_STATEMENTS_OFF' => 'Option: disables multi statement support.', +'MYSQLND_UH_SERVER_OPTION_MULTI_STATEMENTS_ON' => 'Option: enables multi statement support.', +'MYSQLND_UH_SERVER_OPTION_PLUGIN_DIR' => 'Option: server plugin directory.', +'MYSQLND_UH_SERVER_OPTION_SET_CLIENT_IP' => 'TODO', +'MYSQLND_UH_VERSION' => 'Plugin version string, for example, 1.0.0-alpha.', +'MYSQLND_UH_VERSION_ID' => 'Plugin version number, for example, 10000.', +'OAUTH_AUTH_TYPE_AUTHORIZATION' => 'This constant represents putting OAuth parameters in the `Authorization` header.', +'OAUTH_AUTH_TYPE_FORM' => 'This constant represents putting OAuth parameters as part of the HTTP POST body.', +'OAUTH_AUTH_TYPE_NONE' => 'This constant indicates a NoAuth OAuth request.', +'OAUTH_AUTH_TYPE_URI' => 'This constant represents putting OAuth parameters in the request URI.', +'OAUTH_BAD_NONCE' => 'The *oauth_nonce* value was used in a previous request, therefore it cannot be used now.', +'OAUTH_BAD_TIMESTAMP' => 'The *oauth_timestamp* value was not accepted by the service provider. In this case, the response should also contain the *oauth_acceptable_timestamps* parameter.', +'OAUTH_CONSUMER_KEY_REFUSED' => 'The consumer key was refused.', +'OAUTH_CONSUMER_KEY_UNKNOWN' => 'The *oauth_consumer_key* is temporarily unacceptable to the service provider. For example, the service provider may be throttling the consumer.', +'OAUTH_HTTP_METHOD_DELETE' => 'Use the *DELETE* method for the OAuth request.', +'OAUTH_HTTP_METHOD_GET' => 'Use the *GET* method for the OAuth request.', +'OAUTH_HTTP_METHOD_HEAD' => 'Use the *HEAD* method for the OAuth request.', +'OAUTH_HTTP_METHOD_POST' => 'Use the *POST* method for the OAuth request.', +'OAUTH_HTTP_METHOD_PUT' => 'Use the *PUT* method for the OAuth request.', +'OAUTH_INVALID_SIGNATURE' => 'The *oauth_signature* is invalid, as it does not match the signature computed by the service provider.', +'OAUTH_OK' => 'Life is good.', +'OAUTH_PARAMETER_ABSENT' => 'A required parameter was not received. In this case, the response should also contain the *oauth_parameters_absent* parameter.', +'OAUTH_REQENGINE_CURL' => 'Used by `OAuth::setRequestEngine` to set the engine to Curl, as opposed to `OAUTH_REQENGINE_STREAMS` for PHP streams.', +'OAUTH_REQENGINE_STREAMS' => 'Used by `OAuth::setRequestEngine` to set the engine to PHP streams, as opposed to `OAUTH_REQENGINE_CURL` for Curl.', +'OAUTH_SIG_METHOD_HMACSHA1' => 'OAuth *HMAC-SHA1* signature method.', +'OAUTH_SIG_METHOD_HMACSHA256' => 'OAuth *HMAC-SHA256* signature method.', +'OAUTH_SIG_METHOD_RSASHA1' => 'OAuth *RSA-SHA1* signature method.', +'OAUTH_SIGNATURE_METHOD_REJECTED' => 'The *oauth_signature_method* was not accepted by service provider.', +'OAUTH_TOKEN_EXPIRED' => 'The *oauth_token* has expired.', +'OAUTH_TOKEN_REJECTED' => 'The *oauth_token* was not accepted by the service provider. The reason is not known, but it might be because the token was never issued, already consumed, expired, and/or forgotten by the service provider.', +'OAUTH_TOKEN_REVOKED' => 'The *oauth_token* has been revoked, and will never be accepted.', +'OAUTH_TOKEN_USED' => 'The *oauth_token* has been consumed. It can no longer be used because it has already been used in the previous request(s).', +'OAUTH_VERIFIER_INVALID' => 'The *oauth_verifier* is incorrect.', +'OP_ANONYMOUS' => 'Don\'t use or update a .newsrc for news (NNTP only)', +'OP_HALFOPEN' => 'For IMAP and NNTP names, open a connection but don\'t open a mailbox.', +'OP_READONLY' => 'Open mailbox read-only', +'OPENSSL_ALGO_MD2' => 'As of PHP 5.2.13 and PHP 5.3.2, this constant is only available if PHP is compiled with MD2 support. This requires passing in the -DHAVE_OPENSSL_MD2_H CFLAG when compiling PHP, and enable-md2 when compiling OpenSSL 1.0.0+.', +'OPENSSL_ALGO_RMD160' => 'Added in PHP 5.4.8.', +'OPENSSL_ALGO_SHA1' => 'Used as default algorithm by `openssl_sign` and `openssl_verify`.', +'OPENSSL_ALGO_SHA224' => 'Added in PHP 5.4.8.', +'OPENSSL_ALGO_SHA256' => 'Added in PHP 5.4.8.', +'OPENSSL_ALGO_SHA384' => 'Added in PHP 5.4.8.', +'OPENSSL_ALGO_SHA512' => 'Added in PHP 5.4.8.', +'OPENSSL_CIPHER_AES_128_CBC' => 'Added in PHP 5.4.0.', +'OPENSSL_CIPHER_AES_192_CBC' => 'Added in PHP 5.4.0.', +'OPENSSL_CIPHER_AES_256_CBC' => 'Added in PHP 5.4.0.', +'OPENSSL_KEYTYPE_EC' => 'This constant is only available when PHP is compiled with OpenSSL 0.9.8+.', +'OPENSSL_TLSEXT_SERVER_NAME' => 'Whether SNI support is available or not.', +'OPENSSL_VERSION_NUMBER' => 'Added in PHP 5.2.0.', +'OPENSSL_VERSION_TEXT' => 'Added in PHP 5.2.0.', +'parallel\Channel::Infinite' => 'Constant for Infinitely Buffered', +'parallel\Events\Event\Type::Cancel' => 'Event::$object (Future) was cancelled', +'parallel\Events\Event\Type::Close' => 'Event::$object (Channel) was closed', +'parallel\Events\Event\Type::Error' => 'Event::$object (Future) raised error', +'parallel\Events\Event\Type::Kill' => 'Runtime executing Event::$object (Future) was killed', +'parallel\Events\Event\Type::Read' => 'Event::$object was read into Event::$value', +'parallel\Events\Event\Type::Write' => 'Input for Event::$source written to Event::$object', +'Parle\INTERNAL_UTF32' => 'Flag whether the internal UTF-32 support is compiled in. Available since parle 0.7.2.', +'Parle\Token::EOI' => 'End of input token id.', +'Parle\Token::SKIP' => 'Skip token id.', +'Parle\Token::UNKNOWN' => 'Unknown token id.', +'PARSEKIT_EXTENDED_VALUE' => 'Opnode Flag', +'PARSEKIT_IS_CONST' => 'Node Type', +'PARSEKIT_IS_TMP_VAR' => 'Node Type', +'PARSEKIT_IS_UNUSED' => 'Node Type', +'PARSEKIT_IS_VAR' => 'Node Type', +'PARSEKIT_QUIET' => 'Return full detail, but without unnecessary NULL entries.', +'PARSEKIT_RESULT_CONST' => 'Opnode Flag', +'PARSEKIT_RESULT_EA_TYPE' => 'Opnode Flag', +'PARSEKIT_RESULT_JMP_ADDR' => 'Opnode Flag', +'PARSEKIT_RESULT_OPARRAY' => 'Opnode Flag', +'PARSEKIT_RESULT_OPLINE' => 'Opnode Flag', +'PARSEKIT_RESULT_VAR' => 'Opnode Flag', +'PARSEKIT_SIMPLE' => 'Return shorthand opcode notation.', +'PARSEKIT_USAGE_UNKNOWN' => 'Opnode Flag', +'PARSEKIT_ZEND_ADD' => 'Opcode', +'PARSEKIT_ZEND_ADD_ARRAY_ELEMENT' => 'Opcode', +'PARSEKIT_ZEND_ADD_CHAR' => 'Opcode', +'PARSEKIT_ZEND_ADD_INTERFACE' => 'Opcode', +'PARSEKIT_ZEND_ADD_STRING' => 'Opcode', +'PARSEKIT_ZEND_ADD_VAR' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_ADD' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_BW_AND' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_BW_OR' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_BW_XOR' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_CONCAT' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_DIM' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_DIV' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_MOD' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_MUL' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_OBJ' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_REF' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_SL' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_SR' => 'Opcode', +'PARSEKIT_ZEND_ASSIGN_SUB' => 'Opcode', +'PARSEKIT_ZEND_BEGIN_SILENCE' => 'Opcode', +'PARSEKIT_ZEND_BOOL' => 'Opcode', +'PARSEKIT_ZEND_BOOL_NOT' => 'Opcode', +'PARSEKIT_ZEND_BOOL_XOR' => 'Opcode', +'PARSEKIT_ZEND_BRK' => 'Opcode', +'PARSEKIT_ZEND_BW_AND' => 'Opcode', +'PARSEKIT_ZEND_BW_NOT' => 'Opcode', +'PARSEKIT_ZEND_BW_OR' => 'Opcode', +'PARSEKIT_ZEND_BW_XOR' => 'Opcode', +'PARSEKIT_ZEND_CASE' => 'Opcode', +'PARSEKIT_ZEND_CAST' => 'Opcode', +'PARSEKIT_ZEND_CATCH' => 'Opcode', +'PARSEKIT_ZEND_CLONE' => 'Opcode', +'PARSEKIT_ZEND_CONCAT' => 'Opcode', +'PARSEKIT_ZEND_CONT' => 'Opcode', +'PARSEKIT_ZEND_DECLARE_CLASS' => 'Opcode', +'PARSEKIT_ZEND_DECLARE_FUNCTION' => 'Opcode', +'PARSEKIT_ZEND_DECLARE_INHERITED_CLASS' => 'Opcode', +'PARSEKIT_ZEND_DIV' => 'Opcode', +'PARSEKIT_ZEND_DO_FCALL' => 'Opcode', +'PARSEKIT_ZEND_DO_FCALL_BY_NAME' => 'Opcode', +'PARSEKIT_ZEND_ECHO' => 'Opcode', +'PARSEKIT_ZEND_END_SILENCE' => 'Opcode', +'PARSEKIT_ZEND_EVAL_CODE' => 'Function Type', +'PARSEKIT_ZEND_EXIT' => 'Opcode', +'PARSEKIT_ZEND_EXT_FCALL_BEGIN' => 'Opcode', +'PARSEKIT_ZEND_EXT_FCALL_END' => 'Opcode', +'PARSEKIT_ZEND_EXT_NOP' => 'Opcode', +'PARSEKIT_ZEND_EXT_STMT' => 'Opcode', +'PARSEKIT_ZEND_FE_FETCH' => 'Opcode', +'PARSEKIT_ZEND_FE_RESET' => 'Opcode', +'PARSEKIT_ZEND_FETCH_CLASS' => 'Opcode', +'PARSEKIT_ZEND_FETCH_CONSTANT' => 'Opcode', +'PARSEKIT_ZEND_FETCH_DIM_FUNC_ARG' => 'Opcode', +'PARSEKIT_ZEND_FETCH_DIM_IS' => 'Opcode', +'PARSEKIT_ZEND_FETCH_DIM_R' => 'Opcode', +'PARSEKIT_ZEND_FETCH_DIM_RW' => 'Opcode', +'PARSEKIT_ZEND_FETCH_DIM_TMP_VAR' => 'Opcode', +'PARSEKIT_ZEND_FETCH_DIM_UNSET' => 'Opcode', +'PARSEKIT_ZEND_FETCH_DIM_W' => 'Opcode', +'PARSEKIT_ZEND_FETCH_FUNC_ARG' => 'Opcode', +'PARSEKIT_ZEND_FETCH_IS' => 'Opcode', +'PARSEKIT_ZEND_FETCH_OBJ_FUNC_ARG' => 'Opcode', +'PARSEKIT_ZEND_FETCH_OBJ_IS' => 'Opcode', +'PARSEKIT_ZEND_FETCH_OBJ_R' => 'Opcode', +'PARSEKIT_ZEND_FETCH_OBJ_RW' => 'Opcode', +'PARSEKIT_ZEND_FETCH_OBJ_UNSET' => 'Opcode', +'PARSEKIT_ZEND_FETCH_OBJ_W' => 'Opcode', +'PARSEKIT_ZEND_FETCH_R' => 'Opcode', +'PARSEKIT_ZEND_FETCH_RW' => 'Opcode', +'PARSEKIT_ZEND_FETCH_UNSET' => 'Opcode', +'PARSEKIT_ZEND_FETCH_W' => 'Opcode', +'PARSEKIT_ZEND_FREE' => 'Opcode', +'PARSEKIT_ZEND_HANDLE_EXCEPTION' => 'Opcode', +'PARSEKIT_ZEND_IMPORT_CLASS' => 'Opcode', +'PARSEKIT_ZEND_IMPORT_CONST' => 'Opcode', +'PARSEKIT_ZEND_IMPORT_FUNCTION' => 'Opcode', +'PARSEKIT_ZEND_INCLUDE_OR_EVAL' => 'Opcode', +'PARSEKIT_ZEND_INIT_ARRAY' => 'Opcode', +'PARSEKIT_ZEND_INIT_CTOR_CALL' => 'Opcode', +'PARSEKIT_ZEND_INIT_FCALL_BY_NAME' => 'Opcode', +'PARSEKIT_ZEND_INIT_METHOD_CALL' => 'Opcode', +'PARSEKIT_ZEND_INIT_STATIC_METHOD_CALL' => 'Opcode', +'PARSEKIT_ZEND_INIT_STRING' => 'Opcode', +'PARSEKIT_ZEND_INSTANCEOF' => 'Opcode', +'PARSEKIT_ZEND_INTERNAL_CLASS' => 'Class Type', +'PARSEKIT_ZEND_INTERNAL_FUNCTION' => 'Function Type', +'PARSEKIT_ZEND_IS_EQUAL' => 'Opcode', +'PARSEKIT_ZEND_IS_IDENTICAL' => 'Opcode', +'PARSEKIT_ZEND_IS_NOT_EQUAL' => 'Opcode', +'PARSEKIT_ZEND_IS_NOT_IDENTICAL' => 'Opcode', +'PARSEKIT_ZEND_IS_SMALLER' => 'Opcode', +'PARSEKIT_ZEND_IS_SMALLER_OR_EQUAL' => 'Opcode', +'PARSEKIT_ZEND_ISSET_ISEMPTY' => 'Opcode', +'PARSEKIT_ZEND_ISSET_ISEMPTY_DIM_OBJ' => 'Opcode', +'PARSEKIT_ZEND_ISSET_ISEMPTY_PROP_OBJ' => 'Opcode', +'PARSEKIT_ZEND_ISSET_ISEMPTY_VAR' => 'Opcode', +'PARSEKIT_ZEND_JMP' => 'Opcode', +'PARSEKIT_ZEND_JMP_NO_CTOR' => 'Opcode', +'PARSEKIT_ZEND_JMPNZ' => 'Opcode', +'PARSEKIT_ZEND_JMPNZ_EX' => 'Opcode', +'PARSEKIT_ZEND_JMPZ' => 'Opcode', +'PARSEKIT_ZEND_JMPZ_EX' => 'Opcode', +'PARSEKIT_ZEND_JMPZNZ' => 'Opcode', +'PARSEKIT_ZEND_MOD' => 'Opcode', +'PARSEKIT_ZEND_MUL' => 'Opcode', +'PARSEKIT_ZEND_NEW' => 'Opcode', +'PARSEKIT_ZEND_NOP' => 'Opcode', +'PARSEKIT_ZEND_OP_DATA' => 'Opcode', +'PARSEKIT_ZEND_OVERLOADED_FUNCTION' => 'Function Type', +'PARSEKIT_ZEND_OVERLOADED_FUNCTION_TEMPORARY' => 'Function Type', +'PARSEKIT_ZEND_POST_DEC' => 'Opcode', +'PARSEKIT_ZEND_POST_DEC_OBJ' => 'Opcode', +'PARSEKIT_ZEND_POST_INC' => 'Opcode', +'PARSEKIT_ZEND_POST_INC_OBJ' => 'Opcode', +'PARSEKIT_ZEND_PRE_DEC' => 'Opcode', +'PARSEKIT_ZEND_PRE_DEC_OBJ' => 'Opcode', +'PARSEKIT_ZEND_PRE_INC' => 'Opcode', +'PARSEKIT_ZEND_PRE_INC_OBJ' => 'Opcode', +'PARSEKIT_ZEND_PRINT' => 'Opcode', +'PARSEKIT_ZEND_QM_ASSIGN' => 'Opcode', +'PARSEKIT_ZEND_RAISE_ABSTRACT_ERROR' => 'Opcode', +'PARSEKIT_ZEND_RECV' => 'Opcode', +'PARSEKIT_ZEND_RECV_INIT' => 'Opcode', +'PARSEKIT_ZEND_RETURN' => 'Opcode', +'PARSEKIT_ZEND_SEND_REF' => 'Opcode', +'PARSEKIT_ZEND_SEND_VAL' => 'Opcode', +'PARSEKIT_ZEND_SEND_VAR' => 'Opcode', +'PARSEKIT_ZEND_SEND_VAR_NO_REF' => 'Opcode', +'PARSEKIT_ZEND_SL' => 'Opcode', +'PARSEKIT_ZEND_SR' => 'Opcode', +'PARSEKIT_ZEND_SUB' => 'Opcode', +'PARSEKIT_ZEND_SWITCH_FREE' => 'Opcode', +'PARSEKIT_ZEND_THROW' => 'Opcode', +'PARSEKIT_ZEND_TICKS' => 'Opcode', +'PARSEKIT_ZEND_UNSET_DIM_OBJ' => 'Opcode', +'PARSEKIT_ZEND_UNSET_VAR' => 'Opcode', +'PARSEKIT_ZEND_USER_CLASS' => 'Class Type', +'PARSEKIT_ZEND_USER_FUNCTION' => 'Function Type', +'PARSEKIT_ZEND_VERIFY_ABSTRACT_CLASS' => 'Opcode', +'PASSWORD_ARGON2_DEFAULT_MEMORY_COST' => 'Default amount of memory in bytes that Argon2lib will use while trying to compute a hash. + +Available as of PHP 7.2.0.', +'PASSWORD_ARGON2_DEFAULT_THREADS' => 'Default number of threads that Argon2lib will use. + +Available as of PHP 7.2.0.', +'PASSWORD_ARGON2_DEFAULT_TIME_COST' => 'Default amount of time that Argon2lib will spend trying to compute a hash. + +Available as of PHP 7.2.0.', +'PASSWORD_ARGON2I' => '`PASSWORD_ARGON2I` is used to create new password hashes using the Argon2i algorithm. + +Supported Options: + +`memory_cost` (`integer`) - Maximum memory (in bytes) that may be used to compute the Argon2 hash. Defaults to `PASSWORD_ARGON2_DEFAULT_MEMORY_COST`. `time_cost` (`integer`) - Maximum amount of time it may take to compute the Argon2 hash. Defaults to `PASSWORD_ARGON2_DEFAULT_TIME_COST`. `threads` (`integer`) - Number of threads to use for computing the Argon2 hash. Defaults to `PASSWORD_ARGON2_DEFAULT_THREADS`. + +Available as of PHP 7.2.0.', +'PASSWORD_ARGON2ID' => '`PASSWORD_ARGON2ID` is used to create new password hashes using the Argon2id algorithm. It supports the same options as `PASSWORD_ARGON2I`. + +Available as of PHP 7.3.0.', +'PASSWORD_BCRYPT' => '`PASSWORD_BCRYPT` is used to create new password hashes using the `CRYPT_BLOWFISH` algorithm. + +This will always result in a hash using the "$2y$" crypt format, which is always 60 characters wide. + +Supported Options: + +`salt` (`string`) - to manually provide a salt to use when hashing the password. Note that this will override and prevent a salt from being automatically generated. If omitted, a random salt will be generated by `password_hash` for each password hashed. This is the intended mode of operation and as of PHP 7.0.0 the salt option has been deprecated. `cost` (`integer`) - which denotes the algorithmic cost that should be used. Examples of these values can be found on the `crypt` page. If omitted, a default value of `10` will be used. This is a good baseline cost, but you may want to consider increasing it depending on your hardware.', +'PASSWORD_DEFAULT' => 'The default algorithm to use for hashing if no algorithm is provided. This may change in newer PHP releases when newer, stronger hashing algorithms are supported. + +It is worth noting that over time this constant can (and likely will) change. Therefore you should be aware that the length of the resulting hash can change. Therefore, if you use `PASSWORD_DEFAULT` you should store the resulting hash in a way that can store more than 60 characters (255 is the recommended width). + +Values for this constant: + +PHP 5.5.0 - `PASSWORD_BCRYPT`', +'PATH_SEPARATOR' => 'Semicolon on Windows, colon otherwise.', +'PATHINFO_FILENAME' => 'Since PHP 5.2.0.', +'PDO::ATTR_AUTOCOMMIT' => 'If this value is `false`, PDO attempts to disable autocommit so that the connection begins a transaction.', +'PDO::ATTR_CASE' => 'Force column names to a specific case specified by the `PDO::CASE_*` constants.', +'PDO::ATTR_CLIENT_VERSION' => 'This is a read only attribute; it will return information about the version of the client libraries that the PDO driver is using.', +'PDO::ATTR_CURSOR' => 'Selects the cursor type. PDO currently supports either `PDO::CURSOR_FWDONLY` and `PDO::CURSOR_SCROLL`. Stick with `PDO::CURSOR_FWDONLY` unless you know that you need a scrollable cursor.', +'PDO::ATTR_CURSOR_NAME' => 'Get or set the name to use for a cursor. Most useful when using scrollable cursors and positioned updates.', +'PDO::ATTR_DEFAULT_FETCH_MODE' => 'Available since PHP 5.2.0', +'PDO::ATTR_DEFAULT_STR_PARAM' => 'Sets the default string parameter type, this can be one of `PDO::PARAM_STR_NATL` and `PDO::PARAM_STR_CHAR`. + +Available since PHP 7.2.0.', +'PDO::ATTR_DRIVER_NAME' => 'Returns the name of the driver.', +'PDO::ATTR_EMULATE_PREPARES' => 'Available since PHP 5.1.3.', +'PDO::ATTR_ERRMODE' => 'See the Errors and error handling section for more information about this attribute.', +'PDO::ATTR_FETCH_CATALOG_NAMES' => 'Prepend the containing catalog name to each column name returned in the result set. The catalog name and column name are separated by a decimal (.) character. Support of this attribute is at the driver level; it may not be supported by your driver.', +'PDO::ATTR_FETCH_TABLE_NAMES' => 'Prepend the containing table name to each column name returned in the result set. The table name and column name are separated by a decimal (.) character. Support of this attribute is at the driver level; it may not be supported by your driver.', +'PDO::ATTR_MAX_COLUMN_LEN' => 'Sets the maximum column name length.', +'PDO::ATTR_ORACLE_NULLS' => 'Convert empty strings to SQL NULL values on data fetches.', +'PDO::ATTR_PERSISTENT' => 'Request a persistent connection, rather than creating a new connection. See Connections and Connection management for more information on this attribute.', +'PDO::ATTR_PREFETCH' => 'Setting the prefetch size allows you to balance speed against memory usage for your application. Not all database/driver combinations support setting of the prefetch size. A larger prefetch size results in increased performance at the cost of higher memory usage.', +'PDO::ATTR_SERVER_INFO' => 'This is a read only attribute; it will return some meta information about the database server to which PDO is connected.', +'PDO::ATTR_SERVER_VERSION' => 'This is a read only attribute; it will return information about the version of the database server to which PDO is connected.', +'PDO::ATTR_STATEMENT_CLASS' => 'Sets the class name of which statements are returned as.', +'PDO::ATTR_STRINGIFY_FETCHES' => 'Forces all values fetched to be treated as strings.', +'PDO::ATTR_TIMEOUT' => 'Sets the timeout value in seconds for communications with the database.', +'PDO::CASE_LOWER' => 'Force column names to lower case.', +'PDO::CASE_NATURAL' => 'Leave column names as returned by the database driver.', +'PDO::CASE_UPPER' => 'Force column names to upper case.', +'PDO::CURSOR_FWDONLY' => 'Create a `PDOStatement` object with a forward-only cursor. This is the default cursor choice, as it is the fastest and most common data access pattern in PHP.', +'PDO::CURSOR_SCROLL' => 'Create a `PDOStatement` object with a scrollable cursor. Pass the `PDO::FETCH_ORI_*` constants to control the rows fetched from the result set.', +'PDO::ERR_NONE' => 'Corresponds to SQLSTATE \'00000\', meaning that the SQL statement was successfully issued with no errors or warnings. This constant is for your convenience when checking `PDO::errorCode` or `PDOStatement::errorCode` to determine if an error occurred. You will usually know if this is the case by examining the return code from the method that raised the error condition anyway.', +'PDO::ERRMODE_EXCEPTION' => 'Throw a `PDOException` if an error occurs. See Errors and error handling for more information about this attribute.', +'PDO::ERRMODE_SILENT' => 'Do not raise an error or exception if an error occurs. The developer is expected to explicitly check for errors. This is the default mode. See Errors and error handling for more information about this attribute.', +'PDO::ERRMODE_WARNING' => 'Issue a PHP `E_WARNING` message if an error occurs. See Errors and error handling for more information about this attribute.', +'PDO::FB_ATTR_DATE_FORMAT' => 'Sets the date format.', +'PDO::FB_ATTR_TIME_FORMAT' => 'Sets the time format.', +'PDO::FB_ATTR_TIMESTAMP_FORMAT' => 'Sets the timestamp format.', +'PDO::FETCH_ASSOC' => 'Specifies that the fetch method shall return each row as an array indexed by column name as returned in the corresponding result set. If the result set contains multiple columns with the same name, `PDO::FETCH_ASSOC` returns only a single value per column name.', +'PDO::FETCH_BOTH' => 'Specifies that the fetch method shall return each row as an array indexed by both column name and number as returned in the corresponding result set, starting at column 0.', +'PDO::FETCH_BOUND' => 'Specifies that the fetch method shall return TRUE and assign the values of the columns in the result set to the PHP variables to which they were bound with the `PDOStatement::bindParam` or `PDOStatement::bindColumn` methods.', +'PDO::FETCH_CLASS' => 'Specifies that the fetch method shall return a new instance of the requested class, mapping the columns to named properties in the class.', +'PDO::FETCH_CLASSTYPE' => 'Determine the class name from the value of first column.', +'PDO::FETCH_COLUMN' => 'Specifies that the fetch method shall return only a single requested column from the next row in the result set.', +'PDO::FETCH_FUNC' => 'Allows completely customize the way data is treated on the fly (only valid inside `PDOStatement::fetchAll`).', +'PDO::FETCH_GROUP' => 'Group return by values. Usually combined with `PDO::FETCH_COLUMN` or `PDO::FETCH_KEY_PAIR`.', +'PDO::FETCH_INTO' => 'Specifies that the fetch method shall update an existing instance of the requested class, mapping the columns to named properties in the class.', +'PDO::FETCH_KEY_PAIR' => 'Fetch a two-column result into an array where the first column is a key and the second column is the value.', +'PDO::FETCH_LAZY' => 'Specifies that the fetch method shall return each row as an object with variable names that correspond to the column names returned in the result set. `PDO::FETCH_LAZY` creates the object variable names as they are accessed. Not valid inside `PDOStatement::fetchAll`.', +'PDO::FETCH_NAMED' => 'Specifies that the fetch method shall return each row as an array indexed by column name as returned in the corresponding result set. If the result set contains multiple columns with the same name, `PDO::FETCH_NAMED` returns an array of values per column name.', +'PDO::FETCH_NUM' => 'Specifies that the fetch method shall return each row as an array indexed by column number as returned in the corresponding result set, starting at column 0.', +'PDO::FETCH_OBJ' => 'Specifies that the fetch method shall return each row as an object with property names that correspond to the column names returned in the result set.', +'PDO::FETCH_ORI_ABS' => 'Fetch the requested row by row number from the result set. Valid only for scrollable cursors.', +'PDO::FETCH_ORI_FIRST' => 'Fetch the first row in the result set. Valid only for scrollable cursors.', +'PDO::FETCH_ORI_LAST' => 'Fetch the last row in the result set. Valid only for scrollable cursors.', +'PDO::FETCH_ORI_NEXT' => 'Fetch the next row in the result set. Valid only for scrollable cursors.', +'PDO::FETCH_ORI_PRIOR' => 'Fetch the previous row in the result set. Valid only for scrollable cursors.', +'PDO::FETCH_ORI_REL' => 'Fetch the requested row by relative position from the current position of the cursor in the result set. Valid only for scrollable cursors.', +'PDO::FETCH_PROPS_LATE' => 'Call the constructor before setting properties.', +'PDO::FETCH_SERIALIZE' => 'As `PDO::FETCH_INTO` but object is provided as a serialized string.', +'PDO::FETCH_UNIQUE' => 'Fetch only the unique values.', +'PDO::MYSQL_ATTR_COMPRESS' => 'Enable network communication compression. This is also supported when compiled against mysqlnd as of PHP 5.3.11.', +'PDO::MYSQL_ATTR_DIRECT_QUERY' => 'Perform direct queries, don\'t use prepared statements.', +'PDO::MYSQL_ATTR_FOUND_ROWS' => 'Return the number of found (matched) rows, not the number of changed rows.', +'PDO::MYSQL_ATTR_IGNORE_SPACE' => 'Permit spaces after function names. Makes all functions names reserved words.', +'PDO::MYSQL_ATTR_INIT_COMMAND' => 'Command to execute when connecting to the MySQL server. Will automatically be re-executed when reconnecting. + +Note, this constant can only be used in the driver_options array when constructing a new database handle.', +'PDO::MYSQL_ATTR_LOCAL_INFILE' => 'Enable `LOAD LOCAL INFILE`. + +Note, this constant can only be used in the driver_options array when constructing a new database handle.', +'PDO::MYSQL_ATTR_MAX_BUFFER_SIZE' => 'Maximum buffer size. Defaults to 1 MiB. This constant is not supported when compiled against mysqlnd.', +'PDO::MYSQL_ATTR_MULTI_STATEMENTS' => 'Disables multi query execution in both `PDO::prepare` and `PDO::query` when set to `false`. + +Note, this constant can only be used in the driver_options array when constructing a new database handle. + +`version.exists.asof` 5.5.21 and PHP 5.6.5.', +'PDO::MYSQL_ATTR_READ_DEFAULT_FILE' => 'Read options from the named option file instead of from my.cnf. This option is not available if mysqlnd is used, because mysqlnd does not read the mysql configuration files.', +'PDO::MYSQL_ATTR_READ_DEFAULT_GROUP' => 'Read options from the named group from my.cnf or the file specified with `MYSQL_READ_DEFAULT_FILE`. This option is not available if mysqlnd is used, because mysqlnd does not read the mysql configuration files.', +'PDO::MYSQL_ATTR_SSL_CA' => 'The file path to the SSL certificate authority. + +`version.exists.asof` 5.3.7.', +'PDO::MYSQL_ATTR_SSL_CAPATH' => 'The file path to the directory that contains the trusted SSL CA certificates, which are stored in PEM format. + +`version.exists.asof` 5.3.7.', +'PDO::MYSQL_ATTR_SSL_CERT' => 'The file path to the SSL certificate. + +`version.exists.asof` 5.3.7.', +'PDO::MYSQL_ATTR_SSL_CIPHER' => 'A list of one or more permissible ciphers to use for SSL encryption, in a format understood by OpenSSL. For example: `DHE-RSA-AES256-SHA:AES128-SHA` + +`version.exists.asof` 5.3.7.', +'PDO::MYSQL_ATTR_SSL_KEY' => 'The file path to the SSL key. + +`version.exists.asof` 5.3.7.', +'PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT' => 'Provides a way to disable verification of the server SSL certificate. + +`version.exists.asof` 7.0.18 and PHP 7.1.4.', +'PDO::MYSQL_ATTR_USE_BUFFERED_QUERY' => 'If this attribute is set to `true` on a `PDOStatement`, the MySQL driver will use the buffered versions of the MySQL API. If you\'re writing portable code, you should use `PDOStatement::fetchAll` instead.', +'PDO::OCI_ATTR_ACTION' => 'Provides a way to specify the action on the database session. + +`version.exists.asof` 7.2.16 and 7.3.3', +'PDO::OCI_ATTR_CLIENT_IDENTIFIER' => 'Provides a way to specify the client identifier on the database session. + +`version.exists.asof` 7.2.16 and 7.3.3', +'PDO::OCI_ATTR_CLIENT_INFO' => 'Provides a way to specify the client info on the database session. + +`version.exists.asof` 7.2.16 and 7.3.3', +'PDO::OCI_ATTR_MODULE' => 'Provides a way to specify the module on the database session. + +`version.exists.asof` 7.2.16 and 7.3.3', +'PDO::PARAM_BOOL' => 'Represents a boolean data type.', +'PDO::PARAM_EVT_ALLOC' => 'Allocation event', +'PDO::PARAM_EVT_EXEC_POST' => 'Event triggered subsequent to execution of a prepared statement.', +'PDO::PARAM_EVT_EXEC_PRE' => 'Event triggered prior to execution of a prepared statement.', +'PDO::PARAM_EVT_FETCH_POST' => 'Event triggered subsequent to fetching a result from a resultset.', +'PDO::PARAM_EVT_FETCH_PRE' => 'Event triggered prior to fetching a result from a resultset.', +'PDO::PARAM_EVT_FREE' => 'Deallocation event', +'PDO::PARAM_EVT_NORMALIZE' => 'Event triggered during bound parameter registration allowing the driver to normalize the parameter name.', +'PDO::PARAM_INPUT_OUTPUT' => 'Specifies that the parameter is an INOUT parameter for a stored procedure. You must bitwise-OR this value with an explicit PDO::PARAM_* data type.', +'PDO::PARAM_INT' => 'Represents the SQL INTEGER data type.', +'PDO::PARAM_LOB' => 'Represents the SQL large object data type.', +'PDO::PARAM_NULL' => 'Represents the SQL NULL data type.', +'PDO::PARAM_STMT' => 'Represents a recordset type. Not currently supported by any drivers.', +'PDO::PARAM_STR' => 'Represents the SQL CHAR, VARCHAR, or other string data type.', +'PDO::PARAM_STR_CHAR' => 'Flag to denote a string uses the regular character set. + +Available since PHP 7.2.0', +'PDO::PARAM_STR_NATL' => 'Flag to denote a string uses the national character set. + +Available since PHP 7.2.0', +'PDO::SQLITE_DETERMINISTIC' => 'Specifies that a function created with `PDO::sqliteCreateFunction` is deterministic, i.e. it always returns the same result given the same inputs within a single SQL statement. (Available as of PHP 7.1.4.)', +'PDO::SQLSRV_ATTR_DIRECT_QUERY' => 'Indicates that a query should be executed directly, without being prepared. This constant can be passed to PDO::setAttribute, and PDO::prepare. For more information, see Direct and Prepared Statement Execution.', +'PDO::SQLSRV_ATTR_QUERY_TIMEOUT' => 'A non-negative integer representing the timeout period, in seconds. Zero (0) is the default and means no timeout. This constant can be passed to PDOStatement::setAttribute, PDO::setAttribute, and PDO::prepare.', +'PDO::SQLSRV_ENCODING_BINARY' => 'Specifies that data is sent/retrieved as a raw byte stream to/from the server without performing encoding or translation. This constant can be passed to PDOStatement::setAttribute, PDO::prepare, PDOStatement::bindColumn, and PDOStatement::bindParam.', +'PDO::SQLSRV_ENCODING_DEFAULT' => 'Specifies that data is sent/retrieved to/from the server according to PDO::SQLSRV_ENCODING_SYSTEM if specified during connection. The connection\'s encoding is used if specified in a prepare statement. This constant can be passed to PDOStatement::setAttribute, PDO::setAttribute, PDO::prepare, PDOStatement::bindColumn, and PDOStatement::bindParam.', +'PDO::SQLSRV_ENCODING_SYSTEM' => 'Specifies that data is sent/retrieved to/from the server as 8-bit characters as specified in the code page of the Windows locale that is set on the system. Any multi-byte characters or characters that do not map into this code page are substituted with a single byte question mark (?) character. This constant can be passed to PDOStatement::setAttribute, PDO::setAttribute, PDO::prepare, PDOStatement::bindColumn, and PDOStatement::bindParam.', +'PDO::SQLSRV_ENCODING_UTF8' => 'Specifies that data is sent/retrieved to/from the server in UTF-8 encoding. This is the default encoding. This constant can be passed to PDOStatement::setAttribute, PDO::setAttribute, PDO::prepare, PDOStatement::bindColumn, and PDOStatement::bindParam.', +'PDO::SQLSRV_TXN_READ_COMMITTED' => 'This constant is an acceptable value for the SQLSRV DSN key TransactionIsolation. This constant sets the transaction isolation level for the connection to Read Committed.', +'PDO::SQLSRV_TXN_READ_UNCOMMITTED' => 'This constant is an acceptable value for the SQLSRV DSN key TransactionIsolation. This constant sets the transaction isolation level for the connection to Read Uncommitted.', +'PDO::SQLSRV_TXN_REPEATABLE_READ' => 'This constant is an acceptable value for the SQLSRV DSN key TransactionIsolation. This constant sets the transaction isolation level for the connection to Repeateable Read.', +'PDO::SQLSRV_TXN_SERIALIZABLE' => 'This constant is an acceptable value for the SQLSRV DSN key TransactionIsolation. This constant sets the transaction isolation level for the connection to Serializable.', +'PDO::SQLSRV_TXN_SNAPSHOT' => 'This constant is an acceptable value for the SQLSRV DSN key TransactionIsolation. This constant sets the transaction isolation level for the connection to Snapshot.', +'PGSQL_ASSOC' => 'Passed to `pg_fetch_array`. Return an associative array of field names and values.', +'PGSQL_BAD_RESPONSE' => 'Returned by `pg_result_status`. The server\'s response was not understood.', +'PGSQL_BOTH' => 'Passed to `pg_fetch_array`. Return an array of field values that is both numerically indexed (by field number) and associated (by field name).', +'PGSQL_COMMAND_OK' => 'Returned by `pg_result_status`. Successful completion of a command returning no data.', +'PGSQL_CONNECT_ASYNC' => 'Passed to `pg_connect` to create an asynchronous connection. Added in PHP 5.6.0.', +'PGSQL_CONNECT_FORCE_NEW' => 'Passed to `pg_connect` to force the creation of a new connection, rather than re-using an existing identical connection.', +'PGSQL_CONNECTION_BAD' => 'Returned by `pg_connection_status` indicating that the database connection is in an invalid state.', +'PGSQL_CONNECTION_OK' => 'Returned by `pg_connection_status` indicating that the database connection is in a valid state.', +'PGSQL_CONV_FORCE_NULL' => 'Passed to `pg_convert`. Use SQL `NULL` in place of an empty `string`.', +'PGSQL_CONV_IGNORE_DEFAULT' => 'Passed to `pg_convert`. Ignore default values in the table during conversion.', +'PGSQL_CONV_IGNORE_NOT_NULL' => 'Passed to `pg_convert`. Ignore conversion of `null` into SQL `NOT NULL` columns.', +'PGSQL_COPY_IN' => 'Returned by `pg_result_status`. Copy In (to server) data transfer started.', +'PGSQL_COPY_OUT' => 'Returned by `pg_result_status`. Copy Out (from server) data transfer started.', +'PGSQL_DIAG_CONTEXT' => 'Passed to `pg_result_error_field`. An indication of the context in which the error occurred. Presently this includes a call stack traceback of active procedural language functions and internally-generated queries. The trace is one entry per line, most recent first.', +'PGSQL_DIAG_INTERNAL_POSITION' => 'Passed to `pg_result_error_field`. This is defined the same as the `PG_DIAG_STATEMENT_POSITION` field, but it is used when the cursor position refers to an internally generated command rather than the one submitted by the client. The `PG_DIAG_INTERNAL_QUERY` field will always appear when this field appears.', +'PGSQL_DIAG_INTERNAL_QUERY' => 'Passed to `pg_result_error_field`. The text of a failed internally-generated command. This could be, for example, a SQL query issued by a PL/pgSQL function.', +'PGSQL_DIAG_MESSAGE_DETAIL' => 'Passed to `pg_result_error_field`. Detail: an optional secondary error message carrying more detail about the problem. May run to multiple lines.', +'PGSQL_DIAG_MESSAGE_HINT' => 'Passed to `pg_result_error_field`. Hint: an optional suggestion what to do about the problem. This is intended to differ from detail in that it offers advice (potentially inappropriate) rather than hard facts. May run to multiple lines.', +'PGSQL_DIAG_MESSAGE_PRIMARY' => 'Passed to `pg_result_error_field`. The primary human-readable error message (typically one line). Always present.', +'PGSQL_DIAG_SEVERITY' => 'Passed to `pg_result_error_field`. The severity; the field contents are `ERROR`, `FATAL`, or `PANIC` (in an error message), or `WARNING`, `NOTICE`, `DEBUG`, `INFO`, or `LOG` (in a notice message), or a localized translation of one of these. Always present.', +'PGSQL_DIAG_SEVERITY_NONLOCALIZED' => 'The severity; the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message). This is identical to the PG_DIAG_SEVERITY field except that the contents are never localized. This is present only in versions 9.6 and later.', +'PGSQL_DIAG_SOURCE_FILE' => 'Passed to `pg_result_error_field`. The file name of the PostgreSQL source-code location where the error was reported.', +'PGSQL_DIAG_SOURCE_FUNCTION' => 'Passed to `pg_result_error_field`. The name of the PostgreSQL source-code function reporting the error.', +'PGSQL_DIAG_SOURCE_LINE' => 'Passed to `pg_result_error_field`. The line number of the PostgreSQL source-code location where the error was reported.', +'PGSQL_DIAG_SQLSTATE' => 'Passed to `pg_result_error_field`. The SQLSTATE code for the error. The SQLSTATE code identifies the type of error that has occurred; it can be used by front-end applications to perform specific operations (such as error handling) in response to a particular database error. This field is not localizable, and is always present.', +'PGSQL_DIAG_STATEMENT_POSITION' => 'Passed to `pg_result_error_field`. A string containing a decimal integer indicating an error cursor position as an index into the original statement string. The first character has index 1, and positions are measured in characters not bytes.', +'PGSQL_DML_ASYNC' => 'Passed to `pg_insert`, `pg_select`, `pg_update` and `pg_delete`. Execute asynchronous query by these functions.', +'PGSQL_DML_ESCAPE' => 'Passed to `pg_insert`, `pg_select`, `pg_update` and `pg_delete`. Apply escape to all parameters instead of calling `pg_convert` internally. This option omits meta data look up. Query could be as fast as `pg_query` and `pg_send_query`.', +'PGSQL_DML_EXEC' => 'Passed to `pg_insert`, `pg_select`, `pg_update` and `pg_delete`. Execute query by these functions.', +'PGSQL_DML_NO_CONV' => 'Passed to `pg_insert`, `pg_select`, `pg_update` and `pg_delete`. All parameters passed as is. Manual escape is required if parameters contain user supplied data. Use `pg_escape_string` for it.', +'PGSQL_DML_STRING' => 'Passed to `pg_insert`, `pg_select`, `pg_update` and `pg_delete`. Return executed query string.', +'PGSQL_EMPTY_QUERY' => 'Returned by `pg_result_status`. The string sent to the server was empty.', +'PGSQL_ERRORS_DEFAULT' => 'Passed to `pg_set_error_verbosity`. The default mode produces messages that include the above plus any detail, hint, or context fields (these may span multiple lines).', +'PGSQL_ERRORS_TERSE' => 'Passed to `pg_set_error_verbosity`. Specified that returned messages include severity, primary text, and position only; this will normally fit on a single line.', +'PGSQL_ERRORS_VERBOSE' => 'Passed to `pg_set_error_verbosity`. The verbose mode includes all available fields.', +'PGSQL_FATAL_ERROR' => 'Returned by `pg_result_status`. A fatal error occurred.', +'PGSQL_LIBPQ_VERSION' => 'Short libpq version that contains only numbers and dots.', +'PGSQL_LIBPQ_VERSION_STR' => 'Long libpq version that includes compiler information.', +'PGSQL_NONFATAL_ERROR' => 'Returned by `pg_result_status`. A nonfatal error (a notice or warning) occurred.', +'PGSQL_NOTICE_ALL' => 'Used by `pg_last_notice`. Available since PHP 7.1.0.', +'PGSQL_NOTICE_CLEAR' => 'Used by `pg_last_notice`. Available since PHP 7.1.0.', +'PGSQL_NOTICE_LAST' => 'Used by `pg_last_notice`. Available since PHP 7.1.0.', +'PGSQL_NUM' => 'Passed to `pg_fetch_array`. Return a numerically indexed array of field numbers and values.', +'PGSQL_POLLING_ACTIVE' => 'Returned by `pg_connect_poll` to indicate that the connection is currently active.', +'PGSQL_POLLING_FAILED' => 'Returned by `pg_connect_poll` to indicate that the connection attempt failed.', +'PGSQL_POLLING_OK' => 'Returned by `pg_connect_poll` to indicate that the connection is ready to be used.', +'PGSQL_POLLING_READING' => 'Returned by `pg_connect_poll` to indicate that the connection is waiting for the PostgreSQL socket to be readable.', +'PGSQL_POLLING_WRITING' => 'Returned by `pg_connect_poll` to indicate that the connection is waiting for the PostgreSQL socket to be writable.', +'PGSQL_SEEK_CUR' => 'Passed to `pg_lo_seek`. Seek operation is to begin from the current position.', +'PGSQL_SEEK_END' => 'Passed to `pg_lo_seek`. Seek operation is to begin from the end of the object.', +'PGSQL_SEEK_SET' => 'Passed to `pg_lo_seek`. Seek operation is to begin from the start of the object.', +'PGSQL_STATUS_LONG' => 'Passed to `pg_result_status`. Indicates that numerical result code is desired.', +'PGSQL_STATUS_STRING' => 'Passed to `pg_result_status`. Indicates that textual result command tag is desired.', +'PGSQL_TRANSACTION_ACTIVE' => 'Returned by `pg_transaction_status`. A command is in progress on the connection. A query has been sent via the connection and not yet completed.', +'PGSQL_TRANSACTION_IDLE' => 'Returned by `pg_transaction_status`. Connection is currently idle, not in a transaction.', +'PGSQL_TRANSACTION_INERROR' => 'Returned by `pg_transaction_status`. The connection is idle, in a failed transaction block.', +'PGSQL_TRANSACTION_INTRANS' => 'Returned by `pg_transaction_status`. The connection is idle, in a transaction block.', +'PGSQL_TRANSACTION_UNKNOWN' => 'Returned by `pg_transaction_status`. The connection is bad.', +'PGSQL_TUPLES_OK' => 'Returned by `pg_result_status`. Successful completion of a command returning data (such as a `SELECT` or `SHOW`).', +'PHP_BINARY' => 'Specifies the PHP binary path during script execution. Available since PHP 5.4.', +'PHP_BINDIR' => 'Specifies where the binaries were installed into.', +'PHP_DEBUG' => 'Available since PHP 5.2.7.', +'PHP_EOL' => 'The correct \'End Of Line\' symbol for this platform.', +'PHP_EXTRA_VERSION' => 'The current PHP "extra" version as a string (e.g., \'-extra\' from version "5.2.7-extra"). Often used by distribution vendors to indicate a package version.', +'PHP_FD_SETSIZE' => 'The maximum number of file descriptors for select system calls. Available as of PHP 7.1.0.', +'PHP_FLOAT_DIG' => 'Number of decimal digits that can be rounded into a float and back without precision loss. Available as of PHP 7.2.0.', +'PHP_FLOAT_EPSILON' => 'Smallest representable positive number x, so that `x + 1.0 != 1.0`. Available as of PHP 7.2.0.', +'PHP_FLOAT_MAX' => 'Largest representable floating point number. Available as of PHP 7.2.0.', +'PHP_FLOAT_MIN' => 'Smallest representable floating point number. Available as of PHP 7.2.0.', +'PHP_INT_MAX' => 'The largest integer supported in this build of PHP. Usually int(2147483647) in 32 bit systems and int(9223372036854775807) in 64 bit systems.', +'PHP_INT_MIN' => 'The smallest integer supported in this build of PHP. Usually int(-2147483648) in 32 bit systems and int(-9223372036854775808) in 64 bit systems. Available since PHP 7.0.0. Usually, PHP_INT_MIN === ~PHP_INT_MAX.', +'PHP_INT_SIZE' => 'The size of an integer in bytes in this build of PHP.', +'PHP_MAJOR_VERSION' => 'The current PHP "major" version as an integer (e.g., int(5) from version "5.2.7-extra").', +'PHP_MANDIR' => 'Specifies where the manpages were installed into.', +'PHP_MAXPATHLEN' => 'The maximum length of filenames (including path) supported by this build of PHP.', +'PHP_MINOR_VERSION' => 'The current PHP "minor" version as an integer (e.g., int(2) from version "5.2.7-extra").', +'PHP_OS' => 'The operating system PHP was built for.', +'PHP_OS_FAMILY' => 'The operating system family PHP was built for. Either of `\'Windows\'`, `\'BSD\'`, `\'Darwin\'`, `\'Solaris\'`, `\'Linux\'` or `\'Unknown\'`. Available as of PHP 7.2.0.', +'PHP_OUTPUT_HANDLER_CLEAN' => 'Indicates that the output buffer has been cleaned. + +Available since PHP 5.4.', +'PHP_OUTPUT_HANDLER_CLEANABLE' => 'Controls whether an output buffer created by `ob_start` can be cleaned. + +Available since PHP 5.4.', +'PHP_OUTPUT_HANDLER_CONT' => 'Indicates that the buffer has been flushed, but output buffering will continue. + +As of PHP 5.4, this is an alias for `PHP_OUTPUT_HANDLER_WRITE`.', +'PHP_OUTPUT_HANDLER_END' => 'Indicates that output buffering has ended. + +As of PHP 5.4, this is an alias for `PHP_OUTPUT_HANDLER_FINAL`.', +'PHP_OUTPUT_HANDLER_FINAL' => 'Indicates that this is the final output buffering operation. + +Available since PHP 5.4.', +'PHP_OUTPUT_HANDLER_FLUSH' => 'Indicates that the buffer has been flushed. + +Available since PHP 5.4.', +'PHP_OUTPUT_HANDLER_FLUSHABLE' => 'Controls whether an output buffer created by `ob_start` can be flushed. + +Available since PHP 5.4.', +'PHP_OUTPUT_HANDLER_REMOVABLE' => 'Controls whether an output buffer created by `ob_start` can be removed before the end of the script. + +Available since PHP 5.4.', +'PHP_OUTPUT_HANDLER_START' => 'Indicates that output buffering has begun.', +'PHP_OUTPUT_HANDLER_STDFLAGS' => 'The default set of output buffer flags; currently equivalent to `PHP_OUTPUT_HANDLER_CLEANABLE` | `PHP_OUTPUT_HANDLER_FLUSHABLE` | `PHP_OUTPUT_HANDLER_REMOVABLE`. + +Available since PHP 5.4.', +'PHP_OUTPUT_HANDLER_WRITE' => 'Indicates that the output buffer is being flushed, and had data to output. + +Available since PHP 5.4.', +'PHP_PREFIX' => 'The value "--prefix" was set to at configure.', +'PHP_QUERY_RFC1738' => 'Encoding is performed per RFC 1738 and the `application/x-www-form-urlencoded` media type, which implies that spaces are encoded as plus (`+`) signs.', +'PHP_QUERY_RFC3986' => 'Encoding is performed according to RFC 3986, and spaces will be percent encoded (`%20`).', +'PHP_RELEASE_VERSION' => 'The current PHP "release" version as an integer (e.g., int(7) from version "5.2.7-extra").', +'PHP_SAPI' => 'The Server API for this build of PHP. See also `php_sapi_name`.', +'PHP_SESSION_ACTIVE' => 'Since PHP 5.4.0. Return value of `session_status` if sessions are enabled, and a session exists.', +'PHP_SESSION_DISABLED' => 'Since PHP 5.4.0. Return value of `session_status` if sessions are disabled.', +'PHP_SESSION_NONE' => 'Since PHP 5.4.0. Return value of `session_status` if sessions are enabled, but no session exists.', +'PHP_SHLIB_SUFFIX' => 'The build-platform\'s shared library suffix, such as "so" (most Unixes) or "dll" (Windows).', +'PHP_SVN_AUTH_PARAM_IGNORE_SSL_VERIFY_ERRORS' => 'Custom property for ignoring SSL cert verification errors', +'PHP_URL_FRAGMENT' => 'Outputs the fragment (string after the hashmark #) of the URL parsed.', +'PHP_URL_HOST' => 'Outputs the hostname of the URL parsed.', +'PHP_URL_PASS' => 'Outputs the password of the URL parsed.', +'PHP_URL_PATH' => 'Outputs the path of the URL parsed.', +'PHP_URL_PORT' => 'Outputs the port of the URL parsed.', +'PHP_URL_QUERY' => 'Outputs the query string of the URL parsed.', +'PHP_URL_USER' => 'Outputs the user of the URL parsed.', +'PHP_VERSION' => 'The current PHP version as a string in "major.minor.release[extra]" notation.', +'PHP_VERSION_ID' => 'The current PHP version as an integer, useful for version comparisons (e.g., int(50207) from version "5.2.7-extra").', +'PHP_WINDOWS_EVENT_CTRL_BREAK' => 'A Windows `CTRL+BREAK` event. Available as of PHP 7.4.0 (Windows only).', +'PHP_WINDOWS_EVENT_CTRL_C' => 'A Windows `CTRL+C` event. Available as of PHP 7.4.0 (Windows only).', +'PHP_ZTS' => 'Available since PHP 5.2.7.', +'PHPDBG_FILE' => 'Removed as of PHP 7.3.0.', +'PHPDBG_FUNC' => 'Removed as of PHP 7.3.0.', +'PHPDBG_LINENO' => 'Removed as of PHP 7.3.0.', +'PHPDBG_METHOD' => 'Removed as of PHP 7.3.0.', +'POLL_ERR' => 'Available since PHP 5.3.0.', +'POLL_HUP' => 'Available since PHP 5.3.0.', +'POLL_IN' => 'Available since PHP 5.3.0.', +'POLL_MSG' => 'Available since PHP 5.3.0.', +'POLL_OUT' => 'Available since PHP 5.3.0.', +'POLL_PRI' => 'Available since PHP 5.3.0.', +'POSIX_F_OK' => 'Check whether the file exists.', +'POSIX_R_OK' => 'Check whether the file exists and has read permissions.', +'POSIX_RLIMIT_AS' => 'The maximum size of the process\'s address space in bytes. See also PHP\'s memory_limit configuration directive.', +'POSIX_RLIMIT_CORE' => 'The maximum size of a core file. If the limit is set to 0, no core file will be generated.', +'POSIX_RLIMIT_CPU' => 'The maximum amount of CPU time that the process can use, in seconds. When the soft limit is hit, a `SIGXCPU` signal will be sent, which can be caught with `pcntl_signal`. Depending on the operating system, additional `SIGXCPU` signals may be sent each second until the hard limit is hit, at which point an uncatchable `SIGKILL` signal is sent. + +See also `set_time_limit`.', +'POSIX_RLIMIT_DATA' => 'The maximum size of the process\'s data segment, in bytes. It is extremely unlikely that this will have any effect on the execution of PHP unless an extension is in use that calls `brk` or `sbrk`.', +'POSIX_RLIMIT_FSIZE' => 'The maximum size of files that the process can create, in bytes.', +'POSIX_RLIMIT_INFINITY' => 'Used to indicate an infinite value for a resource limit.', +'POSIX_RLIMIT_LOCKS' => 'The maximum number of locks that the process can create. This is only supported on extremely old Linux kernels.', +'POSIX_RLIMIT_MEMLOCK' => 'The maximum number of bytes that can be locked into memory.', +'POSIX_RLIMIT_MSGQUEUE' => 'The maximum number of bytes that can be allocated for POSIX message queues. PHP does not ship with support for POSIX message queues, so this limit will not have any effect unless you are using an extension that implements that support.', +'POSIX_RLIMIT_NICE' => 'The maximum value to which the process can be reniced to. The value that will be used will be `20 - limit`, as resource limit values cannot be negative.', +'POSIX_RLIMIT_NOFILE' => 'A value one greater than the maximum file descriptor number that can be opened by this process.', +'POSIX_RLIMIT_NPROC' => 'The maximum number of processes (and/or threads, on some operating systems) that can be created for the real user ID of the process.', +'POSIX_RLIMIT_RSS' => 'The maximum size of the process\'s resident set, in pages.', +'POSIX_RLIMIT_RTPRIO' => 'The maximum real time priority that can be set via the `sched_setscheduler` and `sched_setparam` system calls.', +'POSIX_RLIMIT_RTTIME' => 'The maximum amount of CPU time, in microseconds, that the process can consume without making a blocking system call if it is using real time scheduling.', +'POSIX_RLIMIT_SIGPENDING' => 'The maximum number of signals that can be queued for the real user ID of the process.', +'POSIX_RLIMIT_STACK' => 'The maximum size of the process stack, in bytes.', +'POSIX_S_IFBLK' => 'Block special file', +'POSIX_S_IFCHR' => 'Character special file', +'POSIX_S_IFIFO' => 'FIFO (named pipe) special file', +'POSIX_S_IFREG' => 'Normal file', +'POSIX_S_IFSOCK' => 'Socket', +'POSIX_W_OK' => 'Check whether the file exists and has write permissions.', +'POSIX_X_OK' => 'Check whether the file exists and has execute permissions.', +'PTHREADS_ALLOW_HEADERS' => 'Allow new Threads to send headers to standard output (normally prohibited)', +'PTHREADS_INHERIT_ALL' => 'The default options for all Threads, causes pthreads to copy the environment when new Threads are started', +'PTHREADS_INHERIT_CLASSES' => 'Inherit user declared classes when new Threads are started', +'PTHREADS_INHERIT_COMMENTS' => 'Inherit all comments when new Threads are started', +'PTHREADS_INHERIT_CONSTANTS' => 'Inherit user declared constants when new Threads are started', +'PTHREADS_INHERIT_FUNCTIONS' => 'Inherit user declared functions when new Threads are started', +'PTHREADS_INHERIT_INCLUDES' => 'Inherit included file information when new Threads are started', +'PTHREADS_INHERIT_INI' => 'Inherit INI entries when new Threads are started', +'PTHREADS_INHERIT_NONE' => 'Do not inherit anything when new Threads are started', +'RADIUS_ACCESS_ACCEPT' => 'An Access-Accept response to an Access-Request indicating that the RADIUS server authenticated the user successfully.', +'RADIUS_ACCESS_CHALLENGE' => 'An Access-Challenge response to an Access-Request indicating that the RADIUS server requires further information in another Access-Request before authenticating the user.', +'RADIUS_ACCESS_REJECT' => 'An Access-Reject response to an Access-Request indicating that the RADIUS server could not authenticate the user.', +'RADIUS_ACCESS_REQUEST' => 'An Access-Request, used to authenticate a user against a RADIUS server. Access request packets must include a `RADIUS_NAS_IP_ADDRESS` or a `RADIUS_NAS_IDENTIFIER` attribute, must also include a `RADIUS_USER_PASSWORD`, `RADIUS_CHAP_PASSWORD` or a `RADIUS_STATE` attribute, and should include a `RADIUS_USER_NAME` attribute.', +'RADIUS_ACCOUNTING_REQUEST' => 'An Accounting-Request, used to convey accounting information for a service to the RADIUS server.', +'RADIUS_ACCOUNTING_RESPONSE' => 'An Accounting-Response response to an Accounting-Request.', +'RADIUS_ACCT_AUTHENTIC' => 'Accounting authentic, one of: `RADIUS_AUTH_RADIUS` `RADIUS_AUTH_LOCAL` `RADIUS_AUTH_REMOTE`', +'RADIUS_ACCT_DELAY_TIME' => 'Accounting delay time', +'RADIUS_ACCT_INPUT_OCTETS' => 'Accounting input bytes', +'RADIUS_ACCT_INPUT_PACKETS' => 'Accounting input packets', +'RADIUS_ACCT_LINK_COUNT' => 'Accounting link count', +'RADIUS_ACCT_MULTI_SESSION_ID' => 'Accounting multi session ID', +'RADIUS_ACCT_OUTPUT_OCTETS' => 'Accounting output bytes', +'RADIUS_ACCT_OUTPUT_PACKETS' => 'Accounting output packets', +'RADIUS_ACCT_SESSION_ID' => 'Accounting session ID', +'RADIUS_ACCT_SESSION_TIME' => 'Accounting session time', +'RADIUS_ACCT_STATUS_TYPE' => 'Accounting status type, one of: `RADIUS_START` `RADIUS_STOP` `RADIUS_ACCOUNTING_ON` `RADIUS_ACCOUNTING_OFF`', +'RADIUS_ACCT_TERMINATE_CAUSE' => 'Accounting terminate cause, one of: `RADIUS_TERM_USER_REQUEST` `RADIUS_TERM_LOST_CARRIER` `RADIUS_TERM_LOST_SERVICE` `RADIUS_TERM_IDLE_TIMEOUT` `RADIUS_TERM_SESSION_TIMEOUT` `RADIUS_TERM_ADMIN_RESET` `RADIUS_TERM_ADMIN_REBOOT` `RADIUS_TERM_PORT_ERROR` `RADIUS_TERM_NAS_ERROR` `RADIUS_TERM_NAS_REQUEST` `RADIUS_TERM_NAS_REBOOT` `RADIUS_TERM_PORT_UNNEEDED` `RADIUS_TERM_PORT_PREEMPTED` `RADIUS_TERM_PORT_SUSPENDED` `RADIUS_TERM_SERVICE_UNAVAILABLE` `RADIUS_TERM_CALLBACK` `RADIUS_TERM_USER_ERROR` `RADIUS_TERM_HOST_REQUEST`', +'RADIUS_CALLBACK_ID' => 'The Callback-Id attribute. The attribute value is a `string` containing an implementation-specific name of the place to be called.', +'RADIUS_CALLBACK_NUMBER' => 'The Callback-Number attribute. The attribute value is a `string` containing the dialing string to use for callback.', +'RADIUS_CALLED_STATION_ID' => 'Called Station Id', +'RADIUS_CALLING_STATION_ID' => 'Calling Station Id', +'RADIUS_CHAP_CHALLENGE' => 'Challenge', +'RADIUS_CHAP_PASSWORD' => 'The Chap-Password attribute. The attribute value is expected to be a `string` with the first byte containing the CHAP identifier, and the subsequent 16 bytes containing the MD5 hash of the CHAP identifier, the plaintext password and the CHAP challenge value concatenated together. Note that the CHAP challenge value should also be sent separately in a `RADIUS_CHAP_CHALLENGE` attribute. + +Using CHAP passwords', +'RADIUS_CLASS' => 'The Class attribute. The attribute value is an arbitrary `string` included in an Access-Accept message that should then be sent to the accounting server in Accounting-Request messages, and can be set using `radius_put_attr`.', +'RADIUS_COA_ACK' => 'A CoA-ACK, sent to the RADIUS server to indicate that the user authorisations have been updated. + +This constant is available in PECL radius 1.3.0 and later.', +'RADIUS_COA_NAK' => 'A CoA-NAK, sent to the RADIUS server to indicate that the user authorisations could not be updated. + +This constant is available in PECL radius 1.3.0 and later.', +'RADIUS_COA_REQUEST' => 'A CoA-Request, sent from the RADIUS server to indicate that the authorisations within the user session have changed. A response must be sent in the form of a CoA-ACK or a CoA-NAK. + +This constant is available in PECL radius 1.3.0 and later.', +'RADIUS_CONNECT_INFO' => 'Connect info', +'RADIUS_DISCONNECT_ACK' => 'A Disconnect-ACK, sent to the RADIUS server to indicate that the user session has been terminated. + +This constant is available in PECL radius 1.3.0 and later.', +'RADIUS_DISCONNECT_NAK' => 'A Disconnect-NAK, sent to the RADIUS server to indicate that the user session could not be terminated. + +This constant is available in PECL radius 1.3.0 and later.', +'RADIUS_DISCONNECT_REQUEST' => 'A Disconnect-Request, sent from the RADIUS server to indicate that the user session must be terminated. + +This constant is available in PECL radius 1.3.0 and later.', +'RADIUS_FILTER_ID' => 'The Filter-ID attribute. The attribute value is expected to be an implementation-specific, human-readable `string` of filters, which can be set using `radius_put_attr`.', +'RADIUS_FRAMED_APPLETALK_LINK' => 'Framed Appletalk Link', +'RADIUS_FRAMED_APPLETALK_NETWORK' => 'Framed Appletalk Network', +'RADIUS_FRAMED_APPLETALK_ZONE' => 'Framed Appletalk Zone', +'RADIUS_FRAMED_COMPRESSION' => 'The Framed-Compression attribute. The attribute value is expected to be an `integer` indicating the compression protocol to be used, and can be set using `radius_put_int`. Possible values include these constants: `RADIUS_COMP_NONE`: No compression `RADIUS_COMP_VJ`: VJ TCP/IP header compression `RADIUS_COMP_IPXHDR`: IPX header compression `RADIUS_COMP_STAC_LZS`: Stac-LZS compression (added in PECL radius 1.3.0b2)', +'RADIUS_FRAMED_IP_ADDRESS' => 'The Framed-IP-Address attribute. The attribute value is expected to be the address of the user\'s network encoded as an `integer`, which can be set using `radius_put_addr` and retrieved using `radius_cvt_addr`.', +'RADIUS_FRAMED_IP_NETMASK' => 'The Framed-IP-Netmask attribute. The attribute value is expected to be the netmask of the user\'s network encoded as an `integer`, which can be set using `radius_put_addr` and retrieved using `radius_cvt_addr`.', +'RADIUS_FRAMED_IPX_NETWORK' => 'The Framed-IPX-Network attribute. The attribute value is an `integer` containing the IPX network to be configured for the user, or `0xFFFFFFFE` to indicate that the RADIUS client should select the network, and can be accessed via `radius_cvt_int`.', +'RADIUS_FRAMED_MTU' => 'The Framed-MTU attribute. The attribute value is expected to be an `integer` indicating the MTU to be configured for the user, and can be set using `radius_put_int`.', +'RADIUS_FRAMED_PROTOCOL' => 'The Framed-Protocol attribute. The attribute value is expected to be an `integer` indicating the framing to be used for framed access, and can be set using `radius_put_int`. The possible attribute values include these constants: `RADIUS_PPP` `RADIUS_SLIP` `RADIUS_ARAP` `RADIUS_GANDALF` `RADIUS_XYLOGICS`', +'RADIUS_FRAMED_ROUTE' => 'The Framed-Route attribute. The attribute value is a `string` containing an implementation-specific set of routes to be configured for the user.', +'RADIUS_FRAMED_ROUTING' => 'The Framed-Routing attribute. The attribute value is expected to be an `integer` indicating the routing method for the user, which can be set using `radius_put_int`. + +Possible values include: `0`: No routing `1`: Send routing packets `2`: Listen for routing packets `3`: Send and listen', +'RADIUS_IDLE_TIMEOUT' => 'Idle timeout', +'RADIUS_LOGIN_IP_HOST' => 'The Login-IP-Host attribute. The attribute value is expected to the IP address to connect the user to, encoded as an `integer`, which can be set using `radius_put_addr`.', +'RADIUS_LOGIN_LAT_GROUP' => 'Login LAT Group', +'RADIUS_LOGIN_LAT_NODE' => 'Login LAT Node', +'RADIUS_LOGIN_LAT_PORT' => 'Login LAT Port', +'RADIUS_LOGIN_LAT_SERVICE' => 'Login LAT Service', +'RADIUS_LOGIN_SERVICE' => 'The Login-Service attribute. The attribute value is an `integer` indicating the service to connect the user to on the login host. The value can be converted to a PHP integer via `radius_cvt_int`.', +'RADIUS_LOGIN_TCP_PORT' => 'The Login-TCP-Port attribute. The attribute value is an `integer` indicating the port to connect the user to on the login host. The value can be converted to a PHP integer via `radius_cvt_int`.', +'RADIUS_MPPE_KEY_LEN' => 'The maximum length of MPPE keys.', +'RADIUS_NAS_IDENTIFIER' => 'NAS ID', +'RADIUS_NAS_IP_ADDRESS' => 'The NAS-IP-Address attribute. The attribute value is expected to the IP address of the RADIUS client encoded as an `integer`, which can be set using `radius_put_addr`.', +'RADIUS_NAS_PORT' => 'The NAS-Port attribute. The attribute value is expected to be the physical port of the user on the RADIUS client encoded as an `integer`, which can be set using `radius_put_int`.', +'RADIUS_NAS_PORT_TYPE' => 'NAS port type, one of: `RADIUS_ASYNC` `RADIUS_SYNC` `RADIUS_ISDN_SYNC` `RADIUS_ISDN_ASYNC_V120` `RADIUS_ISDN_ASYNC_V110` `RADIUS_VIRTUAL` `RADIUS_PIAFS` `RADIUS_HDLC_CLEAR_CHANNEL` `RADIUS_X_25` `RADIUS_X_75` `RADIUS_G_3_FAX` `RADIUS_SDSL` `RADIUS_ADSL_CAP` `RADIUS_ADSL_DMT` `RADIUS_IDSL` `RADIUS_ETHERNET` `RADIUS_XDSL` `RADIUS_CABLE` `RADIUS_WIRELESS_OTHER` `RADIUS_WIRELESS_IEEE_802_11`', +'RADIUS_OPTION_SALT' => 'When set, this option will result in the attribute value being salt-encrypted.', +'RADIUS_OPTION_TAGGED' => 'When set, this option will result in the attribute value being tagged with the value of the tag parameter.', +'RADIUS_PORT_LIMIT' => 'Port Limit', +'RADIUS_PROXY_STATE' => 'Proxy State', +'RADIUS_REPLY_MESSAGE' => 'The Reply-Message attribute. The attribute value is a `string` containing text that may be displayed to the user in response to an access request.', +'RADIUS_SERVICE_TYPE' => 'The Service-Type attribute. The attribute value indicates the service type the user is requesting, and is expected to be an `integer`, which can be set using `radius_put_int`. + +A number of constants are provided to represent the possible values of this attribute. They include: `RADIUS_LOGIN` `RADIUS_FRAMED` `RADIUS_CALLBACK_LOGIN` `RADIUS_CALLBACK_FRAMED` `RADIUS_OUTBOUND` `RADIUS_ADMINISTRATIVE` `RADIUS_NAS_PROMPT` `RADIUS_AUTHENTICATE_ONLY` `RADIUS_CALLBACK_NAS_PROMPT`', +'RADIUS_SESSION_TIMEOUT' => 'Session timeout', +'RADIUS_STATE' => 'The State attribute. The attribute value is an implementation-defined `string` included in an Access-Challenge from a server that must be included in the subsequent Access-Request, and can be set using `radius_put_attr`.', +'RADIUS_TERMINATION_ACTION' => 'Termination action', +'RADIUS_USER_NAME' => 'The User-Name attribute. The attribute value is expected to be a `string` containing the name of the user being authenticated, and can be set using `radius_put_attr`.', +'RADIUS_USER_PASSWORD' => 'The User-Password attribute. The attribute value is expected to be a `string` containing the user\'s password, and can be set using `radius_put_attr`. This value will be obfuscated on transmission as described in section 5.2 of RFC 2865.', +'RADIUS_VENDOR_MICROSOFT' => 'Microsoft specific vendor attributes (RFC 2548), one of: `RADIUS_MICROSOFT_MS_CHAP_RESPONSE` `RADIUS_MICROSOFT_MS_CHAP_ERROR` `RADIUS_MICROSOFT_MS_CHAP_PW_1` `RADIUS_MICROSOFT_MS_CHAP_PW_2` `RADIUS_MICROSOFT_MS_CHAP_LM_ENC_PW` `RADIUS_MICROSOFT_MS_CHAP_NT_ENC_PW` `RADIUS_MICROSOFT_MS_MPPE_ENCRYPTION_POLICY` `RADIUS_MICROSOFT_MS_MPPE_ENCRYPTION_TYPES` `RADIUS_MICROSOFT_MS_RAS_VENDOR` `RADIUS_MICROSOFT_MS_CHAP_DOMAIN` `RADIUS_MICROSOFT_MS_CHAP_CHALLENGE` `RADIUS_MICROSOFT_MS_CHAP_MPPE_KEYS` `RADIUS_MICROSOFT_MS_BAP_USAGE` `RADIUS_MICROSOFT_MS_LINK_UTILIZATION_THRESHOLD` `RADIUS_MICROSOFT_MS_LINK_DROP_TIME_LIMIT` `RADIUS_MICROSOFT_MS_MPPE_SEND_KEY` `RADIUS_MICROSOFT_MS_MPPE_RECV_KEY` `RADIUS_MICROSOFT_MS_RAS_VERSION` `RADIUS_MICROSOFT_MS_OLD_ARAP_PASSWORD` `RADIUS_MICROSOFT_MS_NEW_ARAP_PASSWORD` `RADIUS_MICROSOFT_MS_ARAP_PASSWORD_CHANGE_REASON` `RADIUS_MICROSOFT_MS_FILTER` `RADIUS_MICROSOFT_MS_ACCT_AUTH_TYPE` `RADIUS_MICROSOFT_MS_ACCT_EAP_TYPE` `RADIUS_MICROSOFT_MS_CHAP2_RESPONSE` `RADIUS_MICROSOFT_MS_CHAP2_SUCCESS` `RADIUS_MICROSOFT_MS_CHAP2_PW` `RADIUS_MICROSOFT_MS_PRIMARY_DNS_SERVER` `RADIUS_MICROSOFT_MS_SECONDARY_DNS_SERVER` `RADIUS_MICROSOFT_MS_PRIMARY_NBNS_SERVER` `RADIUS_MICROSOFT_MS_SECONDARY_NBNS_SERVER` `RADIUS_MICROSOFT_MS_ARAP_CHALLENGE`', +'RADIUS_VENDOR_SPECIFIC' => 'The Vendor-Specific attribute. In general, vendor attribute values should be set using `radius_put_vendor_addr`, `radius_put_vendor_attr`, `radius_put_vendor_int` and `radius_put_vendor_string`, rather than directly. + +This constant is mostly useful when interpreting vendor specific attributes in responses from a RADIUS server; when a vendor specific attribute is received, the `radius_get_vendor_attr` function should be used to access the vendor ID, attribute type and attribute value.', +'RAR_HOST_BEOS' => 'Use `RarEntry::HOST_BEOS` instead.', +'RAR_HOST_MSDOS' => 'Use `RarEntry::HOST_MSDOS` instead.', +'RAR_HOST_OS2' => 'Use `RarEntry::HOST_OS2` instead.', +'RAR_HOST_UNIX' => 'Use `RarEntry::HOST_UNIX` instead.', +'RAR_HOST_WIN32' => 'Use `RarEntry::HOST_WIN32` instead.', +'RarEntry::ATTRIBUTE_UNIX_BLOCK_DEV' => 'Unix block devices will have attributes whose last four bits have this value. To be used with +{@see RarEntry::getAttr()} on entries whose host OS is UNIX and with the constant +{@see RarEntry::ATTRIBUTE_UNIX_FINAL_QUARTET}.', +'RarEntry::ATTRIBUTE_UNIX_CHAR_DEV' => 'Unix character devices will have attributes whose last four bits have this value. To be used with +{@see RarEntry::getAttr()} on entries whose host OS is UNIX and with the constant +{@see RarEntry::ATTRIBUTE_UNIX_FINAL_QUARTET}.', +'RarEntry::ATTRIBUTE_UNIX_DIRECTORY' => 'Unix directories will have attributes whose last four bits have this value. To be used with +{@see RarEntry::getAttr()} on entries whose host OS is UNIX and with the constant +{@see RarEntry::ATTRIBUTE_UNIX_FINAL_QUARTET}. + +See also {@see RarEntry::isDirectory()}, which also works with entries that were added in other operating +systems.', +'RarEntry::ATTRIBUTE_UNIX_FIFO' => 'Unix FIFOs will have attributes whose last four bits have this value. To be used with {@see RarEntry::getAttr()} +on entries whose host OS is UNIX and with the constant {@see RarEntry::ATTRIBUTE_UNIX_FINAL_QUARTET}.', +'RarEntry::ATTRIBUTE_UNIX_FINAL_QUARTET' => 'Mask to isolate the last four bits (nibble) of UNIX attributes (_S_IFMT, the type of file mask). To be used with +{@see RarEntry::getAttr()} on entries whose host OS is UNIX and with the constants +{@see RarEntry::ATTRIBUTE_UNIX_FIFO}, {@see RarEntry::ATTRIBUTE_UNIX_CHAR_DEV}, +{@see RarEntry::ATTRIBUTE_UNIX_DIRECTORY}, {@see RarEntry::ATTRIBUTE_UNIX_BLOCK_DEV}, +{@see RarEntry::ATTRIBUTE_UNIX_REGULAR_FILE}, +{@see RarEntry::ATTRIBUTE_UNIX_SYM_LINK} and {@see RarEntry::ATTRIBUTE_UNIX_SOCKET}.', +'RarEntry::ATTRIBUTE_UNIX_GROUP_EXECUTE' => 'Bit that represents a UNIX entry that is group executable. To be used with {@see RarEntry::getAttr()} on entries +whose host OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_GROUP_READ' => 'Bit that represents a UNIX entry that is group readable. To be used with {@see RarEntry::getAttr()} on entries +whose host OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_GROUP_WRITE' => 'Bit that represents a UNIX entry that is group writable. To be used with {@see RarEntry::getAttr()} on entries +whose host OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_OWNER_EXECUTE' => 'Bit that represents a UNIX entry that is owner executable. To be used with {@see RarEntry::getAttr()} on entries +whose host OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_OWNER_READ' => 'Bit that represents a UNIX entry that is owner readable. To be used with {@see RarEntry::getAttr()} on entries +whose host OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_OWNER_WRITE' => 'Bit that represents a UNIX entry that is owner writable. To be used with {@see RarEntry::getAttr()} on entries +whose host OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_REGULAR_FILE' => 'Unix regular files (not directories) will have attributes whose last four bits have this value. To be used with +{@see RarEntry::getAttr()} on entries whose host OS is UNIX and with the constant +{@see RarEntry::ATTRIBUTE_UNIX_FINAL_QUARTET}. See also {@see RarEntry::isDirectory()}, which also works with +entries that were added in other operating systems.', +'RarEntry::ATTRIBUTE_UNIX_SETGID' => 'Bit that represents the UNIX setgid attribute. To be used with {@see RarEntry::getAttr()} on entries whose host +OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_SETUID' => 'Bit that represents the UNIX setuid attribute. To be used with {@see RarEntry::getAttr()} on entries whose host +OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_SOCKET' => 'Unix sockets will have attributes whose last four bits have this value. To be used with +{@see RarEntry::getAttr()} on entries whose host OS is UNIX and with the constant +{@see RarEntry::ATTRIBUTE_UNIX_FINAL_QUARTET}.', +'RarEntry::ATTRIBUTE_UNIX_STICKY' => 'Bit that represents the UNIX sticky bit. To be used with {@see RarEntry::getAttr()} on entries whose host OS is +UNIX.', +'RarEntry::ATTRIBUTE_UNIX_SYM_LINK' => 'Unix symbolic links will have attributes whose last four bits have this value. To be used with +{@see RarEntry::getAttr()} on entries whose host OS is UNIX and with the constant +{@see RarEntry::ATTRIBUTE_UNIX_FINAL_QUARTET}.', +'RarEntry::ATTRIBUTE_UNIX_WORLD_EXECUTE' => 'Bit that represents a UNIX entry that is world executable. To be used with {@see RarEntry::getAttr()} on entries +whose host OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_WORLD_READ' => 'Bit that represents a UNIX entry that is world readable. To be used with {@see RarEntry::getAttr()} on entries +whose host OS is UNIX.', +'RarEntry::ATTRIBUTE_UNIX_WORLD_WRITE' => 'Bit that represents a UNIX entry that is world writable. To be used with {@see RarEntry::getAttr()} on entries +whose host OS is UNIX.', +'RarEntry::ATTRIBUTE_WIN_ARCHIVE' => 'Bit that represents a Windows entry with an archive attribute. To be used with {@see RarEntry::getAttr()} on +entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_COMPRESSED' => 'Bit that represents a Windows entry with a compressed attribute (NTFS only). To be used with +{@see RarEntry::getAttr()} on entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_DEVICE' => 'Bit that represents a Windows entry with a device attribute. To be used with {@see RarEntry::getAttr()} on +entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_DIRECTORY' => 'Bit that represents a Windows entry with a directory attribute (entry is a directory). To be used with +{@see RarEntry::getAttr()} on entries whose host OS is Microsoft Windows. See also +{@see RarEntry::isDirectory()}, which also works with entries that were not added in WinRAR.', +'RarEntry::ATTRIBUTE_WIN_ENCRYPTED' => 'Bit that represents a Windows entry with an encrypted attribute (NTFS only). To be used with +{@see RarEntry::getAttr()} on entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_HIDDEN' => 'Bit that represents a Windows entry with a hidden attribute. To be used with {@see RarEntry::getAttr()} on +entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_NORMAL' => 'Bit that represents a Windows entry with a normal file attribute (entry is NOT a directory). To be used with +{@see RarEntry::getAttr()} on entries whose host OS is Microsoft Windows. See also +{@see RarEntry::isDirectory()}, which also works with entries that were not added in WinRAR.', +'RarEntry::ATTRIBUTE_WIN_NOT_CONTENT_INDEXED' => 'Bit that represents a Windows entry with a not content indexed attribute (entry is to be indexed). To be used +with {@see RarEntry::getAttr()} on entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_OFFLINE' => 'Bit that represents a Windows entry with an offline attribute (entry is offline and not accessible). To be used +with {@see RarEntry::getAttr()} on entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_READONLY' => 'Bit that represents a Windows entry with a read-only attribute. To be used with {@see RarEntry::getAttr()} on +entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_REPARSE_POINT' => 'Bit that represents a Windows entry with a reparse point attribute (entry is an NTFS reparse point, e.g. a +directory junction or a mount file system). To be used with {@see RarEntry::getAttr()} on entries whose host OS +is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_SPARSE_FILE' => 'Bit that represents a Windows entry with a sparse file attribute (file is an NTFS sparse file). To be used with +{@see RarEntry::getAttr()} on entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_SYSTEM' => 'Bit that represents a Windows entry with a system attribute. To be used with {@see RarEntry::getAttr()} on +entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_TEMPORARY' => 'Bit that represents a Windows entry with a temporary attribute. To be used with {@see RarEntry::getAttr()} on +entries whose host OS is Microsoft Windows.', +'RarEntry::ATTRIBUTE_WIN_VIRTUAL' => 'Bit that represents a Windows entry with a virtual attribute. To be used with {@see RarEntry::getAttr()} +on entries whose host OS is Microsoft Windows.', +'RarEntry::HOST_BEOS' => 'If the return value of {@see RarEntry::getHostOs()} equals this constant, BeOS was used to add this entry. +Intended to replace {@see RAR_HOST_BEOS}.', +'RarEntry::HOST_MACOS' => 'If the return value of {@see RarEntry::getHostOs()} equals this constant, Mac OS was used to add this entry.', +'RarEntry::HOST_MSDOS' => 'If the return value of {@see RarEntry::getHostOs()} equals this constant, MS-DOS was used to add this entry. +Use instead of {@see RAR_HOST_MSDOS}.', +'RarEntry::HOST_OS2' => 'If the return value of {@see RarEntry::getHostOs()} equals this constant, OS/2 was used to add this entry. +Intended to replace {@see RAR_HOST_OS2}.', +'RarEntry::HOST_UNIX' => 'If the return value of {@see RarEntry::getHostOs()} equals this constant, an unspecified UNIX OS was used to add +this entry. Intended to replace {@see RAR_HOST_UNIX}.', +'RarEntry::HOST_WIN32' => 'If the return value of {@see RarEntry::getHostOs()} equals this constant, Microsoft Windows was used to add this entry. +Intended to replace {@see RAR_HOST_WIN32}', +'READLINE_LIB' => 'The library which is used for readline support; currently either `readline` or `libedit`. Available as of PHP 5.5.0.', +'RPMMIRE_DEFAULT' => 'Search pattern is a regular expression with \., .* and ^...$ added.', +'RPMMIRE_GLOB' => 'Search pattern is a glob expression, using fnmatch(3).', +'RPMMIRE_REGEX' => 'Search pattern is a regular expression, using regcomp(3).', +'RPMMIRE_STRCMP' => 'Search pattern is a `string`, using strcmp(3).', +'RPMREADER_BASENAMES' => 'The list of the names of files in the RPM package without path information. This tag is used in conjunction with RPMREADER_DIRINDEXES and RPMREADER_DIRNAMES to reconstruct filenames in the RPM package stored with the new "CompressedFileNames" method in RPM.', +'RPMREADER_BUILDHOST' => 'The name of the host on which the RPM package was built.', +'RPMREADER_BUILDTIME' => 'The date and time when the RPM package was built.', +'RPMREADER_CHANGELOGNAME' => 'The list of changelog entry names.', +'RPMREADER_CHANGELOGTEXT' => 'The list of the text from changelog entries.', +'RPMREADER_CHANGELOGTIME' => 'The list of dates from changelog entries.', +'RPMREADER_DESCRIPTION' => 'The full description text of the RPM package.', +'RPMREADER_DIRINDEXES' => 'The list of indices that relate directory names to files in the RPM package. This tag is used in conjunction with RPMREADER_BASENAMES and RPMREADER_DIRNAMES to reconstruct filenames in the RPM package stored with the new "CompressedFileNames" method in RPM.', +'RPMREADER_DIRNAMES' => 'The list of directory names used by files in the RPM package. This tag is used in conjunction with RPMREADER_BASENAMES and RPMREADER_DIRINDEXES to reconstruct filenames in the RPM package stored with the new "CompressedFileNames" method in RPM.', +'RPMREADER_MAXIMUM' => 'The maximum valid value of any RPM tag number.', +'RPMREADER_MINIMUM' => 'The minimum valid value of any RPM tag number.', +'RPMREADER_NAME' => 'The name of the RPM package.', +'RPMREADER_OLDFILENAMES' => 'The list of files in an RPM package (deprecated format). The correct way is now to use a combination of 3 tags (RPMREADER_BASENAMES, RPMREADER_DIRINDEXES, RPMREADER_DIRNAMES) in what RPM now calls "CompressedFileNames". This tag is still used in older RPM files that did not use the "CompressedFileNames" method and is maintained for backward compatibility.', +'RPMREADER_RELEASE' => 'The release of the RPM package.', +'RPMREADER_SIZE' => 'The size of the RPM package.', +'RPMREADER_SUMMARY' => 'The summary text of the RPM package.', +'RPMREADER_VERSION' => 'The version of the RPM package.', +'RPMTAG_BASENAMES' => 'Name (not path) of files, with database index.', +'RPMTAG_CONFLICTNAME' => 'Conflicting dependencies, with database index.', +'RPMTAG_DIRNAMES' => 'Directory of files, with database index.', +'RPMTAG_ENHANCENAME' => 'Weak dependencies, with database index, requires librpm >= 4.13.', +'RPMTAG_FILETRIGGERNAME' => 'File trigger name, with database index, requires librpm >= 4.13.', +'RPMTAG_GROUP' => 'Group of the package, with database index.', +'RPMTAG_INSTALLTID' => 'Installation transaction ID, with database index.', +'RPMTAG_INSTFILENAMES' => 'Path of files, with database index.', +'RPMTAG_NAME' => 'Package name, with database index.', +'RPMTAG_OBSOLETENAME' => 'Obsoleted packages, with database index.', +'RPMTAG_PROVIDENAME' => 'Provided dependencies, with database index.', +'RPMTAG_RECOMMENDNAME' => 'Recommended weak dependencies, with database index, requires librpm >= 4.13.', +'RPMTAG_REQUIRENAME' => 'Required dependencies, with database index.', +'RPMTAG_SHA1HEADER' => 'SHA1 signature, with database index.', +'RPMTAG_SIGMD5' => 'MD5 signature, with database index.', +'RPMTAG_SUGGESTNAME' => 'Suggested weak dependencies, with database index, requires librpm >= 4.13.', +'RPMTAG_SUPPLEMENTNAME' => 'Weak dependencies, with database index, requires librpm >= 4.13.', +'RPMTAG_TRANSFILETRIGGERNAME' => 'Transaction file trigger name, with database index, requires librpm >= 4.13.', +'RPMTAG_TRIGGERNAME' => 'Trigger name, with database index.', +'RPMVERSION' => 'System librpm version.', +'RUNKIT7_ACC_PRIVATE' => 'Flag for `runkit7_method_add` and `runkit7_method_redefine` to make the method private.', +'RUNKIT7_ACC_PROTECTED' => 'Flag for `runkit7_method_add` and `runkit7_method_redefine` to make the method protected.', +'RUNKIT7_ACC_PUBLIC' => 'Flag for `runkit7_method_add` and `runkit7_method_redefine` to make the method public.', +'RUNKIT7_ACC_RETURN_REFERENCE' => 'Include this flag to make the function or method being created or redeclared return a reference.', +'RUNKIT7_ACC_STATIC' => 'Flag for `runkit7_method_add` and `runkit7_method_redefine` to make the method static.', +'RUNKIT7_FEATURE_MANIPULATION' => 'Equal to 1 if runtime manipulation is enabled, and 0 otherwise.', +'RUNKIT7_FEATURE_SANDBOX' => 'Always 0, it\'s impractical to implement the sandbox feature in php 7.', +'RUNKIT7_FEATURE_SUPERGLOBALS' => 'Equal to 1 if custom superglobals are enabled, and 0 otherwise.', +'RUNKIT7_IMPORT_CLASS_CONSTS' => '`runkit7_import` flag indicating that class constants should be imported from the specified file.', +'RUNKIT7_IMPORT_CLASS_METHODS' => '`runkit7_import` flag indicating that class methods should be imported from the specified file.', +'RUNKIT7_IMPORT_CLASS_PROPS' => '`runkit7_import` flag indicating that class standard properties should be imported from the specified file.', +'RUNKIT7_IMPORT_CLASS_STATIC_PROPS' => '`runkit7_import` flag indicating that class static properties should be imported from the specified file. Available since Runkit 1.0.1.', +'RUNKIT7_IMPORT_CLASSES' => '`runkit7_import` flag representing a bitwise OR of the `RUNKIT7_IMPORT_CLASS_*` constants.', +'RUNKIT7_IMPORT_FUNCTIONS' => '`runkit7_import` flag indicating that normal functions should be imported from the specified file.', +'RUNKIT7_IMPORT_OVERRIDE' => '`runkit7_import` flag indicating that if any of the imported functions, methods, constants, or properties already exist, they should be replaced with the new definitions. If this flag is not set, then any imported definitions which already exist will be discarded.', +'RUNKIT_ACC_PRIVATE' => 'PHP 5 specific flag to `runkit_method_add` and `runkit_method_redefine`', +'RUNKIT_ACC_PROTECTED' => 'PHP 5 specific flag to `runkit_method_add` and `runkit_method_redefine`', +'RUNKIT_ACC_PUBLIC' => 'PHP 5 specific flag to `runkit_method_add` and `runkit_method_redefine`', +'RUNKIT_ACC_STATIC' => 'PHP 5 specific flag to `runkit_method_add` and `runkit_method_redefine`. Available since Runkit 1.0.1.', +'RUNKIT_IMPORT_CLASS_CONSTS' => '`runkit_import` flag indicating that class constants should be imported from the specified file. Note that this flag is only meaningful in PHP versions 5.1.0 and above.', +'RUNKIT_IMPORT_CLASS_METHODS' => '`runkit_import` flag indicating that class methods should be imported from the specified file.', +'RUNKIT_IMPORT_CLASS_PROPS' => '`runkit_import` flag indicating that class standard properties should be imported from the specified file.', +'RUNKIT_IMPORT_CLASS_STATIC_PROPS' => '`runkit_import` flag indicating that class static properties should be imported from the specified file. Available since Runkit 1.0.1.', +'RUNKIT_IMPORT_CLASSES' => '`runkit_import` flag representing a bitwise OR of the `RUNKIT_IMPORT_CLASS_*` constants.', +'RUNKIT_IMPORT_FUNCTIONS' => '`runkit_import` flag indicating that normal functions should be imported from the specified file.', +'RUNKIT_IMPORT_OVERRIDE' => '`runkit_import` flag indicating that if any of the imported functions, methods, constants, or properties already exist, they should be replaced with the new definitions. If this flag is not set, then any imported definitions which already exist will be discarded.', +'RUNKIT_VERSION' => 'Defined to the current version of the runkit package.', +'SAM_AUTO' => 'Automatic behaviour', +'SAM_BOOLEAN' => 'Type specifier used when setting properties on SAM_Message objects.', +'SAM_BUS' => 'Connect attribute used to set the name of the enterprise service bus to connect to.', +'SAM_BYTE' => 'Type specifier used when setting properties on SAM_Message objects.', +'SAM_BYTES' => 'Message body type descriptor.', +'SAM_CORRELID' => 'Attribute used on receive, send and remove requests to identify specific messages.', +'SAM_DELIVERYMODE' => 'Message header property.', +'SAM_DOUBLE' => 'Type specifier used when setting properties on SAM_Message objects.', +'SAM_ENDPOINTS' => 'Connect attribute used to define the possible endpoints to connect to.', +'SAM_FLOAT' => 'Type specifier used when setting properties on SAM_Message objects.', +'SAM_HOST' => 'Connect attribute used to set the hostname of the required messaging server.', +'SAM_INT' => 'Type specifier used when setting properties on SAM_Message objects.', +'SAM_LONG' => 'Type specifier used when setting properties on SAM_Message objects.', +'SAM_MANUAL' => 'Manual (script controlled) behaviour', +'SAM_MESSAGEID' => 'Attribute used on receive and remove requests to identify specific messages.', +'SAM_MQTT' => 'Connect protocol definition for selecting the MQTT (MQ Telemetry Transport) protocol.', +'SAM_MQTT_CLEANSTART' => 'Optional connect option to indicate to an MQTT server that all previous connection data for this client should be removed and that subscriptions should be deleted when the client disconnects explicitly or unexpectedly.', +'SAM_NON_PERSISTENT' => 'Connect attribute value used to request messages are not made persistent on the messaging server.', +'SAM_PASSWORD' => 'Connect attribute used to define the password to be used for the user account being used to connect to a messaging server that requires authorisation for connections.', +'SAM_PERSISTENT' => 'Connect attribute value used to request messages are made persistent on the messaging server to protect against loss of messages in the event of failure.', +'SAM_PORT' => 'Connect attribute used to set the port number on which to communicate with the messaging server.', +'SAM_PRIORITY' => 'Option name used on send requests to specify a delivery priority value.', +'SAM_REPLY_TO' => 'Message property used to specify the queue identity on to which the script expects response or reply messages to be posted.', +'SAM_RTT' => 'Connect protocol definition for selecting the IBM Realtime Transport protocol for communication with a business integration messaging server.', +'SAM_STRING' => 'Type specifier used when setting properties on SAM_Message objects.', +'SAM_TARGETCHAIN' => 'Connection attribute used to set the required target chain identifier.', +'SAM_TEXT' => 'Message body type descriptor.', +'SAM_TIMETOLIVE' => 'Message send option name used to specify the length of time a message should be retained in milliseconds.', +'SAM_TRANSACTIONS' => 'Connection attribute used to set required transactional behaviour. May be set to SAM_AUTO (default) or SAM_MANUAL.', +'SAM_USERID' => 'Connect attribute used to define the account to being used to connect to a messaging server that requires authorisation for connections.', +'SAM_WAIT' => 'Receive property used to specify the wait timeout to be used when receiving a message from a queue or subscription.', +'SAM_WMQ' => 'Connect protocol definition for selecting the IBM WebSphere MQSeries protocol for communication with the desired messaging server.', +'SAM_WMQ_BINDINGS' => 'Connect protocol definition for selecting the IBM WebSphere MQSeries protocol for communication with a local messaging server.', +'SAM_WMQ_CLIENT' => 'Connect protocol definition for selecting the IBM WebSphere MQSeries protocol for communication with a remote messaging server.', +'SAM_WMQ_TARGET_CLIENT' => 'Option name used on send requests to specify the target client mode. This can either be default to \'jms\' or \'mq\'. The default is \'jms\' which means an RFH2 header is sent with the message whereas the \'mq\' setting means no RFH2 is included.', +'SAM_WPM' => 'Connect protocol definition for selecting the IBM WebSphere Platform Messaging protocol for communication with a WebSphere Application Server messaging server.', +'SCANDIR_SORT_ASCENDING' => 'Available since PHP 5.4.0.', +'SCANDIR_SORT_DESCENDING' => 'Available since PHP 5.4.0.', +'SCANDIR_SORT_NONE' => 'Available since PHP 5.4.0.', +'SDO_DAS_ChangeSummary::ADDITION' => 'Represents a change type of \'addition\'.', +'SDO_DAS_ChangeSummary::DELETION' => 'Represents a change type of \'deletion\'.', +'SDO_DAS_ChangeSummary::MODIFICATION' => 'Represents a change type of \'modification\'.', +'SDO_DAS_ChangeSummary::NONE' => 'Represents a change type of \'none\'.', +'SE_NOPREFETCH' => 'Don\'t prefetch searched messages', +'SE_UID' => 'Return UIDs instead of sequence numbers', +'SEASLOG_ALERT' => '"ALERT" - Action must be taken immediately.Immediate attention should be given to relevant personnel for emergency repairs.', +'SEASLOG_ALL' => '"ALL"', +'SEASLOG_APPENDER_FILE' => '1', +'SEASLOG_APPENDER_TCP' => '2', +'SEASLOG_APPENDER_UDP' => '3', +'SEASLOG_CLOSE_LOGGER_STREAM_MOD_ALL' => '1', +'SEASLOG_CLOSE_LOGGER_STREAM_MOD_ASSIGN' => '2', +'SEASLOG_CRITICAL' => '"CRITICAL" - Critical conditions.Need to be repaired immediately, and the program component is unavailable.', +'SEASLOG_DEBUG' => '"DEBUG" - Detailed debug information.Fine-grained information events.', +'SEASLOG_DETAIL_ORDER_ASC' => '1', +'SEASLOG_DETAIL_ORDER_DESC' => '2', +'SEASLOG_EMERGENCY' => '"EMERGENCY" - System is unusable.', +'SEASLOG_ERROR' => '"ERROR" - Runtime errors that do not require immediate action but should typically.', +'SEASLOG_INFO' => '"INFO" - Interesting events.Emphasizes the running process of the application.', +'SEASLOG_NOTICE' => '"NOTICE" - Normal but significant events.Information that is more important than the INFO level during execution.', +'SEASLOG_REQUEST_VARIABLE_CLIENT_IP' => '4', +'SEASLOG_REQUEST_VARIABLE_DOMAIN_PORT' => '1', +'SEASLOG_REQUEST_VARIABLE_REQUEST_METHOD' => '3', +'SEASLOG_REQUEST_VARIABLE_REQUEST_URI' => '2', +'SEASLOG_WARNING' => '"WARNING" - Exceptional occurrences that are not errors.Potentially aberrant information that needs attention and needs to be repaired.', +'SEGV_ACCERR' => 'Available since PHP 5.3.0.', +'SEGV_MAPERR' => 'Available since PHP 5.3.0.', +'SI_ASYNCIO' => 'Available since PHP 5.3.0.', +'SI_KERNEL' => 'Available since PHP 5.3.0.', +'SI_MSGGQ' => 'Available since PHP 5.3.0.', +'SI_NOINFO' => 'Available since PHP 5.3.0.', +'SI_QUEUE' => 'Available since PHP 5.3.0.', +'SI_SIGIO' => 'Available since PHP 5.3.0.', +'SI_TIMER' => 'Available since PHP 5.3.0.', +'SI_TKILL' => 'Available since PHP 5.3.0.', +'SI_USER' => 'Available since PHP 5.3.0.', +'SID' => 'Constant containing either the session name and session ID in the form of `"name=ID"` or empty string if session ID was set in an appropriate session cookie. This is the same id as the one returned by `session_id`.', +'SIG_BLOCK' => 'Available since PHP 5.3.0.', +'SIG_SETMASK' => 'Available since PHP 5.3.0.', +'SIG_UNBLOCK' => 'Available since PHP 5.3.0.', +'SNMP_OID_OUTPUT_FULL' => 'As of 5.2.0', +'SNMP_OID_OUTPUT_MODULE' => 'As of 5.4.0', +'SNMP_OID_OUTPUT_NONE' => 'As of 5.4.0', +'SNMP_OID_OUTPUT_NUMERIC' => 'As of 5.2.0', +'SNMP_OID_OUTPUT_SUFFIX' => 'As of 5.4.0', +'SNMP_OID_OUTPUT_UCD' => 'As of 5.4.0', +'SO_REUSEPORT' => 'This constant is only available in PHP 5.4.10 or later on platforms that support the `SO_REUSEPORT` socket option: this includes macOS and FreeBSD, but does not include Linux or Windows.', +'SOCKET_ADDRINUSE' => 'Address already in use.', +'SOCKET_E2BIG' => 'Arg list too long.', +'SOCKET_EACCES' => 'Permission denied.', +'SOCKET_EADDRNOTAVAIL' => 'Cannot assign requested address.', +'SOCKET_EADV' => 'Advertise error.', +'SOCKET_EAFNOSUPPORT' => 'Address family not supported by protocol.', +'SOCKET_EAGAIN' => 'Try again.', +'SOCKET_EALREADY' => 'Operation already in progress.', +'SOCKET_EBADE' => 'Invalid exchange.', +'SOCKET_EBADF' => 'Bad file number.', +'SOCKET_EBADFD' => 'File descriptor in bad state.', +'SOCKET_EBADMSG' => 'Not a data message.', +'SOCKET_EBADR' => 'Invalid request descriptor.', +'SOCKET_EBADRQC' => 'Invalid request code.', +'SOCKET_EBADSLT' => 'Invalid slot.', +'SOCKET_EBUSY' => 'Device or resource busy.', +'SOCKET_ECHRNG' => 'Channel number out of range.', +'SOCKET_ECOMM' => 'Communication error on send.', +'SOCKET_ECONNABORTED' => 'Software caused connection abort.', +'SOCKET_ECONNREFUSED' => 'Connection refused.', +'SOCKET_ECONNRESET' => 'Connection reset by peer.', +'SOCKET_EDESTADDRREQ' => 'Destination address required.', +'SOCKET_EDQUOT' => 'Quota exceeded.', +'SOCKET_EEXIST' => 'File exists.', +'SOCKET_EFAULT' => 'Bad address.', +'SOCKET_EHOSTDOWN' => 'Host is down.', +'SOCKET_EHOSTUNREACH' => 'No route to host.', +'SOCKET_EIDRM' => 'Identifier removed.', +'SOCKET_EINPROGRESS' => 'Operation now in progress.', +'SOCKET_EINTR' => 'Interrupted system call.', +'SOCKET_EINVAL' => 'Invalid argument.', +'SOCKET_EIO' => 'I/O error.', +'SOCKET_EISCONN' => 'Transport endpoint is already connected.', +'SOCKET_EISDIR' => 'Is a directory.', +'SOCKET_EISNAM' => 'Is a named type file.', +'SOCKET_EL2HLT' => 'Level 2 halted.', +'SOCKET_EL2NSYNC' => 'Level 2 not synchronized.', +'SOCKET_EL3HLT' => 'Level 3 halted.', +'SOCKET_EL3RST' => 'Level 3 reset.', +'SOCKET_ELNRNG' => 'Link number out of range.', +'SOCKET_ELOOP' => 'Too many symbolic links encountered.', +'SOCKET_EMEDIUMTYPE' => 'Wrong medium type.', +'SOCKET_EMFILE' => 'Too many open files.', +'SOCKET_EMLINK' => 'Too many links.', +'SOCKET_EMSGSIZE' => 'Message too long.', +'SOCKET_EMULTIHOP' => 'Multihop attempted.', +'SOCKET_ENAMETOOLONG' => 'File name too long.', +'SOCKET_ENETDOWN' => 'Network is down.', +'SOCKET_ENETRESET' => 'Network dropped connection because of reset.', +'SOCKET_ENETUNREACH' => 'Network is unreachable.', +'SOCKET_ENFILE' => 'File table overflow.', +'SOCKET_ENOANO' => 'No anode.', +'SOCKET_ENOBUFS' => 'No buffer space available.', +'SOCKET_ENOCSI' => 'No CSI structure available.', +'SOCKET_ENODATA' => 'No data available.', +'SOCKET_ENODEV' => 'No such device.', +'SOCKET_ENOENT' => 'No such file or directory.', +'SOCKET_ENOLCK' => 'No record locks available.', +'SOCKET_ENOLINK' => 'Link has been severed.', +'SOCKET_ENOMEDIUM' => 'No medium found.', +'SOCKET_ENOMEM' => 'Out of memory.', +'SOCKET_ENOMSG' => 'No message of desired type.', +'SOCKET_ENONET' => 'Machine is not on the network.', +'SOCKET_ENOSPC' => 'No space left on device.', +'SOCKET_ENOSR' => 'Out of streams resources.', +'SOCKET_ENOSTR' => 'Device not a stream.', +'SOCKET_ENOSYS' => 'Function not implemented.', +'SOCKET_ENOTBLK' => 'Block device required.', +'SOCKET_ENOTCONN' => 'Transport endpoint is not connected.', +'SOCKET_ENOTDIR' => 'Not a directory.', +'SOCKET_ENOTEMPTY' => 'Directory not empty.', +'SOCKET_ENOTSOCK' => 'Socket operation on non-socket.', +'SOCKET_ENOTTY' => 'Not a typewriter.', +'SOCKET_ENOTUNIQ' => 'Name not unique on network.', +'SOCKET_ENXIO' => 'No such device or address.', +'SOCKET_EOPNOTSUPP' => 'Operation not supported on transport endpoint.', +'SOCKET_EPERM' => 'Operation not permitted.', +'SOCKET_EPFNOSUPPORT' => 'Protocol family not supported.', +'SOCKET_EPIPE' => 'Broken pipe.', +'SOCKET_EPROTO' => 'Protocol error.', +'SOCKET_EPROTONOSUPPORT' => 'Protocol not supported.', +'SOCKET_EPROTOOPT' => 'Protocol not available.', +'SOCKET_EPROTOTYPE' => 'Protocol wrong type for socket.', +'SOCKET_EREMCHG' => 'Remote address changed.', +'SOCKET_EREMOTE' => 'Object is remote.', +'SOCKET_EREMOTEIO' => 'Remote I/O error.', +'SOCKET_ERESTART' => 'Interrupted system call should be restarted.', +'SOCKET_EROFS' => 'Read-only file system.', +'SOCKET_ESHUTDOWN' => 'Cannot send after transport endpoint shutdown.', +'SOCKET_ESOCKTNOSUPPORT' => 'Socket type not supported.', +'SOCKET_ESPIPE' => 'Illegal seek.', +'SOCKET_ESRMNT' => 'Srmount error.', +'SOCKET_ESTRPIPE' => 'Streams pipe error.', +'SOCKET_ETIME' => 'Timer expired.', +'SOCKET_ETIMEDOUT' => 'Connection timed out.', +'SOCKET_ETOOMANYREFS' => 'Too many references: cannot splice.', +'SOCKET_EUNATCH' => 'Protocol driver not attached.', +'SOCKET_EUSERS' => 'Too many users.', +'SOCKET_EWOULDBLOCK' => 'Operation would block.', +'SOCKET_EXDEV' => 'Cross-device link.', +'SOCKET_EXFULL' => 'Exchange full.', +'Sodium\CRYPTO_AEAD_AES256GCM_KEYBYTES' => 'To silence the phpstorm "unknown namespace" errors.', +'SolrClient::DEFAULT_PING_SERVLET' => 'This is the initial value for the ping servlet.', +'SolrClient::DEFAULT_SEARCH_SERVLET' => 'This is the initial value for the search servlet.', +'SolrClient::DEFAULT_SYSTEM_SERVLET' => 'This is the initial value for the system servlet used to obtain Solr Server information.', +'SolrClient::DEFAULT_TERMS_SERVLET' => 'This is the initial value for the terms servlet used for the TermsComponent.', +'SolrClient::DEFAULT_THREADS_SERVLET' => 'This is the initial value for the threads servlet.', +'SolrClient::DEFAULT_UPDATE_SERVLET' => 'This is the initial value for the update servlet.', +'SolrClient::PING_SERVLET_TYPE' => 'Used when updating the ping servlet.', +'SolrClient::SEARCH_SERVLET_TYPE' => 'Used when updating the search servlet.', +'SolrClient::SYSTEM_SERVLET_TYPE' => 'Used when retrieving system information from the system servlet.', +'SolrClient::TERMS_SERVLET_TYPE' => 'Used when updating the terms servlet.', +'SolrClient::THREADS_SERVLET_TYPE' => 'Used when updating the threads servlet.', +'SolrClient::UPDATE_SERVLET_TYPE' => 'Used when updating the update servlet.', +'SolrDisMaxQuery::FACET_SORT_COUNT' => 'Used to specify that the facet should sort by count', +'SolrDisMaxQuery::FACET_SORT_INDEX' => 'Used to specify that the facet should sort by index', +'SolrDisMaxQuery::ORDER_ASC' => 'Used to specify that the sorting should be in acending order', +'SolrDisMaxQuery::ORDER_DESC' => 'Used to specify that the sorting should be in descending order', +'SolrDisMaxQuery::TERMS_SORT_COUNT' => 'Used in the TermsComponent', +'SolrDisMaxQuery::TERMS_SORT_INDEX' => 'Used in the TermsComponent', +'SolrDocument::SORT_ASC' => 'Sorts the fields in ascending order.', +'SolrDocument::SORT_DEFAULT' => 'Sorts the fields in ascending order.', +'SolrDocument::SORT_DESC' => 'Sorts the fields in descending order.', +'SolrDocument::SORT_FIELD_BOOST_VALUE' => 'Sorts the fields by boost value.', +'SolrDocument::SORT_FIELD_NAME' => 'Sorts the fields by name', +'SolrDocument::SORT_FIELD_VALUE_COUNT' => 'Sorts the fields by number of values.', +'SolrGenericResponse::PARSE_SOLR_DOC' => 'Documents should be parsed as SolrDocument instances.', +'SolrGenericResponse::PARSE_SOLR_OBJ' => 'Documents should be parsed as SolrObject instances', +'SolrInputDocument::SORT_ASC' => 'Sorts the fields in ascending order.', +'SolrInputDocument::SORT_DEFAULT' => 'Sorts the fields in ascending order.', +'SolrInputDocument::SORT_DESC' => 'Sorts the fields in descending order.', +'SolrInputDocument::SORT_FIELD_BOOST_VALUE' => 'Sorts the fields by boost value.', +'SolrInputDocument::SORT_FIELD_NAME' => 'Sorts the fields by name', +'SolrInputDocument::SORT_FIELD_VALUE_COUNT' => 'Sorts the fields by number of values.', +'SolrPingResponse::PARSE_SOLR_DOC' => 'Documents should be parsed as SolrDocument instances.', +'SolrPingResponse::PARSE_SOLR_OBJ' => 'Documents should be parsed as SolrObject instances', +'SolrQuery::FACET_SORT_COUNT' => 'Used to specify that the facet should sort by count', +'SolrQuery::FACET_SORT_INDEX' => 'Used to specify that the facet should sort by index', +'SolrQuery::ORDER_ASC' => 'Used to specify that the sorting should be in acending order', +'SolrQuery::ORDER_DESC' => 'Used to specify that the sorting should be in descending order', +'SolrQuery::TERMS_SORT_COUNT' => 'Used in the TermsComponent', +'SolrQuery::TERMS_SORT_INDEX' => 'Used in the TermsComponent', +'SolrQueryResponse::PARSE_SOLR_DOC' => 'Documents should be parsed as SolrDocument instances.', +'SolrQueryResponse::PARSE_SOLR_OBJ' => 'Documents should be parsed as SolrObject instances', +'SolrResponse::PARSE_SOLR_DOC' => 'Documents should be parsed as SolrDocument instances.', +'SolrResponse::PARSE_SOLR_OBJ' => 'Documents should be parsed as SolrObject instances', +'SolrUpdateResponse::PARSE_SOLR_DOC' => 'Documents should be parsed as SolrDocument instances.', +'SolrUpdateResponse::PARSE_SOLR_OBJ' => 'Documents should be parsed as SolrObject instances', +'SORT_ASC' => '`SORT_ASC` is used with `array_multisort` to sort in ascending order.', +'SORT_DESC' => '`SORT_DESC` is used with `array_multisort` to sort in descending order.', +'SORT_FLAG_CASE' => '`SORT_FLAG_CASE` can be combined (bitwise OR) with `SORT_STRING` or `SORT_NATURAL` to sort strings case-insensitively. Added in PHP 5.4.0.', +'SORT_LOCALE_STRING' => '`SORT_LOCALE_STRING` is used to compare items as strings, based on the current locale. Added in PHP 5.0.2.', +'SORT_NATURAL' => '`SORT_NATURAL` is used to compare items as strings using "natural ordering" like `natsort`. Added in PHP 5.4.0.', +'SORT_NUMERIC' => '`SORT_NUMERIC` is used to compare items numerically.', +'SORT_REGULAR' => '`SORT_REGULAR` is used to compare items normally.', +'SORT_STRING' => '`SORT_STRING` is used to compare items as strings.', +'SORTARRIVAL' => 'Sort criteria for `imap_sort`: arrival date', +'SORTCC' => 'Sort criteria for `imap_sort`: mailbox in first cc address', +'SORTDATE' => 'Sort criteria for `imap_sort`: message Date', +'SORTFROM' => 'Sort criteria for `imap_sort`: mailbox in first From address', +'SORTSIZE' => 'Sort criteria for `imap_sort`: size of message in octets', +'SORTSUBJECT' => 'Sort criteria for `imap_sort`: message subject', +'SORTTO' => 'Sort criteria for `imap_sort`: mailbox in first To address', +'SplType::__default' => 'Default value', +'SQLBIT' => 'Indicates the \'`BIT`\' type in MSSQL, used by `mssql_bind`\'s `type` parameter.', +'SQLCHAR' => 'Indicates the \'`CHAR`\' type in MSSQL, used by `mssql_bind`\'s `type` parameter.', +'SQLFLT4' => 'Represents an four byte float.', +'SQLFLT8' => 'Represents an eight byte float.', +'SQLINT1' => 'Represents one byte, with a range of -128 to 127.', +'SQLINT2' => 'Represents two bytes, with a range of -32768 to 32767.', +'SQLINT4' => 'Represents four bytes, with a range of -2147483648 to 2147483647.', +'SQLITE3_ASSOC' => 'Specifies that the `Sqlite3Result::fetchArray` method shall return an array indexed by column name as returned in the corresponding result set.', +'SQLITE3_BLOB' => 'Represents the SQLite3 BLOB storage class.', +'SQLITE3_BOTH' => 'Specifies that the `Sqlite3Result::fetchArray` method shall return an array indexed by both column name and number as returned in the corresponding result set, starting at column 0.', +'SQLITE3_DETERMINISTIC' => 'Specifies that a function created with `SQLite3::createFunction` is deterministic, i.e. it always returns the same result given the same inputs within a single SQL statement. (Available as of PHP 7.1.4.)', +'SQLITE3_FLOAT' => 'Represents the SQLite3 REAL (FLOAT) storage class.', +'SQLITE3_INTEGER' => 'Represents the SQLite3 INTEGER storage class.', +'SQLITE3_NULL' => 'Represents the SQLite3 NULL storage class.', +'SQLITE3_NUM' => 'Specifies that the `Sqlite3Result::fetchArray` method shall return an array indexed by column number as returned in the corresponding result set, starting at column 0.', +'SQLITE3_OPEN_CREATE' => 'Specifies that the SQLite3 database be created if it does not already exist.', +'SQLITE3_OPEN_READONLY' => 'Specifies that the SQLite3 database be opened for reading only.', +'SQLITE3_OPEN_READWRITE' => 'Specifies that the SQLite3 database be opened for reading and writing.', +'SQLITE3_TEXT' => 'Represents the SQLite3 TEXT storage class.', +'SQLITE_ABORT' => 'Callback routine requested an abort.', +'SQLITE_ASSOC' => 'Columns are returned into the array having the field name as the array index.', +'SQLITE_AUTH' => 'Authorized failed.', +'SQLITE_BOTH' => 'Columns are returned into the array having both a numerical index and the field name as the array index.', +'SQLITE_BUSY' => 'The database file is locked.', +'SQLITE_CANTOPEN' => 'Unable to open the database file.', +'SQLITE_CONSTRAINT' => 'Abort due to constraint violation.', +'SQLITE_CORRUPT' => 'The database disk image is malformed.', +'SQLITE_DONE' => 'Internal process has finished executing.', +'SQLITE_EMPTY' => '(Internal) Database table is empty.', +'SQLITE_ERROR' => 'SQL error or missing database.', +'SQLITE_FORMAT' => 'Auxiliary database format error.', +'SQLITE_FULL' => 'Insertion failed because database is full.', +'SQLITE_INTERNAL' => 'An internal logic error in SQLite.', +'SQLITE_INTERRUPT' => 'Operation terminated internally.', +'SQLITE_IOERR' => 'Disk I/O error occurred.', +'SQLITE_LOCKED' => 'A table in the database is locked.', +'SQLITE_MISMATCH' => 'Data type mismatch.', +'SQLITE_MISUSE' => 'Library used incorrectly.', +'SQLITE_NOLFS' => 'Uses of OS features not supported on host.', +'SQLITE_NOMEM' => 'Memory allocation failed.', +'SQLITE_NOTADB' => 'File opened that is not a database file.', +'SQLITE_NOTFOUND' => '(Internal) Table or record not found.', +'SQLITE_NUM' => 'Columns are returned into the array having a numerical index to the fields. This index starts with 0, the first field in the result.', +'SQLITE_OK' => 'Successful result.', +'SQLITE_PERM' => 'Access permission denied.', +'SQLITE_PROTOCOL' => 'Database lock protocol error.', +'SQLITE_READONLY' => 'Attempt to write a readonly database.', +'SQLITE_ROW' => 'Internal process has another row ready.', +'SQLITE_SCHEMA' => 'The database schema changed.', +'SQLITE_TOOBIG' => 'Too much data for one row of a table.', +'SQLSRV_CURSOR_BUFFERED' => 'Creates a client-side cursor query. Lets you access rows in any order. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_CURSOR_DYNAMIC' => 'Indicates a dynamic cursor. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_CURSOR_FORWARD' => 'Indicates a forward-only cursor. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_CURSOR_KEYSET' => 'Indicates a keyset cursor. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_CURSOR_STATIC' => 'Indicates a static cursor. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_ENC_BINARY' => 'Specifies that data is returned as a raw byte stream from the server without performing encoding or translation. For usage information, see How to: Specify PHP Types.', +'SQLSRV_ENC_CHAR' => 'Data is returned in 8-bit characters as specified in the code page of the Windows locale that is set on the system. Any multi-byte characters or characters that do not map into this code page are substituted with a single byte question mark (?) character. This is the default encoding. For usage information, see How to: Specify PHP Types.', +'SQLSRV_ERR_ALL' => 'Forces `sqlsrv_errors` to return both errors and warings when passed as a parameter (the default behavior).', +'SQLSRV_ERR_ERRORS' => 'Forces `sqlsrv_errors` to return errors only (no warnings) when passed as a parameter.', +'SQLSRV_ERR_WARNINGS' => 'Forces `sqlsrv_errors` to return warnings only (no errors) when passed as a parameter.', +'SQLSRV_FETCH_ASSOC' => 'Forces `sqlsrv_fetch_array` to return an associative array when passed as a parameter.', +'SQLSRV_FETCH_BOTH' => 'Forces `sqlsrv_fetch_array` to return an array with both associative and numeric keys when passed as a parameter (the default behavior).', +'SQLSRV_FETCH_NUMERIC' => 'Forces `sqlsrv_fetch_array` to return an array with numeric when passed as a parameter.', +'SQLSRV_LOG_SEVERITY_ALL' => 'Specifies that errors, warnings, and notices will be logged when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_LOG_SEVERITY_ERROR' => 'Specifies that errors will be logged when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_LOG_SEVERITY_NOTICE' => 'Specifies that notices will be logged when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_LOG_SEVERITY_WARNING' => 'Specifies that warnings will be logged when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_LOG_SYSTEM_ALL' => 'Turns on logging of all subsystems when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_LOG_SYSTEM_CONN' => 'Turns on logging of connection activity when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_LOG_SYSTEM_INIT' => 'Turns on logging of initialization activity when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_LOG_SYSTEM_OFF' => 'Turns off logging of all subsystems when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_LOG_SYSTEM_STMT' => 'Turns on logging of statement activity when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_LOG_SYSTEM_UTIL' => 'Turns on logging of error function activity when passed to `sqlsrv_configure` as a parameter.', +'SQLSRV_NULLABLE_NO' => 'Indicates that a column is not nullable.', +'SQLSRV_NULLABLE_UNKNOWN' => 'Indicates that it is not known if a column is nullable.', +'SQLSRV_NULLABLE_YES' => 'Indicates that a column is nullable.', +'SQLSRV_PARAM_IN' => 'Indicates an input parameter when passed to `sqlsrv_query` or `sqlsrv_prepare`.', +'SQLSRV_PARAM_INOUT' => 'Indicates a bidirectional parameter when passed to `sqlsrv_query` or `sqlsrv_prepare`.', +'SQLSRV_PARAM_OUT' => 'Indicates an output parameter when passed to `sqlsrv_query` or `sqlsrv_prepare`.', +'SQLSRV_PHPTYPE_DATETIME' => 'Specifies a datetime PHP data type. For usage information, see How to: Specify PHP Types.', +'SQLSRV_PHPTYPE_FLOAT' => 'Specifies a float PHP data type. For usage information, see How to: Specify PHP Types.', +'SQLSRV_PHPTYPE_INT' => 'Specifies an integer PHP data type. For usage information, see How to: Specify PHP Types.', +'SQLSRV_PHPTYPE_STREAM' => 'Specifies a PHP stream. This constant works like a function and accepts an encoding constant. See the SQLSRV_ENC_* constants. For usage information, see How to: Specify PHP Types.', +'SQLSRV_PHPTYPE_STRING' => 'Specifies a string PHP data type. This constant works like a function and accepts an encoding constant. See the SQLSRV_ENC_* constants. For usage information, see How to: Specify PHP Types.', +'SQLSRV_SCROLL_ABSOLUTE' => 'Specifies which row to select in a result set. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_SCROLL_FIRST' => 'Specifies which row to select in a result set. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_SCROLL_LAST' => 'Specifies which row to select in a result set. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_SCROLL_NEXT' => 'Specifies which row to select in a result set. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_SCROLL_PRIOR' => 'Specifies which row to select in a result set. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_SCROLL_RELATIVE' => 'Specifies which row to select in a result set. For usage information, see Specifying a Cursor Type and Selecting Rows.', +'SQLSRV_SQLTYPE_BIGINT' => 'Describes the bigint SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_BINARY' => 'Describes the binary SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_BIT' => 'Describes the bit SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_CHAR' => 'Describes the char SQL Server data type. This constant works like a function and accepts a parameter indicating the number characters. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_DATE' => 'Describes the date SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_DATETIME' => 'Describes the datetime SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_DATETIME2' => 'Describes the datetime2 SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_DATETIMEOFFSET' => 'Describes the datetimeoffset SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_DECIMAL' => 'Describes the decimal SQL Server data type. This constant works like a function and accepts two parameters indicating (in order) precision and scale. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_FLOAT' => 'Describes the float SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_IMAGE' => 'Describes the image SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_INT' => 'Describes the int SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_MONEY' => 'Describes the money SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_NCHAR' => 'Describes the nchar SQL Server data type. This constant works like a function and accepts a single parameter indicating the character count. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_NTEXT' => 'Describes the ntext SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_NUMERIC' => 'Describes the numeric SQL Server data type. This constant works like a function and accepts two parameter indicating (in order) precision and scale. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_NVARCHAR' => 'Describes the nvarchar(MAX) SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_REAL' => 'Describes the real SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_SMALLDATETIME' => 'Describes the smalldatetime SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_SMALLINT' => 'Describes the smallint SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_SMALLMONEY' => 'Describes the smallmoney SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_TEXT' => 'Describes the text SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_TIME' => 'Describes the time SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_TIMESTAMP' => 'Describes the timestamp SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_TINYINT' => 'Describes the tinyint SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_UDT' => 'Describes the UDT SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_UNIQUEIDENTIFIER' => 'Describes the uniqueidentifier SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_VARBINARY' => 'Describes the varbinary(MAX) SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_VARCHAR' => 'Describes the varchar(MAX) SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_SQLTYPE_XML' => 'Describes the XML SQL Server data type. For usage information, see How to: Specify SQL Types.', +'SQLSRV_TXN_READ_COMMITTED' => 'Indicates a transaction isolation level of READ COMMITTED. This value is used to set the TransactionIsolation level in the $connectionOptions array passed to `sqlsrv_connect`.', +'SQLSRV_TXN_READ_SERIALIZABLE' => 'Indicates a transaction isolation level of SERIALIZABLE. This value is used to set the TransactionIsolation level in the $connectionOptions array passed to `sqlsrv_connect`.', +'SQLSRV_TXN_READ_UNCOMMITTED' => 'Indicates a transaction isolation level of READ UNCOMMITTED. This value is used to set the TransactionIsolation level in the $connectionOptions array passed to `sqlsrv_connect`.', +'SQLSRV_TXN_REPEATABLE_READ' => 'Indicates a transaction isolation level of REPEATABLE READ. This value is used to set the TransactionIsolation level in the $connectionOptions array passed to `sqlsrv_connect`.', +'SQLSRV_TXN_SNAPSHOT' => 'Indicates a transaction isolation level of SNAPSHOT. This value is used to set the TransactionIsolation level in the $connectionOptions array passed to `sqlsrv_connect`.', +'SQLTEXT' => 'Indicates the \'`TEXT`\' type in MSSQL, used by `mssql_bind`\'s `type` parameter.', +'SQLVARCHAR' => 'Indicates the \'`VARCHAR`\' type in MSSQL, used by `mssql_bind`\'s `type` parameter.', +'SSH2_DEFAULT_TERM_HEIGHT' => 'Default terminal height requested by `ssh2_shell`.', +'SSH2_DEFAULT_TERM_UNIT' => 'Default terminal units requested by `ssh2_shell`.', +'SSH2_DEFAULT_TERM_WIDTH' => 'Default terminal width requested by `ssh2_shell`.', +'SSH2_DEFAULT_TERMINAL' => 'Default terminal type (e.g. vt102, ansi, xterm, vanilla) requested by `ssh2_shell`.', +'SSH2_FINGERPRINT_HEX' => 'Flag to `ssh2_fingerprint` requesting hostkey fingerprint as a string of hexits.', +'SSH2_FINGERPRINT_MD5' => 'Flag to `ssh2_fingerprint` requesting hostkey fingerprint as an MD5 hash.', +'SSH2_FINGERPRINT_RAW' => 'Flag to `ssh2_fingerprint` requesting hostkey fingerprint as a raw string of 8-bit characters.', +'SSH2_FINGERPRINT_SHA1' => 'Flag to `ssh2_fingerprint` requesting hostkey fingerprint as an SHA1 hash.', +'SSH2_STREAM_STDERR' => 'Flag to `ssh2_fetch_stream` requesting STDERR subchannel.', +'SSH2_STREAM_STDIO' => 'Flag to `ssh2_fetch_stream` requesting STDIO subchannel.', +'SSH2_TERM_UNIT_CHARS' => 'Flag to `ssh2_shell` specifying that width and height are provided as character sizes.', +'SSH2_TERM_UNIT_PIXELS' => 'Flag to `ssh2_shell` specifying that width and height are provided in pixel units.', +'ST_UID' => 'The sequence argument contains UIDs instead of sequence numbers', +'STDERR' => 'An already opened stream to *stderr* (standard error).', +'STDIN' => 'An already opened stream to *stdin* (standard input).', +'STDOUT' => 'An already opened stream to *stdout* (standard output).', +'SUNFUNCS_RET_DOUBLE' => 'Hours as floating point number (example 8.75)', +'SUNFUNCS_RET_STRING' => 'Hours:minutes (example: 08:02)', +'SUNFUNCS_RET_TIMESTAMP' => 'Timestamp', +'SVN_AUTH_PARAM_DEFAULT_PASSWORD' => 'Property for default password to use when performing basic authentication', +'SVN_AUTH_PARAM_DEFAULT_USERNAME' => 'Property for default username to use when performing basic authentication', +'SVN_FS_CONFIG_FS_TYPE' => 'Configuration key that determines filesystem type', +'SVN_FS_TYPE_BDB' => 'Filesystem is Berkeley-DB implementation', +'SVN_FS_TYPE_FSFS' => 'Filesystem is native-filesystem implementation', +'SVN_NODE_DIR' => 'Directory', +'SVN_NODE_FILE' => 'File', +'SVN_NODE_NONE' => 'Absent', +'SVN_NODE_UNKNOWN' => 'Something Subversion cannot identify', +'SVN_PROP_REVISION_AUTHOR' => 'svn:author', +'SVN_PROP_REVISION_DATE' => 'svn:date', +'SVN_PROP_REVISION_LOG' => 'svn:log', +'SVN_PROP_REVISION_ORIG_DATE' => 'svn:original-date', +'SVN_REVISION_HEAD' => 'Magic number (-1) specifying the HEAD revision', +'SVN_WC_STATUS_ADDED' => 'Item is scheduled for addition', +'SVN_WC_STATUS_CONFLICTED' => 'Item\'s local modifications conflicted with repository modifications', +'SVN_WC_STATUS_DELETED' => 'Item is scheduled for deletion', +'SVN_WC_STATUS_EXTERNAL' => 'Unversioned path that is populated using svn:externals', +'SVN_WC_STATUS_IGNORED' => 'Item is unversioned but configured to be ignored', +'SVN_WC_STATUS_INCOMPLETE' => 'Directory does not contain complete entries list', +'SVN_WC_STATUS_MERGED' => 'Item\'s local modifications were merged with repository modifications', +'SVN_WC_STATUS_MISSING' => 'Item is versioned but missing from the working copy', +'SVN_WC_STATUS_MODIFIED' => 'Item (text or properties) was modified', +'SVN_WC_STATUS_NONE' => 'Status does not exist', +'SVN_WC_STATUS_NORMAL' => 'Item exists, nothing else is happening', +'SVN_WC_STATUS_OBSTRUCTED' => 'Unversioned item is in the way of a versioned resource', +'SVN_WC_STATUS_REPLACED' => 'Item was deleted and then re-added', +'SVN_WC_STATUS_UNVERSIONED' => 'Item is not versioned in working copy', +'TCP_NODELAY' => 'Used to disable Nagle TCP algorithm. Added in PHP 5.2.7.', +'TRAP_BRKPT' => 'Available since PHP 5.3.0.', +'TRAP_TRACE' => 'Available since PHP 5.3.0.', +'TYPEAPPLICATION' => 'Primary body type: application data', +'TYPEAUDIO' => 'Primary body type: audio', +'TYPEIMAGE' => 'Primary body type: static image', +'TYPEMESSAGE' => 'Primary body type: encapsulated message', +'TYPEMODEL' => 'Primary body type: model', +'TYPEMULTIPART' => 'Primary body type: multiple part', +'TYPEOTHER' => 'Primary body type: unknown', +'TYPETEXT' => 'Primary body type: unformatted text', +'TYPEVIDEO' => 'Primary body type: video', +'UTF-8' => 'Specifies that data is returned with UTF-8 encoding. For usage information, see How to: Specify PHP Types.', +'XATTR_CREATE' => 'Function will fail if extended attribute already exists.', +'XATTR_DONTFOLLOW' => 'Do not follow the symbolic link but operate on symbolic link itself.', +'XATTR_REPLACE' => 'Function will fail if extended attribute doesn\'t exist.', +'XATTR_ROOT' => 'Set attribute in root (trusted) namespace. Requires root privileges.', +'XDIFF_PATCH_NORMAL' => 'This flag indicates that `xdiff_string_patch` and `xdiff_file_patch` functions should create result by applying patch to original content thus creating newer version of file. This is the default mode of operation.', +'XDIFF_PATCH_REVERSE' => 'This flag indicated that `xdiff_string_patch` and `xdiff_file_patch` functions should create result by reversing patch changed from newer content thus creating original version.', +'XHPROF_FLAGS_CPU' => 'Used to add CPU profiling information to the output.', +'XHPROF_FLAGS_MEMORY' => 'Used to add memory profiling information to the output.', +'XHPROF_FLAGS_NO_BUILTINS' => 'Used to skip all built-in (internal) functions.', +'XML_SAX_IMPL' => 'Holds the SAX implementation method. Can be `libxml` or `expat`.', +'XSL_SECPREF_CREATE_DIRECTORY' => 'Disallows creating directories.', +'XSL_SECPREF_DEFAULT' => 'Disallows all write access, i.e. a bitmask of `XSL_SECPREF_WRITE_NETWORK` | `XSL_SECPREF_CREATE_DIRECTORY` | `XSL_SECPREF_WRITE_FILE`.', +'XSL_SECPREF_NONE' => 'Deactivate all security restrictions.', +'XSL_SECPREF_READ_FILE' => 'Disallows reading files.', +'XSL_SECPREF_READ_NETWORK' => 'Disallows reading network files.', +'XSL_SECPREF_WRITE_FILE' => 'Disallows writing files.', +'XSL_SECPREF_WRITE_NETWORK' => 'Disallows writing network files.', +'YAC_MAX_KEY_LEN' => 'Max length of a key could be, it is 48 bytes.', +'YAC_SERIALIZER' => 'Which serialzier is yac used', +'YAC_SERIALIZER_IGBINARY' => 'Use igbinary as serializer(require --enable-igbinary)', +'YAC_SERIALIZER_JSON' => 'Use json as serializer(requrie --enable-json)', +'YAC_SERIALIZER_MSGPACK' => 'Use msgpack as serializer(require --enable-msgpack)', +'YAC_SERIALIZER_PHP' => 'Use php serialize as serializer', +'YAML_ANY_BREAK' => 'Let emitter choose linebreak character.', +'YAML_ANY_ENCODING' => 'Let the emitter choose an encoding.', +'YAML_BOOL_TAG' => '"tag:yaml.org,2002:bool"', +'YAML_CR_BREAK' => 'Use `\r` as break character (Mac style).', +'YAML_CRLN_BREAK' => 'Use `\r\n` as break character (DOS style).', +'YAML_FLOAT_TAG' => '"tag:yaml.org,2002:float"', +'YAML_INT_TAG' => '"tag:yaml.org,2002:int"', +'YAML_LN_BREAK' => 'Use `\n` as break character (Unix style).', +'YAML_MAP_TAG' => '"tag:yaml.org,2002:map"', +'YAML_NULL_TAG' => '"tag:yaml.org,2002:null"', +'YAML_PHP_TAG' => '"!php/object"', +'YAML_SEQ_TAG' => '"tag:yaml.org,2002:seq"', +'YAML_STR_TAG' => '"tag:yaml.org,2002:str"', +'YAML_TIMESTAMP_TAG' => '"tag:yaml.org,2002:timestamp"', +'YAML_UTF16BE_ENCODING' => 'Encode as UTF16BE.', +'YAML_UTF16LE_ENCODING' => 'Encode as UTF16LE.', +'YAML_UTF8_ENCODING' => 'Encode as UTF8.', +'YAR_OPT_HEADER' => 'Since 2.0.4', +'YPERR_ACCESS' => 'access violation (this has only been added recently and is only available from PECL SVN for now)', +'YPERR_BADARGS' => 'The function arguments are bad', +'YPERR_BADDB' => 'YP database is bad', +'YPERR_BUSY' => 'Database busy', +'YPERR_DOMAIN' => 'cannot bind to server in this domain', +'YPERR_KEY' => 'no such key in map', +'YPERR_MAP' => 'no such map in server\'s domain', +'YPERR_NODOM' => 'Local domain name not set', +'YPERR_NOMORE' => 'No more records in map database', +'YPERR_PMAP' => 'Can\'t communicate with portmapper', +'YPERR_RESRC' => 'resource allocation failure', +'YPERR_RPC' => 'RPC failure - domain has been unbound', +'YPERR_VERS' => 'YP version mismatch', +'YPERR_YPBIND' => 'Can\'t communicate with ypbind', +'YPERR_YPERR' => 'internal yp server or client error', +'YPERR_YPSERV' => 'Can\'t communicate with ypserv', +'ZEND_ACC_ABSTRACT' => 'Mark function as abstract', +'ZEND_ACC_CLASS' => 'Dummy registered for consistency, the default kind of class entry. Removed as of uopz 5.0.0.', +'ZEND_ACC_FETCH' => 'Used for getting flags only. Removed as of uopz 5.0.0.', +'ZEND_ACC_FINAL' => 'Mark function as final', +'ZEND_ACC_INTERFACE' => 'Mark class as interface. Removed as of uopz 5.0.0.', +'ZEND_ACC_PRIVATE' => 'Mark function as private', +'ZEND_ACC_PROTECTED' => 'Mark function as protected', +'ZEND_ACC_PUBLIC' => 'Mark function as public, the default', +'ZEND_ACC_STATIC' => 'Mark function as static', +'ZEND_ACC_TRAIT' => 'Mark class as trait. Removed as of uopz 5.0.0.', +'ZEND_ADD_INTERFACE' => 'Invoked upon composure, receives the class the interface is being added to as the first argument, and the name of the interface as the second argument', +'ZEND_ADD_TRAIT' => 'Invoked upon composure, receives the class the trait is being added to as the first argument, and the name of the trait as the second argument', +'ZEND_EXIT' => 'Invoked by exit() and die(), receives no arguments. Return boolean `true` to exit, `false` to continue', +'ZEND_FETCH_CLASS' => 'Invoked upon composure, receives the class the name of the class being fetched as the only argument', +'ZEND_INSTANCEOF' => 'Invoked by instanceof operator, receives the object being verified as the first argument, and the name of the class which that object should be as the second argument', +'ZEND_NEW' => 'Invoked by object construction, receives the class of object being created as the only argument', +'ZEND_THROW' => 'Invoked by the throw construct, receives the class of exception being thrown as the only argument', +'ZEND_USER_OPCODE_CONTINUE' => 'Advance 1 opcode and continuue', +'ZEND_USER_OPCODE_DISPATCH' => 'Dispatch to original opcode handler', +'ZEND_USER_OPCODE_DISPATCH_TO' => 'Dispatch to a specific handler (OR\'d with ZEND opcode constant)', +'ZEND_USER_OPCODE_ENTER' => 'Enter into new op_array without recursion', +'ZEND_USER_OPCODE_LEAVE' => 'Return to calling op_array within the same executor', +'ZEND_USER_OPCODE_RETURN' => 'Exit from executor (return from function)', +'ZipArchive::CHECKCONS' => 'Perform additional consistency checks on the archive, and error if they fail.', +'ZipArchive::CM_BZIP2' => 'BZIP2 algorithm', +'ZipArchive::CM_DEFAULT' => 'better of deflate or store.', +'ZipArchive::CM_DEFLATE' => 'deflated', +'ZipArchive::CM_DEFLATE64' => 'deflate64', +'ZipArchive::CM_IMPLODE' => 'imploded', +'ZipArchive::CM_LZMA' => 'LZMA algorithm', +'ZipArchive::CM_LZMA2' => 'LZMA2 algorithm. Available as of PHP 7.4.3 and PECL zip 1.16.0, respectively, if built against libzip ≥ 1.6.0.', +'ZipArchive::CM_PKWARE_IMPLODE' => 'PKWARE imploding', +'ZipArchive::CM_REDUCE_1' => 'reduced with factor 1', +'ZipArchive::CM_REDUCE_2' => 'reduced with factor 2', +'ZipArchive::CM_REDUCE_3' => 'reduced with factor 3', +'ZipArchive::CM_REDUCE_4' => 'reduced with factor 4', +'ZipArchive::CM_SHRINK' => 'shrunk', +'ZipArchive::CM_STORE' => 'stored (uncompressed).', +'ZipArchive::CM_XZ' => 'XZ algorithm. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively, if built against libzip ≥ 1.6.0.', +'ZipArchive::CREATE' => 'Create the archive if it does not exist.', +'ZipArchive::EM_AES_128' => 'AES 128 encryption, since PHP 7.2.0, PECL zip 1.14.0', +'ZipArchive::EM_AES_192' => 'AES 1192 encryption, since PHP 7.2.0, PECL zip 1.14.0', +'ZipArchive::EM_AES_256' => 'AES 256 encryption, since PHP 7.2.0, PECL zip 1.14.0', +'ZipArchive::EM_NONE' => 'No encryption, since PHP 7.2.0, PECL zip 1.14.0', +'ZipArchive::ER_CANCELLED' => 'Operation cancelled. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively, if built against libzip ≥ 1.6.0.', +'ZipArchive::ER_CHANGED' => 'Entry has been changed', +'ZipArchive::ER_CLOSE' => 'Closing zip archive failed', +'ZipArchive::ER_COMPNOTSUPP' => 'Compression method not supported.', +'ZipArchive::ER_CRC' => 'CRC error', +'ZipArchive::ER_DELETED' => 'Entry has been deleted', +'ZipArchive::ER_ENCRNOTSUPP' => 'Encryption method not supported. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively.', +'ZipArchive::ER_EOF' => 'Premature EOF', +'ZipArchive::ER_EXISTS' => 'File already exists', +'ZipArchive::ER_INCONS' => 'Zip archive inconsistent', +'ZipArchive::ER_INTERNAL' => 'Internal error', +'ZipArchive::ER_INVAL' => 'Invalid argument', +'ZipArchive::ER_MEMORY' => 'Memory allocation failure', +'ZipArchive::ER_MULTIDISK' => 'Multi-disk zip archives not supported.', +'ZipArchive::ER_NOENT' => 'No such file.', +'ZipArchive::ER_NOPASSWD' => 'No password provided. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively.', +'ZipArchive::ER_NOZIP' => 'Not a zip archive', +'ZipArchive::ER_OK' => 'No error.', +'ZipArchive::ER_OPEN' => 'Can\'t open file', +'ZipArchive::ER_RDONLY' => 'Read-only archive. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively.', +'ZipArchive::ER_READ' => 'Read error', +'ZipArchive::ER_REMOVE' => 'Can\'t remove file', +'ZipArchive::ER_RENAME' => 'Renaming temporary file failed.', +'ZipArchive::ER_SEEK' => 'Seek error', +'ZipArchive::ER_TMPOPEN' => 'Failure to create temporary file.', +'ZipArchive::ER_WRITE' => 'Write error', +'ZipArchive::ER_WRONGPASSWD' => 'Wrong password provided. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively.', +'ZipArchive::ER_ZIPCLOSED' => 'Containing zip archive was closed', +'ZipArchive::ER_ZLIB' => 'Zlib error', +'ZipArchive::EXCL' => 'Error if archive already exists.', +'ZipArchive::FL_COMPRESSED' => 'Read compressed data', +'ZipArchive::FL_ENC_CP437' => 'String is CP437 encoded. Available as of PHP 7.0.8.', +'ZipArchive::FL_ENC_GUESS' => 'Guess string encoding (is default). Available as of PHP 7.0.8.', +'ZipArchive::FL_ENC_RAW' => 'Get unmodified string. Available as of PHP 7.0.8.', +'ZipArchive::FL_ENC_STRICT' => 'Follow specification strictly. Available as of PHP 7.0.8.', +'ZipArchive::FL_ENC_UTF_8' => 'String is UTF-8 encoded. Available as of PHP 7.0.8.', +'ZipArchive::FL_ENCRYPTED' => 'Read encrypted data (implies FL_COMPRESSED). Available as of PHP 8.0.0 and PECL zip 1.18.0.', +'ZipArchive::FL_LOCAL' => 'In local header. Available as of PHP 8.0.0 and PECL zip 1.18.0.', +'ZipArchive::FL_NOCASE' => 'Ignore case on name lookup', +'ZipArchive::FL_NODIR' => 'Ignore directory component', +'ZipArchive::FL_OVERWRITE' => 'If file with name exists, overwrite (replace) it. Available as of PHP 8.0.0 and PECL zip 1.18.0.', +'ZipArchive::FL_RECOMPRESS' => 'Force recompression of data. Available as of PHP 8.0.0 and PECL zip 1.18.0.', +'ZipArchive::FL_UNCHANGED' => 'Use original data, ignoring changes.', +'ZipArchive::LIBZIP_VERSION' => 'Zip library version. Available as of PHP 7.4.3 and PECL zip 1.16.0.', +'ZipArchive::OPSYS_DOS' => 'Since PHP 5.6.0, PECL zip 1.12.4', +'ZipArchive::OVERWRITE' => 'If archive exists, ignore its current contents. In other words, handle it the same way as an empty archive.', +'ZipArchive::RDONLY' => 'Open archive in read only mode. Available as of PHP 7.4.3 and PECL zip 1.17.1, respectively, if built against libzip ≥ 1.0.0.', +'ZipArchive::ZIP_ER_COMPRESSED_DATA' => 'Compressed data invalid. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively, if built against libzip ≥ 1.6.0.', +'ZipArchive::ZIP_ER_INUSE' => 'Resource still in use. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively, if built against libzip ≥ 1.0.0.', +'ZipArchive::ZIP_ER_OPNOTSUPP' => 'Operation not supported. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively, if built against libzip ≥ 1.0.0.', +'ZipArchive::ZIP_ER_TELL' => 'Tell error. Available as of PHP 7.4.3 and PECL zip 1.16.1, respectively, if built against libzip ≥ 1.0.0.', +'ZipArchive::ZIP_FL_CENTRAL' => 'In central directory. Available as of PHP 8.0.0 and PECL zip 1.18.0.', +'ZLIB_BLOCK' => 'Available as of PHP 7.0.0.', +'ZLIB_DEFAULT_STRATEGY' => 'Available as of PHP 7.0.0.', +'ZLIB_ENCODING_DEFLATE' => 'ZLIB compression algorithm as per RFC 1950. Available as of PHP 7.0.0.', +'ZLIB_ENCODING_GZIP' => 'GZIP algorithm as per RFC 1952. Available as of PHP 7.0.0.', +'ZLIB_ENCODING_RAW' => 'DEFLATE algorithm as per RFC 1951. Available as of PHP 7.0.0.', +'ZLIB_FILTERED' => 'Available as of PHP 7.0.0.', +'ZLIB_FINISH' => 'Available as of PHP 7.0.0.', +'ZLIB_FIXED' => 'Available as of PHP 7.0.0.', +'ZLIB_FULL_FLUSH' => 'Available as of PHP 7.0.0.', +'ZLIB_HUFFMAN_ONLY' => 'Available as of PHP 7.0.0.', +'ZLIB_NO_FLUSH' => 'Available as of PHP 7.0.0.', +'ZLIB_PARTIAL_FLUSH' => 'Available as of PHP 7.0.0.', +'ZLIB_RLE' => 'Available as of PHP 7.0.0.', +'ZLIB_SYNC_FLUSH' => 'Available as of PHP 7.0.0.', +'ZMQ::CTXOPT_MAX_SOCKETS' => 'The socket limit for this context. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::DEVICE_FORWARDER' => 'Forwarder device', +'ZMQ::DEVICE_QUEUE' => 'Queue device', +'ZMQ::DEVICE_STREAMER' => 'Streamer device', +'ZMQ::ERR_EAGAIN' => 'Implies that the operation would block when ZMQ::MODE_DONTWAIT is used', +'ZMQ::ERR_EFSM' => 'The operation can not be executed because the socket is not in correct state', +'ZMQ::ERR_ENOTSUP' => 'The operation is not supported by the socket type', +'ZMQ::ERR_ETERM' => 'The context has been terminated', +'ZMQ::ERR_INTERNAL' => 'ZMQ extension internal error', +'ZMQ::MODE_DONTWAIT' => 'Non-blocking operation', +'ZMQ::MODE_NOBLOCK' => 'Non-blocking operation.', +'ZMQ::MODE_SNDMORE' => 'Send multi-part message', +'ZMQ::POLL_IN' => 'Poll for incoming data', +'ZMQ::POLL_OUT' => 'Poll for outgoing data', +'ZMQ::SOCKET_DEALER' => 'Extended REQ socket that load balances to all connected peers', +'ZMQ::SOCKET_PAIR' => 'Exclusive pair pattern', +'ZMQ::SOCKET_PUB' => 'Publisher socket', +'ZMQ::SOCKET_PULL' => 'Pipeline downstream pull socket', +'ZMQ::SOCKET_PUSH' => 'Pipeline upstream push socket', +'ZMQ::SOCKET_REP' => 'Reply socket', +'ZMQ::SOCKET_REQ' => 'Request socket', +'ZMQ::SOCKET_ROUTER' => 'Extended REP socket that can route replies to requesters', +'ZMQ::SOCKET_STREAM' => 'Used to send and receive TCP data from a non-ØMQ peer. +Available if compiled against ZeroMQ 4.x or higher.', +'ZMQ::SOCKET_SUB' => 'Subscriber socket', +'ZMQ::SOCKET_XPUB' => 'Similar to SOCKET_PUB, except you can receive subscriptions as messages. +The subscription message is 0 (unsubscribe) or 1 (subscribe) followed by the topic.', +'ZMQ::SOCKET_XREP' => 'Alias for SOCKET_ROUTER', +'ZMQ::SOCKET_XREQ' => 'Alias for SOCKET_DEALER', +'ZMQ::SOCKET_XSUB' => 'Similar to SOCKET_SUB, except you can send subscriptions as messages. See SOCKET_XPUB for format.', +'ZMQ::SOCKOPT_AFFINITY' => 'Set I/O thread affinity', +'ZMQ::SOCKOPT_BACKLOG' => 'The SOCKOPT_BACKLOG option shall set the maximum length of the queue of outstanding peer connections +for the specified socket; this only applies to connection-oriented transports.', +'ZMQ::SOCKOPT_DELAY_ATTACH_ON_CONNECT' => 'Set a CIDR string to match against incoming TCP connections. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::SOCKOPT_HWM' => 'The high water mark for inbound and outbound messages is a hard +limit on the maximum number of outstanding messages ØMQ shall queue in memory +for any single peer that the specified socket is communicating with. +Setting this option on a socket will only affect connections made after the option has been set. +On ZeroMQ 3.x this is a wrapper for setting both SNDHWM and RCVHWM.', +'ZMQ::SOCKOPT_IDENTITY' => 'Set socket identity', +'ZMQ::SOCKOPT_IPV4ONLY' => 'Disable IPV6 support if 1. +Available if compiled against ZeroMQ 3.x', +'ZMQ::SOCKOPT_IPV6' => 'Enable IPV6. +Available if compiled against ZeroMQ 4.0 or higher', +'ZMQ::SOCKOPT_LAST_ENDPOINT' => 'Retrieve the last connected endpoint - for use with * wildcard ports. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::SOCKOPT_LINGER' => 'The linger value of the socket. +Specifies how long the socket blocks trying flush messages after it has been closed', +'ZMQ::SOCKOPT_MAXMSGSIZE' => 'Limits the maximum size of the inbound message. Value -1 means no limit. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::SOCKOPT_MCAST_LOOP' => 'Control multicast loopback (Value: int >= 0)', +'ZMQ::SOCKOPT_RATE' => 'Set rate for multicast sockets (pgm) (Value: int >= 0)', +'ZMQ::SOCKOPT_RCVBUF' => 'Set kernel receive buffer size (Value: int >= 0)', +'ZMQ::SOCKOPT_RCVHWM' => 'The ZMQ_SNDHWM option shall set the high water mark for inbound messages on the specified socket. +Available if compiled against ZeroMQ 3.x or higher.', +'ZMQ::SOCKOPT_RCVMORE' => 'Receive multi-part messages', +'ZMQ::SOCKOPT_RCVTIMEO' => 'Sets the timeout for receive operation on the socket. Value -1 means no limit. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::SOCKOPT_RECONNECT_IVL' => 'Set the initial reconnection interval (Value: int >= 0)', +'ZMQ::SOCKOPT_RECONNECT_IVL_MAX' => 'Set the max reconnection interval (Value: int >= 0)', +'ZMQ::SOCKOPT_RECOVERY_IVL' => 'Set multicast recovery interval (Value: int >= 0)', +'ZMQ::SOCKOPT_ROUTER_RAW' => 'Sets the raw mode on the ROUTER, when set to 1. +In raw mode when using tcp:// transport the socket will read and write without ZeroMQ framing. +Available if compiled against ZeroMQ 4.0 or higher', +'ZMQ::SOCKOPT_SNDBUF' => 'Set kernel transmit buffer size (Value: int >= 0)', +'ZMQ::SOCKOPT_SNDHWM' => 'The ZMQ_SNDHWM option shall set the high water mark for outbound messages on the specified socket. +Available if compiled against ZeroMQ 3.x or higher.', +'ZMQ::SOCKOPT_SNDTIMEO' => 'Sets the timeout for send operation on the socket. Value -1 means no limit. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::SOCKOPT_SUBSCRIBE' => 'Establish message filter. Valid for subscriber socket', +'ZMQ::SOCKOPT_TCP_ACCEPT_FILTER' => 'Set a CIDR string to match against incoming TCP connections. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::SOCKOPT_TCP_KEEPALIVE_CNT' => 'Count time for TCP keepalive. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::SOCKOPT_TCP_KEEPALIVE_IDLE' => 'Idle time for TCP keepalive. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::SOCKOPT_TCP_KEEPALIVE_INTVL' => 'Interval for TCP keepalive. +Available if compiled against ZeroMQ 3.x or higher', +'ZMQ::SOCKOPT_TYPE' => 'Get the socket type. Valid for getSockOpt', +'ZMQ::SOCKOPT_UNSUBSCRIBE' => 'Remove message filter. Valid for subscriber socket', +'ZMQ::SOCKOPT_XPUB_VERBOSE' => 'Set the XPUB to receive an application message on each instance of a subscription. +Available if compiled against ZeroMQ 3.x or higher', +]; diff --git a/bundled-libs/phan/phan/src/Phan/Language/Internal/DynamicPropertyMap.php b/bundled-libs/phan/phan/src/Phan/Language/Internal/DynamicPropertyMap.php new file mode 100644 index 000000000..b0308be9e --- /dev/null +++ b/bundled-libs/phan/phan/src/Phan/Language/Internal/DynamicPropertyMap.php @@ -0,0 +1,17 @@ +' => 'documentation', + * + * NOTE: This format will very likely change as information is added and should not be used directly. + * + * Sources of function/method summary info: + * + * 1. docs.php.net's SVN repo or website, and examples (See internal/internalsignatures.php) + * + * See https://secure.php.net/manual/en/copyright.php + * + * The PHP manual text and comments are covered by the [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/legalcode), + * copyright (c) the PHP Documentation Group + * 2. Various websites documenting individual extensions (e.g. php-ast) + * 3. PHPStorm stubs (for anything missing from the above sources) + * See internal/internalsignatures.php + * + * Available from https://github.com/JetBrains/phpstorm-stubs under the [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0) + * + * CONTRIBUTING: + * + * Running `internal/internalstubs.php` can be used to update signature maps + * + * There are no plans for these signatures to diverge from what the above upstream sources contain. + * + * - If the descriptions cause Phan to crash, bug reports are welcome + * - If Phan improperly extracted text from a summary (and this affects multiple signatures), patches fixing the extraction will be accepted. + * - Otherwise, fixes for typos/grammar/inaccuracies in the summary will only be accepted once they are contributed upstream and can be regenerated (e.g. to the svn repo for docs.php.net). + * + * Note that the summaries are used in a wide variety of contexts, and what makes sense for Phan may not make sense for those projects, and vice versa. + */ +return [ +'__halt_compiler' => 'Halts the compiler execution', +'abs' => 'Absolute value', +'acos' => 'Arc cosine', +'acosh' => 'Inverse hyperbolic cosine', +'addcslashes' => 'Quote string with slashes in a C style', +'addslashes' => 'Quote string with slashes', +'AMQPBasicProperties::getAppId' => 'Get the application id of the message.', +'AMQPBasicProperties::getClusterId' => 'Get the cluster id of the message.', +'AMQPBasicProperties::getContentEncoding' => 'Get the content encoding of the message.', +'AMQPBasicProperties::getContentType' => 'Get the message content type.', +'AMQPBasicProperties::getCorrelationId' => 'Get the message correlation id.', +'AMQPBasicProperties::getDeliveryMode' => 'Get the delivery mode of the message.', +'AMQPBasicProperties::getExpiration' => 'Get the expiration of the message.', +'AMQPBasicProperties::getHeaders' => 'Get the headers of the message.', +'AMQPBasicProperties::getMessageId' => 'Get the message id of the message.', +'AMQPBasicProperties::getPriority' => 'Get the priority of the message.', +'AMQPBasicProperties::getReplyTo' => 'Get the reply-to address of the message.', +'AMQPBasicProperties::getTimestamp' => 'Get the timestamp of the message.', +'AMQPBasicProperties::getType' => 'Get the message type.', +'AMQPBasicProperties::getUserId' => 'Get the message user id.', +'AMQPChannel::__construct' => 'Create an instance of an AMQPChannel object.', +'AMQPChannel::basicRecover' => 'Redeliver unacknowledged messages.', +'AMQPChannel::close' => 'Closes the channel.', +'AMQPChannel::commitTransaction' => 'Commit a pending transaction.', +'AMQPChannel::confirmSelect' => 'Set the channel to use publisher acknowledgements. This can only used on a non-transactional channel.', +'AMQPChannel::getChannelId' => 'Return internal channel ID', +'AMQPChannel::getConnection' => 'Get the AMQPConnection object in use', +'AMQPChannel::getConsumers' => 'Return array of current consumers where key is consumer and value is AMQPQueue consumer is running on', +'AMQPChannel::getPrefetchCount' => 'Get the number of messages to prefetch from the broker.', +'AMQPChannel::getPrefetchSize' => 'Get the window size to prefetch from the broker.', +'AMQPChannel::isConnected' => 'Check the channel connection.', +'AMQPChannel::qos' => 'Set the Quality Of Service settings for the given channel. + +Specify the amount of data to prefetch in terms of window size (octets) +or number of messages from a queue during a AMQPQueue::consume() or +AMQPQueue::get() method call. The client will prefetch data up to size +octets or count messages from the server, whichever limit is hit first. +Setting either value to 0 will instruct the client to ignore that +particular setting. A call to AMQPChannel::qos() will overwrite any +values set by calling AMQPChannel::setPrefetchSize() and +AMQPChannel::setPrefetchCount(). If the call to either +AMQPQueue::consume() or AMQPQueue::get() is done with the AMQP_AUTOACK +flag set, the client will not do any prefetching of data, regardless of +the QOS settings.', +'AMQPChannel::rollbackTransaction' => 'Rollback a transaction. + +Rollback an existing transaction. AMQPChannel::startTransaction() must +be called prior to this.', +'AMQPChannel::setConfirmCallback' => 'Set callback to process basic.ack and basic.nac AMQP server methods (applicable when channel in confirm mode).', +'AMQPChannel::setPrefetchCount' => 'Set the number of messages to prefetch from the broker. + +Set the number of messages to prefetch from the broker during a call to +AMQPQueue::consume() or AMQPQueue::get(). Any call to this method will +automatically set the prefetch window size to 0, meaning that the +prefetch window size setting will be ignored.', +'AMQPChannel::setPrefetchSize' => 'Set the window size to prefetch from the broker. + +Set the prefetch window size, in octets, during a call to +AMQPQueue::consume() or AMQPQueue::get(). Any call to this method will +automatically set the prefetch message count to 0, meaning that the +prefetch message count setting will be ignored. If the call to either +AMQPQueue::consume() or AMQPQueue::get() is done with the AMQP_AUTOACK +flag set, this setting will be ignored.', +'AMQPChannel::setReturnCallback' => 'Set callback to process basic.return AMQP server method', +'AMQPChannel::startTransaction' => 'Start a transaction. + +This method must be called on the given channel prior to calling +AMQPChannel::commitTransaction() or AMQPChannel::rollbackTransaction().', +'AMQPChannel::waitForBasicReturn' => 'Start wait loop for basic.return AMQP server methods', +'AMQPChannel::waitForConfirm' => 'Wait until all messages published since the last call have been either ack\'d or nack\'d by the broker. + +Note, this method also catch all basic.return message from server.', +'AMQPConnection::__construct' => 'Create an instance of AMQPConnection. + +Creates an AMQPConnection instance representing a connection to an AMQP +broker. A connection will not be established until +AMQPConnection::connect() is called. + + $credentials = array( + \'host\' => amqp.host The host to connect too. Note: Max 1024 characters. + \'port\' => amqp.port Port on the host. + \'vhost\' => amqp.vhost The virtual host on the host. Note: Max 128 characters. + \'login\' => amqp.login The login name to use. Note: Max 128 characters. + \'password\' => amqp.password Password. Note: Max 128 characters. + \'read_timeout\' => Timeout in for income activity. Note: 0 or greater seconds. May be fractional. + \'write_timeout\' => Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional. + \'connect_timeout\' => Connection timeout. Note: 0 or greater seconds. May be fractional. + + Connection tuning options (see https://www.rabbitmq.com/amqp-0-9-1-reference.html#connection.tune for details): + \'channel_max\' => Specifies highest channel number that the server permits. 0 means standard extension limit + (see PHP_AMQP_MAX_CHANNELS constant) + \'frame_max\' => The largest frame size that the server proposes for the connection, including frame header + and end-byte. 0 means standard extension limit (depends on librabbimq default frame size limit) + \'heartbeat\' => The delay, in seconds, of the connection heartbeat that the server wants. + 0 means the server does not want a heartbeat. Note, librabbitmq has limited heartbeat support, + which means heartbeats checked only during blocking calls. + + TLS support (see https://www.rabbitmq.com/ssl.html for details): + \'cacert\' => Path to the CA cert file in PEM format.. + \'cert\' => Path to the client certificate in PEM format. + \'key\' => Path to the client key in PEM format. + \'verify\' => Enable or disable peer verification. If peer verification is enabled then the common name in the + server certificate must match the server name. Peer verification is enabled by default. +)', +'AMQPConnection::connect' => 'Establish a transient connection with the AMQP broker. + +This method will initiate a connection with the AMQP broker.', +'AMQPConnection::disconnect' => 'Closes the transient connection with the AMQP broker. + +This method will close an open connection with the AMQP broker.', +'AMQPConnection::getCACert' => 'Get path to the CA cert file in PEM format', +'AMQPConnection::getCert' => 'Get path to the client certificate in PEM format', +'AMQPConnection::getHeartbeatInterval' => 'Get number of seconds between heartbeats of the connection in seconds. + +When connection is connected, effective connection value returned, which is normally the same as original +correspondent value passed to constructor, otherwise original value passed to constructor returned.', +'AMQPConnection::getHost' => 'Get the configured host.', +'AMQPConnection::getKey' => 'Get path to the client key in PEM format', +'AMQPConnection::getLogin' => 'Get the configured login.', +'AMQPConnection::getMaxChannels' => 'Get the maximum number of channels the connection can handle. + +When connection is connected, effective connection value returned, which is normally the same as original +correspondent value passed to constructor, otherwise original value passed to constructor returned.', +'AMQPConnection::getMaxFrameSize' => 'Get max supported frame size per connection in bytes. + +When connection is connected, effective connection value returned, which is normally the same as original +correspondent value passed to constructor, otherwise original value passed to constructor returned.', +'AMQPConnection::getPassword' => 'Get the configured password.', +'AMQPConnection::getPort' => 'Get the configured port.', +'AMQPConnection::getReadTimeout' => 'Get the configured interval of time to wait for income activity +from AMQP broker', +'AMQPConnection::getTimeout' => 'Get the configured interval of time to wait for income activity +from AMQP broker', +'AMQPConnection::getUsedChannels' => 'Return last used channel id during current connection session.', +'AMQPConnection::getVerify' => 'Get whether peer verification enabled or disabled', +'AMQPConnection::getVhost' => 'Get the configured vhost.', +'AMQPConnection::getWriteTimeout' => 'Get the configured interval of time to wait for outcome activity +to AMQP broker', +'AMQPConnection::isConnected' => 'Check whether the connection to the AMQP broker is still valid. + +It does so by checking the return status of the last connect-command.', +'AMQPConnection::isPersistent' => 'Whether connection persistent. + +When connection is not connected, boolean false always returned', +'AMQPConnection::pconnect' => 'Establish a persistent connection with the AMQP broker. + +This method will initiate a connection with the AMQP broker +or reuse an existing one if present.', +'AMQPConnection::pdisconnect' => 'Closes a persistent connection with the AMQP broker. + +This method will close an open persistent connection with the AMQP +broker.', +'AMQPConnection::preconnect' => 'Close any open persistent connections and initiate a new one with the AMQP broker.', +'AMQPConnection::reconnect' => 'Close any open transient connections and initiate a new one with the AMQP broker.', +'AMQPConnection::setCACert' => 'Set path to the CA cert file in PEM format', +'AMQPConnection::setCert' => 'Set path to the client certificate in PEM format', +'AMQPConnection::setHost' => 'Set the hostname used to connect to the AMQP broker.', +'AMQPConnection::setKey' => 'Set path to the client key in PEM format', +'AMQPConnection::setLogin' => 'Set the login string used to connect to the AMQP broker.', +'AMQPConnection::setPassword' => 'Set the password string used to connect to the AMQP broker.', +'AMQPConnection::setPort' => 'Set the port used to connect to the AMQP broker.', +'AMQPConnection::setReadTimeout' => 'Sets the interval of time to wait for income activity from AMQP broker', +'AMQPConnection::setTimeout' => 'Sets the interval of time to wait for income activity from AMQP broker', +'AMQPConnection::setVerify' => 'Enable or disable peer verification', +'AMQPConnection::setVhost' => 'Sets the virtual host to which to connect on the AMQP broker.', +'AMQPConnection::setWriteTimeout' => 'Sets the interval of time to wait for outcome activity to AMQP broker', +'AMQPEnvelope::getAppId' => 'Get the application id of the message.', +'AMQPEnvelope::getBody' => 'Get the body of the message.', +'AMQPEnvelope::getClusterId' => 'Get the cluster id of the message.', +'AMQPEnvelope::getConsumerTag' => 'Get the consumer tag of the message.', +'AMQPEnvelope::getContentEncoding' => 'Get the content encoding of the message.', +'AMQPEnvelope::getContentType' => 'Get the message content type.', +'AMQPEnvelope::getCorrelationId' => 'Get the message correlation id.', +'AMQPEnvelope::getDeliveryMode' => 'Get the delivery mode of the message.', +'AMQPEnvelope::getDeliveryTag' => 'Get the delivery tag of the message.', +'AMQPEnvelope::getExchangeName' => 'Get the exchange name on which the message was published.', +'AMQPEnvelope::getExpiration' => 'Get the expiration of the message.', +'AMQPEnvelope::getHeader' => 'Get a specific message header.', +'AMQPEnvelope::getHeaders' => 'Get the headers of the message.', +'AMQPEnvelope::getMessageId' => 'Get the message id of the message.', +'AMQPEnvelope::getPriority' => 'Get the priority of the message.', +'AMQPEnvelope::getReplyTo' => 'Get the reply-to address of the message.', +'AMQPEnvelope::getRoutingKey' => 'Get the routing key of the message.', +'AMQPEnvelope::getTimestamp' => 'Get the timestamp of the message.', +'AMQPEnvelope::getType' => 'Get the message type.', +'AMQPEnvelope::getUserId' => 'Get the message user id.', +'AMQPEnvelope::hasHeader' => 'Check whether specific message header exists.', +'AMQPEnvelope::isRedelivery' => 'Whether this is a redelivery of the message. + +Whether this is a redelivery of a message. If this message has been +delivered and AMQPEnvelope::nack() was called, the message will be put +back on the queue to be redelivered, at which point the message will +always return TRUE when this method is called.', +'AMQPExchange::__construct' => 'Create an instance of AMQPExchange. + +Returns a new instance of an AMQPExchange object, associated with the +given AMQPChannel object.', +'AMQPExchange::bind' => 'Bind to another exchange. + +Bind an exchange to another exchange using the specified routing key.', +'AMQPExchange::declare' => 'Declare a new exchange on the broker.', +'AMQPExchange::declareExchange' => 'Declare a new exchange on the broker.', +'AMQPExchange::delete' => 'Delete the exchange from the broker.', +'AMQPExchange::getArgument' => 'Get the argument associated with the given key.', +'AMQPExchange::getArguments' => 'Get all arguments set on the given exchange.', +'AMQPExchange::getChannel' => 'Get the AMQPChannel object in use', +'AMQPExchange::getConnection' => 'Get the AMQPConnection object in use', +'AMQPExchange::getFlags' => 'Get all the flags currently set on the given exchange.', +'AMQPExchange::getName' => 'Get the configured name.', +'AMQPExchange::getType' => 'Get the configured type.', +'AMQPExchange::hasArgument' => 'Check whether argument associated with the given key exists.', +'AMQPExchange::publish' => 'Publish a message to an exchange. + +Publish a message to the exchange represented by the AMQPExchange object.', +'AMQPExchange::setArgument' => 'Set the value for the given key.', +'AMQPExchange::setArguments' => 'Set all arguments on the exchange.', +'AMQPExchange::setFlags' => 'Set the flags on an exchange.', +'AMQPExchange::setName' => 'Set the name of the exchange.', +'AMQPExchange::setType' => 'Set the type of the exchange. + +Set the type of the exchange. This can be any of AMQP_EX_TYPE_DIRECT, +AMQP_EX_TYPE_FANOUT, AMQP_EX_TYPE_HEADERS or AMQP_EX_TYPE_TOPIC.', +'AMQPExchange::unbind' => 'Remove binding to another exchange. + +Remove a routing key binding on an another exchange from the given exchange.', +'AMQPQueue::__construct' => 'Create an instance of an AMQPQueue object.', +'AMQPQueue::ack' => 'Acknowledge the receipt of a message. + +This method allows the acknowledgement of a message that is retrieved +without the AMQP_AUTOACK flag through AMQPQueue::get() or +AMQPQueue::consume()', +'AMQPQueue::bind' => 'Bind the given queue to a routing key on an exchange.', +'AMQPQueue::cancel' => 'Cancel a queue that is already bound to an exchange and routing key.', +'AMQPQueue::consume' => 'Consume messages from a queue. + +Blocking function that will retrieve the next message from the queue as +it becomes available and will pass it off to the callback.', +'AMQPQueue::declare' => 'Declare a new queue', +'AMQPQueue::declareQueue' => 'Declare a new queue on the broker.', +'AMQPQueue::delete' => 'Delete a queue from the broker. + +This includes its entire contents of unread or unacknowledged messages.', +'AMQPQueue::get' => 'Retrieve the next message from the queue. + +Retrieve the next available message from the queue. If no messages are +present in the queue, this function will return FALSE immediately. This +is a non blocking alternative to the AMQPQueue::consume() method. +Currently, the only supported flag for the flags parameter is +AMQP_AUTOACK. If this flag is passed in, then the message returned will +automatically be marked as acknowledged by the broker as soon as the +frames are sent to the client.', +'AMQPQueue::getArgument' => 'Get the argument associated with the given key.', +'AMQPQueue::getArguments' => 'Get all set arguments as an array of key/value pairs.', +'AMQPQueue::getChannel' => 'Get the AMQPChannel object in use', +'AMQPQueue::getConnection' => 'Get the AMQPConnection object in use', +'AMQPQueue::getConsumerTag' => 'Get latest consumer tag. If no consumer available or the latest on was canceled null will be returned.', +'AMQPQueue::getFlags' => 'Get all the flags currently set on the given queue.', +'AMQPQueue::getName' => 'Get the configured name.', +'AMQPQueue::hasArgument' => 'Check whether a queue has specific argument.', +'AMQPQueue::nack' => 'Mark a message as explicitly not acknowledged. + +Mark the message identified by delivery_tag as explicitly not +acknowledged. This method can only be called on messages that have not +yet been acknowledged, meaning that messages retrieved with by +AMQPQueue::consume() and AMQPQueue::get() and using the AMQP_AUTOACK +flag are not eligible. When called, the broker will immediately put the +message back onto the queue, instead of waiting until the connection is +closed. This method is only supported by the RabbitMQ broker. The +behavior of calling this method while connected to any other broker is +undefined.', +'AMQPQueue::purge' => 'Purge the contents of a queue.', +'AMQPQueue::reject' => 'Mark one message as explicitly not acknowledged. + +Mark the message identified by delivery_tag as explicitly not +acknowledged. This method can only be called on messages that have not +yet been acknowledged, meaning that messages retrieved with by +AMQPQueue::consume() and AMQPQueue::get() and using the AMQP_AUTOACK +flag are not eligible.', +'AMQPQueue::setArgument' => 'Set a queue argument.', +'AMQPQueue::setArguments' => 'Set all arguments on the given queue. + +All other argument settings will be wiped.', +'AMQPQueue::setFlags' => 'Set the flags on the queue.', +'AMQPQueue::setName' => 'Set the queue name.', +'AMQPQueue::unbind' => 'Remove a routing key binding on an exchange from the given queue.', +'apache_child_terminate' => 'Terminate apache process after this request', +'apache_get_modules' => 'Get a list of loaded Apache modules', +'apache_get_version' => 'Fetch Apache version', +'apache_getenv' => 'Get an Apache subprocess_env variable', +'apache_lookup_uri' => 'Perform a partial request for the specified URI and return all info about it', +'apache_note' => 'Get and set apache request notes', +'apache_request_headers' => 'Fetch all HTTP request headers', +'apache_reset_timeout' => 'Reset the Apache write timer', +'apache_response_headers' => 'Fetch all HTTP response headers', +'apache_setenv' => 'Set an Apache subprocess_env variable', +'apc_add' => 'Cache a new variable in the data store', +'apc_bin_dump' => 'Get a binary dump of the given files and user variables', +'apc_bin_dumpfile' => 'Output a binary dump of cached files and user variables to a file', +'apc_bin_load' => 'Load a binary dump into the APC file/user cache', +'apc_bin_loadfile' => 'Load a binary dump from a file into the APC file/user cache', +'apc_cache_info' => 'Retrieves cached information from APC\'s data store', +'apc_cas' => 'Updates an old value with a new value', +'apc_clear_cache' => 'Clears the APC cache', +'apc_compile_file' => 'Stores a file in the bytecode cache, bypassing all filters', +'apc_dec' => 'Decrease a stored number', +'apc_define_constants' => 'Defines a set of constants for retrieval and mass-definition', +'apc_delete' => 'Removes a stored variable from the cache', +'apc_delete_file' => 'Deletes files from the opcode cache', +'apc_exists' => 'Checks if APC key exists', +'apc_fetch' => 'Fetch a stored variable from the cache', +'apc_inc' => 'Increase a stored number', +'apc_load_constants' => 'Loads a set of constants from the cache', +'apc_sma_info' => 'Retrieves APC\'s Shared Memory Allocation information', +'apc_store' => 'Cache a variable in the data store', +'apciterator::__construct' => 'Constructs an APCIterator iterator object', +'apciterator::current' => 'Get current item', +'apciterator::getTotalCount' => 'Get total count', +'apciterator::getTotalHits' => 'Get total cache hits', +'apciterator::getTotalSize' => 'Get total cache size', +'apciterator::key' => 'Get iterator key', +'apciterator::next' => 'Move pointer to next item', +'apciterator::rewind' => 'Rewinds iterator', +'apciterator::valid' => 'Checks if current position is valid', +'apcu_add' => 'Cache a new variable in the data store', +'apcu_cache_info' => 'Retrieves cached information from APCu\'s data store', +'apcu_cas' => 'Updates an old value with a new value', +'apcu_clear_cache' => 'Clears the APCu cache', +'apcu_dec' => 'Decrease a stored number', +'apcu_delete' => 'Removes a stored variable from the cache', +'apcu_enabled' => 'Whether APCu is usable in the current environment', +'apcu_entry' => 'Atomically fetch or generate a cache entry', +'apcu_exists' => 'Checks if entry exists', +'apcu_fetch' => 'Fetch a stored variable from the cache', +'apcu_inc' => 'Increase a stored number', +'apcu_sma_info' => 'Retrieves APCu Shared Memory Allocation information', +'apcu_store' => 'Cache a variable in the data store', +'apcuiterator::__construct' => 'Constructs an APCUIterator iterator object', +'apcuiterator::current' => 'Get current item', +'apcuiterator::getTotalCount' => 'Get total count', +'apcuiterator::getTotalHits' => 'Get total cache hits', +'apcuiterator::getTotalSize' => 'Get total cache size', +'apcuiterator::key' => 'Get iterator key', +'apcuiterator::next' => 'Move pointer to next item', +'apcuiterator::rewind' => 'Rewinds iterator', +'apcuiterator::valid' => 'Checks if current position is valid', +'apd_breakpoint' => 'Stops the interpreter and waits on a CR from the socket', +'apd_callstack' => 'Returns the current call stack as an array', +'apd_clunk' => 'Throw a warning and a callstack', +'apd_continue' => 'Restarts the interpreter', +'apd_croak' => 'Throw an error, a callstack and then exit', +'apd_dump_function_table' => 'Outputs the current function table', +'apd_dump_persistent_resources' => 'Return all persistent resources as an array', +'apd_dump_regular_resources' => 'Return all current regular resources as an array', +'apd_echo' => 'Echo to the debugging socket', +'apd_get_active_symbols' => 'Get an array of the current variables names in the local scope', +'apd_set_pprof_trace' => 'Starts the session debugging', +'apd_set_session' => 'Changes or sets the current debugging level', +'apd_set_session_trace' => 'Starts the session debugging', +'apd_set_session_trace_socket' => 'Starts the remote session debugging', +'appenditerator::__construct' => 'Constructs an AppendIterator', +'appenditerator::append' => 'Appends an iterator', +'appenditerator::current' => 'Gets the current value', +'appenditerator::getArrayIterator' => 'Gets the ArrayIterator', +'appenditerator::getInnerIterator' => 'Gets the inner iterator', +'appenditerator::getIteratorIndex' => 'Gets an index of iterators', +'appenditerator::key' => 'Gets the current key', +'appenditerator::next' => 'Moves to the next element', +'appenditerator::rewind' => 'Rewinds the Iterator', +'appenditerator::valid' => 'Checks validity of the current element', +'array_change_key_case' => 'Changes the case of all keys in an array', +'array_chunk' => 'Split an array into chunks', +'array_column' => 'Return the values from a single column in the input array', +'array_combine' => 'Creates an array by using one array for keys and another for its values', +'array_count_values' => 'Counts all the values of an array', +'array_diff' => 'Computes the difference of arrays', +'array_diff_assoc' => 'Computes the difference of arrays with additional index check', +'array_diff_key' => 'Computes the difference of arrays using keys for comparison', +'array_diff_uassoc' => 'Computes the difference of arrays with additional index check which is performed by a user supplied callback function', +'array_diff_ukey' => 'Computes the difference of arrays using a callback function on the keys for comparison', +'array_fill' => 'Fill an array with values', +'array_fill_keys' => 'Fill an array with values, specifying keys', +'array_filter' => 'Filters elements of an array using a callback function', +'array_flip' => 'Exchanges all keys with their associated values in an array', +'array_intersect' => 'Computes the intersection of arrays', +'array_intersect_assoc' => 'Computes the intersection of arrays with additional index check', +'array_intersect_key' => 'Computes the intersection of arrays using keys for comparison', +'array_intersect_uassoc' => 'Computes the intersection of arrays with additional index check, compares indexes by a callback function', +'array_intersect_ukey' => 'Computes the intersection of arrays using a callback function on the keys for comparison', +'array_key_exists' => 'Checks if the given key or index exists in the array', +'array_key_first' => 'Gets the first key of an array', +'array_key_last' => 'Gets the last key of an array', +'array_keys' => 'Return all the keys or a subset of the keys of an array', +'array_map' => 'Applies the callback to the elements of the given arrays', +'array_merge' => 'Merge one or more arrays', +'array_merge_recursive' => 'Merge one or more arrays recursively', +'array_multisort' => 'Sort multiple or multi-dimensional arrays', +'array_pad' => 'Pad array to the specified length with a value', +'array_pop' => 'Pop the element off the end of array', +'array_product' => 'Calculate the product of values in an array', +'array_push' => 'Push one or more elements onto the end of array', +'array_rand' => 'Pick one or more random keys out of an array', +'array_reduce' => 'Iteratively reduce the array to a single value using a callback function', +'array_replace' => 'Replaces elements from passed arrays into the first array', +'array_replace_recursive' => 'Replaces elements from passed arrays into the first array recursively', +'array_reverse' => 'Return an array with elements in reverse order', +'array_search' => 'Searches the array for a given value and returns the first corresponding key if successful', +'array_shift' => 'Shift an element off the beginning of array', +'array_slice' => 'Extract a slice of the array', +'array_splice' => 'Remove a portion of the array and replace it with something else', +'array_sum' => 'Calculate the sum of values in an array', +'array_udiff' => 'Computes the difference of arrays by using a callback function for data comparison', +'array_udiff_assoc' => 'Computes the difference of arrays with additional index check, compares data by a callback function', +'array_udiff_uassoc' => 'Computes the difference of arrays with additional index check, compares data and indexes by a callback function', +'array_uintersect' => 'Computes the intersection of arrays, compares data by a callback function', +'array_uintersect_assoc' => 'Computes the intersection of arrays with additional index check, compares data by a callback function', +'array_uintersect_uassoc' => 'Computes the intersection of arrays with additional index check, compares data and indexes by separate callback functions', +'array_unique' => 'Removes duplicate values from an array', +'array_unshift' => 'Prepend one or more elements to the beginning of an array', +'array_values' => 'Return all the values of an array', +'array_walk' => 'Apply a user supplied function to every member of an array', +'array_walk_recursive' => 'Apply a user function recursively to every member of an array', +'ArrayAccess::offsetExists' => 'Whether a offset exists', +'ArrayAccess::offsetGet' => 'Offset to retrieve', +'ArrayAccess::offsetSet' => 'Offset to set', +'ArrayAccess::offsetUnset' => 'Offset to unset', +'arrayiterator::__construct' => 'Construct an ArrayIterator', +'arrayiterator::append' => 'Append an element', +'arrayiterator::asort' => 'Sort array by values', +'arrayiterator::count' => 'Count elements', +'arrayiterator::current' => 'Return current array entry', +'arrayiterator::getArrayCopy' => 'Get array copy', +'arrayiterator::getFlags' => 'Get behavior flags', +'arrayiterator::key' => 'Return current array key', +'arrayiterator::ksort' => 'Sort array by keys', +'arrayiterator::natcasesort' => 'Sort an array naturally, case insensitive', +'arrayiterator::natsort' => 'Sort an array naturally', +'arrayiterator::next' => 'Move to next entry', +'arrayiterator::offsetExists' => 'Check if offset exists', +'arrayiterator::offsetGet' => 'Get value for an offset', +'arrayiterator::offsetSet' => 'Set value for an offset', +'arrayiterator::offsetUnset' => 'Unset value for an offset', +'arrayiterator::rewind' => 'Rewind array back to the start', +'arrayiterator::seek' => 'Seek to position', +'arrayiterator::serialize' => 'Serialize', +'arrayiterator::setFlags' => 'Set behaviour flags', +'arrayiterator::uasort' => 'Sort with a user-defined comparison function and maintain index association', +'arrayiterator::uksort' => 'Sort by keys using a user-defined comparison function', +'arrayiterator::unserialize' => 'Unserialize', +'arrayiterator::valid' => 'Check whether array contains more entries', +'arrayobject::__construct' => 'Construct a new array object', +'arrayobject::append' => 'Appends the value', +'arrayobject::asort' => 'Sort the entries by value', +'arrayobject::count' => 'Get the number of public properties in the ArrayObject', +'arrayobject::exchangeArray' => 'Exchange the array for another one', +'arrayobject::getArrayCopy' => 'Creates a copy of the ArrayObject', +'arrayobject::getFlags' => 'Gets the behavior flags', +'arrayobject::getIterator' => 'Create a new iterator from an ArrayObject instance', +'arrayobject::getIteratorClass' => 'Gets the iterator classname for the ArrayObject', +'arrayobject::ksort' => 'Sort the entries by key', +'arrayobject::natcasesort' => 'Sort an array using a case insensitive "natural order" algorithm', +'arrayobject::natsort' => 'Sort entries using a "natural order" algorithm', +'arrayobject::offsetExists' => 'Returns whether the requested index exists', +'arrayobject::offsetGet' => 'Returns the value at the specified index', +'arrayobject::offsetSet' => 'Sets the value at the specified index to newval', +'arrayobject::offsetUnset' => 'Unsets the value at the specified index', +'arrayobject::serialize' => 'Serialize an ArrayObject', +'arrayobject::setFlags' => 'Sets the behavior flags', +'arrayobject::setIteratorClass' => 'Sets the iterator classname for the ArrayObject', +'arrayobject::uasort' => 'Sort the entries with a user-defined comparison function and maintain key association', +'arrayobject::uksort' => 'Sort the entries by keys using a user-defined comparison function', +'arrayobject::unserialize' => 'Unserialize an ArrayObject', +'arsort' => 'Sort an array in reverse order and maintain index association', +'asin' => 'Arc sine', +'asinh' => 'Inverse hyperbolic sine', +'asort' => 'Sort an array and maintain index association', +'assert' => 'Checks if assertion is `false`', +'assert_options' => 'Set/get the various assert flags', +'ast\get_kind_name' => 'Get string representation of AST kind value', +'ast\get_metadata' => 'Provides metadata for the AST kinds', +'ast\get_supported_versions' => 'Returns currently supported AST versions.', +'ast\kind_uses_flags' => 'Check if AST kind uses flags', +'ast\parse_code' => 'Parses code string and returns AST root node.', +'ast\parse_file' => 'Parses code file and returns AST root node.', +'atan' => 'Arc tangent', +'atan2' => 'Arc tangent of two variables', +'atanh' => 'Inverse hyperbolic tangent', +'base64_decode' => 'Decodes data encoded with MIME base64', +'base64_encode' => 'Encodes data with MIME base64', +'base_convert' => 'Convert a number between arbitrary bases', +'basename' => 'Returns trailing name component of path', +'bbcode_add_element' => 'Adds a bbcode element', +'bbcode_add_smiley' => 'Adds a smiley to the parser', +'bbcode_create' => 'Create a BBCode Resource', +'bbcode_destroy' => 'Close BBCode_container resource', +'bbcode_parse' => 'Parse a string following a given rule set', +'bbcode_set_arg_parser' => 'Attach another parser in order to use another rule set for argument parsing', +'bbcode_set_flags' => 'Set or alter parser options', +'bcadd' => 'Add two arbitrary precision numbers', +'bccomp' => 'Compare two arbitrary precision numbers', +'bcdiv' => 'Divide two arbitrary precision numbers', +'bcmod' => 'Get modulus of an arbitrary precision number', +'bcmul' => 'Multiply two arbitrary precision numbers', +'bcompiler_load' => 'Reads and creates classes from a bz compressed file', +'bcompiler_load_exe' => 'Reads and creates classes from a bcompiler exe file', +'bcompiler_parse_class' => 'Reads the bytecodes of a class and calls back to a user function', +'bcompiler_read' => 'Reads and creates classes from a filehandle', +'bcompiler_write_class' => 'Writes a defined class as bytecodes', +'bcompiler_write_constant' => 'Writes a defined constant as bytecodes', +'bcompiler_write_exe_footer' => 'Writes the start pos, and sig to the end of a exe type file', +'bcompiler_write_file' => 'Writes a php source file as bytecodes', +'bcompiler_write_footer' => 'Writes the single character \x00 to indicate End of compiled data', +'bcompiler_write_function' => 'Writes a defined function as bytecodes', +'bcompiler_write_functions_from_file' => 'Writes all functions defined in a file as bytecodes', +'bcompiler_write_header' => 'Writes the bcompiler header', +'bcompiler_write_included_filename' => 'Writes an included file as bytecodes', +'bcpow' => 'Raise an arbitrary precision number to another', +'bcpowmod' => 'Raise an arbitrary precision number to another, reduced by a specified modulus', +'bcscale' => 'Set or get default scale parameter for all bc math functions', +'bcsqrt' => 'Get the square root of an arbitrary precision number', +'bcsub' => 'Subtract one arbitrary precision number from another', +'bin2hex' => 'Convert binary data into hexadecimal representation', +'bind_textdomain_codeset' => 'Specify the character encoding in which the messages from the DOMAIN message catalog will be returned', +'bindec' => 'Binary to decimal', +'bindtextdomain' => 'Sets the path for a domain', +'blenc_encrypt' => 'Encrypt a PHP script with BLENC', +'boolval' => 'Get the boolean value of a variable', +'bson_decode' => 'Deserializes a BSON object into a PHP array', +'bson_encode' => 'Serializes a PHP variable into a BSON string', +'bzclose' => 'Close a bzip2 file', +'bzcompress' => 'Compress a string into bzip2 encoded data', +'bzdecompress' => 'Decompresses bzip2 encoded data', +'bzerrno' => 'Returns a bzip2 error number', +'bzerror' => 'Returns the bzip2 error number and error string in an array', +'bzerrstr' => 'Returns a bzip2 error string', +'bzflush' => 'Force a write of all buffered data', +'bzopen' => 'Opens a bzip2 compressed file', +'bzread' => 'Binary safe bzip2 file read', +'bzwrite' => 'Binary safe bzip2 file write', +'cachingiterator::__construct' => 'Construct a new CachingIterator object for the iterator', +'cachingiterator::__toString' => 'Return the string representation of the current element', +'cachingiterator::count' => 'The number of elements in the iterator', +'cachingiterator::current' => 'Return the current element', +'cachingiterator::getCache' => 'Retrieve the contents of the cache', +'cachingiterator::getFlags' => 'Get flags used', +'cachingiterator::getInnerIterator' => 'Returns the inner iterator', +'cachingiterator::hasNext' => 'Check whether the inner iterator has a valid next element', +'cachingiterator::key' => 'Return the key for the current element', +'cachingiterator::next' => 'Move the iterator forward', +'cachingiterator::offsetExists' => 'The offsetExists purpose', +'cachingiterator::offsetGet' => 'The offsetGet purpose', +'cachingiterator::offsetSet' => 'The offsetSet purpose', +'cachingiterator::offsetUnset' => 'The offsetUnset purpose', +'cachingiterator::rewind' => 'Rewind the iterator', +'cachingiterator::setFlags' => 'The setFlags purpose', +'cachingiterator::valid' => 'Check whether the current element is valid', +'cairo::availableFonts' => 'Retrieves the availables font types', +'cairo::availableSurfaces' => 'Retrieves all available surfaces', +'cairo::statusToString' => 'Retrieves the current status as string', +'cairo::version' => 'Retrieves cairo\'s library version', +'cairo::versionString' => 'Retrieves cairo version as string', +'cairo_create' => 'Returns a new CairoContext object on the requested surface', +'cairo_matrix_create_scale' => 'Alias of CairoMatrix::initScale', +'cairo_matrix_create_translate' => 'Alias of CairoMatrix::initTranslate', +'cairocontext::__construct' => 'Creates a new CairoContext', +'cairocontext::appendPath' => 'Appends a path to current path', +'cairocontext::arc' => 'Adds a circular arc', +'cairocontext::arcNegative' => 'Adds a negative arc', +'cairocontext::clip' => 'Establishes a new clip region', +'cairocontext::clipExtents' => 'Computes the area inside the current clip', +'cairocontext::clipPreserve' => 'Establishes a new clip region from the current clip', +'cairocontext::clipRectangleList' => 'Retrieves the current clip as a list of rectangles', +'cairocontext::closePath' => 'Closes the current path', +'cairocontext::copyPage' => 'Emits the current page', +'cairocontext::copyPath' => 'Creates a copy of the current path', +'cairocontext::copyPathFlat' => 'Gets a flattened copy of the current path', +'cairocontext::curveTo' => 'Adds a curve', +'cairocontext::deviceToUser' => 'Transform a coordinate', +'cairocontext::deviceToUserDistance' => 'Transform a distance', +'cairocontext::fill' => 'Fills the current path', +'cairocontext::fillExtents' => 'Computes the filled area', +'cairocontext::fillPreserve' => 'Fills and preserve the current path', +'cairocontext::fontExtents' => 'Get the font extents', +'cairocontext::getAntialias' => 'Retrieves the current antialias mode', +'cairocontext::getCurrentPoint' => 'The getCurrentPoint purpose', +'cairocontext::getDash' => 'The getDash purpose', +'cairocontext::getDashCount' => 'The getDashCount purpose', +'cairocontext::getFillRule' => 'The getFillRule purpose', +'cairocontext::getFontFace' => 'The getFontFace purpose', +'cairocontext::getFontMatrix' => 'The getFontMatrix purpose', +'cairocontext::getFontOptions' => 'The getFontOptions purpose', +'cairocontext::getGroupTarget' => 'The getGroupTarget purpose', +'cairocontext::getLineCap' => 'The getLineCap purpose', +'cairocontext::getLineJoin' => 'The getLineJoin purpose', +'cairocontext::getLineWidth' => 'The getLineWidth purpose', +'cairocontext::getMatrix' => 'The getMatrix purpose', +'cairocontext::getMiterLimit' => 'The getMiterLimit purpose', +'cairocontext::getOperator' => 'The getOperator purpose', +'cairocontext::getScaledFont' => 'The getScaledFont purpose', +'cairocontext::getSource' => 'The getSource purpose', +'cairocontext::getTarget' => 'The getTarget purpose', +'cairocontext::getTolerance' => 'The getTolerance purpose', +'cairocontext::glyphPath' => 'The glyphPath purpose', +'cairocontext::hasCurrentPoint' => 'The hasCurrentPoint purpose', +'cairocontext::identityMatrix' => 'The identityMatrix purpose', +'cairocontext::inFill' => 'The inFill purpose', +'cairocontext::inStroke' => 'The inStroke purpose', +'cairocontext::lineTo' => 'The lineTo purpose', +'cairocontext::mask' => 'The mask purpose', +'cairocontext::maskSurface' => 'The maskSurface purpose', +'cairocontext::moveTo' => 'The moveTo purpose', +'cairocontext::newPath' => 'The newPath purpose', +'cairocontext::newSubPath' => 'The newSubPath purpose', +'cairocontext::paint' => 'The paint purpose', +'cairocontext::paintWithAlpha' => 'The paintWithAlpha purpose', +'cairocontext::pathExtents' => 'The pathExtents purpose', +'cairocontext::popGroup' => 'The popGroup purpose', +'cairocontext::popGroupToSource' => 'The popGroupToSource purpose', +'cairocontext::pushGroup' => 'The pushGroup purpose', +'cairocontext::pushGroupWithContent' => 'The pushGroupWithContent purpose', +'cairocontext::rectangle' => 'The rectangle purpose', +'cairocontext::relCurveTo' => 'The relCurveTo purpose', +'cairocontext::relLineTo' => 'The relLineTo purpose', +'cairocontext::relMoveTo' => 'The relMoveTo purpose', +'cairocontext::resetClip' => 'The resetClip purpose', +'cairocontext::restore' => 'The restore purpose', +'cairocontext::rotate' => 'The rotate purpose', +'cairocontext::save' => 'The save purpose', +'cairocontext::scale' => 'The scale purpose', +'cairocontext::selectFontFace' => 'The selectFontFace purpose', +'cairocontext::setAntialias' => 'The setAntialias purpose', +'cairocontext::setDash' => 'The setDash purpose', +'cairocontext::setFillRule' => 'The setFillRule purpose', +'cairocontext::setFontFace' => 'The setFontFace purpose', +'cairocontext::setFontMatrix' => 'The setFontMatrix purpose', +'cairocontext::setFontOptions' => 'The setFontOptions purpose', +'cairocontext::setFontSize' => 'The setFontSize purpose', +'cairocontext::setLineCap' => 'The setLineCap purpose', +'cairocontext::setLineJoin' => 'The setLineJoin purpose', +'cairocontext::setLineWidth' => 'The setLineWidth purpose', +'cairocontext::setMatrix' => 'The setMatrix purpose', +'cairocontext::setMiterLimit' => 'The setMiterLimit purpose', +'cairocontext::setOperator' => 'The setOperator purpose', +'cairocontext::setScaledFont' => 'The setScaledFont purpose', +'cairocontext::setSource' => 'The setSource purpose', +'cairocontext::setSourceRGB' => 'The setSourceRGB purpose', +'cairocontext::setSourceRGBA' => 'The setSourceRGBA purpose', +'cairocontext::setSourceSurface' => 'The setSourceSurface purpose', +'cairocontext::setTolerance' => 'The setTolerance purpose', +'cairocontext::showPage' => 'The showPage purpose', +'cairocontext::showText' => 'The showText purpose', +'cairocontext::status' => 'The status purpose', +'cairocontext::stroke' => 'The stroke purpose', +'cairocontext::strokeExtents' => 'The strokeExtents purpose', +'cairocontext::strokePreserve' => 'The strokePreserve purpose', +'cairocontext::textExtents' => 'The textExtents purpose', +'cairocontext::textPath' => 'The textPath purpose', +'cairocontext::transform' => 'The transform purpose', +'cairocontext::translate' => 'The translate purpose', +'cairocontext::userToDevice' => 'The userToDevice purpose', +'cairocontext::userToDeviceDistance' => 'The userToDeviceDistance purpose', +'cairofontface::__construct' => 'Creates a new CairoFontFace object', +'cairofontface::getType' => 'Retrieves the font face type', +'cairofontface::status' => 'Check for CairoFontFace errors', +'cairofontoptions::__construct' => 'The __construct purpose', +'cairofontoptions::equal' => 'The equal purpose', +'cairofontoptions::getAntialias' => 'The getAntialias purpose', +'cairofontoptions::getHintMetrics' => 'The getHintMetrics purpose', +'cairofontoptions::getHintStyle' => 'The getHintStyle purpose', +'cairofontoptions::getSubpixelOrder' => 'The getSubpixelOrder purpose', +'cairofontoptions::hash' => 'The hash purpose', +'cairofontoptions::merge' => 'The merge purpose', +'cairofontoptions::setAntialias' => 'The setAntialias purpose', +'cairofontoptions::setHintMetrics' => 'The setHintMetrics purpose', +'cairofontoptions::setHintStyle' => 'The setHintStyle purpose', +'cairofontoptions::setSubpixelOrder' => 'The setSubpixelOrder purpose', +'cairofontoptions::status' => 'The status purpose', +'cairoformat::strideForWidth' => 'Provides an appropriate stride to use', +'cairogradientpattern::addColorStopRgb' => 'The addColorStopRgb purpose', +'cairogradientpattern::addColorStopRgba' => 'The addColorStopRgba purpose', +'cairogradientpattern::getColorStopCount' => 'The getColorStopCount purpose', +'cairogradientpattern::getColorStopRgba' => 'The getColorStopRgba purpose', +'cairogradientpattern::getExtend' => 'The getExtend purpose', +'cairogradientpattern::setExtend' => 'The setExtend purpose', +'cairoimagesurface::__construct' => 'Creates a new CairoImageSurface', +'cairoimagesurface::createForData' => 'The createForData purpose', +'cairoimagesurface::createFromPng' => 'Creates a new CairoImageSurface form a png image file', +'cairoimagesurface::getData' => 'Gets the image data as string', +'cairoimagesurface::getFormat' => 'Get the image format', +'cairoimagesurface::getHeight' => 'Retrieves the height of the CairoImageSurface', +'cairoimagesurface::getStride' => 'The getStride purpose', +'cairoimagesurface::getWidth' => 'Retrieves the width of the CairoImageSurface', +'cairolineargradient::__construct' => 'The __construct purpose', +'cairolineargradient::getPoints' => 'The getPoints purpose', +'cairomatrix::__construct' => 'Creates a new CairoMatrix object', +'cairomatrix::initIdentity' => 'Creates a new identity matrix', +'cairomatrix::initRotate' => 'Creates a new rotated matrix', +'cairomatrix::initScale' => 'Creates a new scaling matrix', +'cairomatrix::initTranslate' => 'Creates a new translation matrix', +'cairomatrix::invert' => 'The invert purpose', +'cairomatrix::multiply' => 'The multiply purpose', +'cairomatrix::rotate' => 'The rotate purpose', +'cairomatrix::scale' => 'Applies scaling to a matrix', +'cairomatrix::transformDistance' => 'The transformDistance purpose', +'cairomatrix::transformPoint' => 'The transformPoint purpose', +'cairomatrix::translate' => 'The translate purpose', +'cairopattern::__construct' => 'The __construct purpose', +'cairopattern::getMatrix' => 'The getMatrix purpose', +'cairopattern::getType' => 'The getType purpose', +'cairopattern::setMatrix' => 'The setMatrix purpose', +'cairopattern::status' => 'The status purpose', +'cairopdfsurface::__construct' => 'The __construct purpose', +'cairopdfsurface::setSize' => 'The setSize purpose', +'cairopssurface::__construct' => 'The __construct purpose', +'cairopssurface::dscBeginPageSetup' => 'The dscBeginPageSetup purpose', +'cairopssurface::dscBeginSetup' => 'The dscBeginSetup purpose', +'cairopssurface::dscComment' => 'The dscComment purpose', +'cairopssurface::getEps' => 'The getEps purpose', +'cairopssurface::getLevels' => 'The getLevels purpose', +'cairopssurface::levelToString' => 'The levelToString purpose', +'cairopssurface::restrictToLevel' => 'The restrictToLevel purpose', +'cairopssurface::setEps' => 'The setEps purpose', +'cairopssurface::setSize' => 'The setSize purpose', +'cairoradialgradient::__construct' => 'The __construct purpose', +'cairoradialgradient::getCircles' => 'The getCircles purpose', +'cairoscaledfont::__construct' => 'The __construct purpose', +'cairoscaledfont::extents' => 'The extents purpose', +'cairoscaledfont::getCtm' => 'The getCtm purpose', +'cairoscaledfont::getFontFace' => 'The getFontFace purpose', +'cairoscaledfont::getFontMatrix' => 'The getFontMatrix purpose', +'cairoscaledfont::getFontOptions' => 'The getFontOptions purpose', +'cairoscaledfont::getScaleMatrix' => 'The getScaleMatrix purpose', +'cairoscaledfont::getType' => 'The getType purpose', +'cairoscaledfont::glyphExtents' => 'The glyphExtents purpose', +'cairoscaledfont::status' => 'The status purpose', +'cairoscaledfont::textExtents' => 'The textExtents purpose', +'cairosolidpattern::__construct' => 'The __construct purpose', +'cairosolidpattern::getRgba' => 'The getRgba purpose', +'cairosurface::__construct' => 'The __construct purpose', +'cairosurface::copyPage' => 'The copyPage purpose', +'cairosurface::createSimilar' => 'The createSimilar purpose', +'cairosurface::finish' => 'The finish purpose', +'cairosurface::flush' => 'The flush purpose', +'cairosurface::getContent' => 'The getContent purpose', +'cairosurface::getDeviceOffset' => 'The getDeviceOffset purpose', +'cairosurface::getFontOptions' => 'The getFontOptions purpose', +'cairosurface::getType' => 'The getType purpose', +'cairosurface::markDirty' => 'The markDirty purpose', +'cairosurface::markDirtyRectangle' => 'The markDirtyRectangle purpose', +'cairosurface::setDeviceOffset' => 'The setDeviceOffset purpose', +'cairosurface::setFallbackResolution' => 'The setFallbackResolution purpose', +'cairosurface::showPage' => 'The showPage purpose', +'cairosurface::status' => 'The status purpose', +'cairosurface::writeToPng' => 'The writeToPng purpose', +'cairosurfacepattern::__construct' => 'The __construct purpose', +'cairosurfacepattern::getExtend' => 'The getExtend purpose', +'cairosurfacepattern::getFilter' => 'The getFilter purpose', +'cairosurfacepattern::getSurface' => 'The getSurface purpose', +'cairosurfacepattern::setExtend' => 'The setExtend purpose', +'cairosurfacepattern::setFilter' => 'The setFilter purpose', +'cairosvgsurface::__construct' => 'The __construct purpose', +'cairosvgsurface::getVersions' => 'Used to retrieve a list of supported SVG versions', +'cairosvgsurface::restrictToVersion' => 'The restrictToVersion purpose', +'cairosvgsurface::versionToString' => 'The versionToString purpose', +'cal_days_in_month' => 'Return the number of days in a month for a given year and calendar', +'cal_from_jd' => 'Converts from Julian Day Count to a supported calendar', +'cal_info' => 'Returns information about a particular calendar', +'cal_to_jd' => 'Converts from a supported calendar to Julian Day Count', +'call_user_func' => 'Call the callback given by the first parameter', +'call_user_func_array' => 'Call a callback with an array of parameters', +'call_user_method' => 'Call a user method on an specific object', +'call_user_method_array' => 'Call a user method given with an array of parameters', +'callbackfilteriterator::__construct' => 'Create a filtered iterator from another iterator', +'callbackfilteriterator::accept' => 'Calls the callback with the current value, the current key and the inner iterator as arguments', +'CallbackFilterIterator::current' => 'Get the current element value', +'CallbackFilterIterator::getInnerIterator' => 'Get the inner iterator', +'CallbackFilterIterator::key' => 'Get the current key', +'CallbackFilterIterator::next' => 'Move the iterator forward', +'CallbackFilterIterator::rewind' => 'Rewind the iterator', +'CallbackFilterIterator::valid' => 'Check whether the current element is valid', +'Cassandra::cluster' => 'Creates a new cluster builder for constructing a Cluster object.', +'Cassandra::ssl' => 'Creates a new ssl builder for constructing a SSLOptions object.', +'Cassandra\Aggregate::argumentTypes' => 'Returns the argument types of the aggregate', +'Cassandra\Aggregate::finalFunction' => 'Returns the final function of the aggregate', +'Cassandra\Aggregate::initialCondition' => 'Returns the initial condition of the aggregate', +'Cassandra\Aggregate::name' => 'Returns the full name of the aggregate', +'Cassandra\Aggregate::returnType' => 'Returns the return type of the aggregate', +'Cassandra\Aggregate::signature' => 'Returns the signature of the aggregate', +'Cassandra\Aggregate::simpleName' => 'Returns the simple name of the aggregate', +'Cassandra\Aggregate::stateFunction' => 'Returns the state function of the aggregate', +'Cassandra\Aggregate::stateType' => 'Returns the state type of the aggregate', +'Cassandra\BatchStatement::__construct' => 'Creates a new batch statement.', +'Cassandra\BatchStatement::add' => 'Adds a statement to this batch.', +'Cassandra\Bigint::__construct' => 'Creates a new 64bit integer.', +'Cassandra\Bigint::__toString' => 'Returns string representation of the integer value.', +'Cassandra\Bigint::abs' => '`@return \Cassandra\Numeric` absolute value', +'Cassandra\Bigint::add' => '`@return \Cassandra\Numeric` sum', +'Cassandra\Bigint::div' => '`@return \Cassandra\Numeric` quotient', +'Cassandra\Bigint::max' => 'Maximum possible Bigint value', +'Cassandra\Bigint::min' => 'Minimum possible Bigint value', +'Cassandra\Bigint::mod' => '`@return \Cassandra\Numeric` remainder', +'Cassandra\Bigint::mul' => '`@return \Cassandra\Numeric` product', +'Cassandra\Bigint::neg' => '`@return \Cassandra\Numeric` negative value', +'Cassandra\Bigint::sqrt' => '`@return \Cassandra\Numeric` square root', +'Cassandra\Bigint::sub' => '`@return \Cassandra\Numeric` difference', +'Cassandra\Bigint::toDouble' => '`@return float` this number as float', +'Cassandra\Bigint::toInt' => '`@return int` this number as int', +'Cassandra\Bigint::type' => 'The type of this bigint.', +'Cassandra\Bigint::value' => 'Returns the integer value.', +'Cassandra\Blob::__construct' => 'Creates a new bytes array.', +'Cassandra\Blob::__toString' => 'Returns bytes as a hex string.', +'Cassandra\Blob::bytes' => 'Returns bytes as a hex string.', +'Cassandra\Blob::toBinaryString' => 'Returns bytes as a binary string.', +'Cassandra\Blob::type' => 'The type of this blob.', +'Cassandra\Cluster::connect' => 'Creates a new Session instance.', +'Cassandra\Cluster::connectAsync' => 'Creates a new Session instance.', +'Cassandra\Cluster\Builder::build' => 'Returns a Cluster Instance.', +'Cassandra\Cluster\Builder::withBlackListDCs' => 'Sets the blacklist datacenters. Any datacenter in the blacklist will be +ignored and a connection will not be established to any host in those +datacenters. This policy is useful for ensuring the driver will not +connect to any host in a specific datacenter.', +'Cassandra\Cluster\Builder::withBlackListHosts' => 'Sets the blacklist hosts. Any host in the blacklist will be ignored and +a conneciton will not be established. This is useful for ensuring that +the driver will not connection to a predefied set of hosts.', +'Cassandra\Cluster\Builder::withConnectionHeartbeatInterval' => 'Specify interval in seconds that the driver should wait before attempting +to send heartbeat messages and control the amount of time the connection +must be idle before sending heartbeat messages. This is useful for +preventing intermediate network devices from dropping connections.', +'Cassandra\Cluster\Builder::withConnectionsPerHost' => 'Set the size of connection pools used by the driver. Pools are fixed +when only `$core` is given, when a `$max` is specified as well, +additional connections will be created automatically based on current +load until the maximum number of connection has been reached. When +request load goes down, extra connections are automatically cleaned up +until only the core number of connections is left.', +'Cassandra\Cluster\Builder::withConnectTimeout' => 'Timeout used for establishing TCP connections.', +'Cassandra\Cluster\Builder::withContactPoints' => 'Configures the initial endpoints. Note that the driver will +automatically discover and connect to the rest of the cluster.', +'Cassandra\Cluster\Builder::withCredentials' => 'Configures plain-text authentication.', +'Cassandra\Cluster\Builder::withDatacenterAwareRoundRobinLoadBalancingPolicy' => 'Configures this cluster to use a datacenter aware round robin load balancing policy.', +'Cassandra\Cluster\Builder::withDefaultConsistency' => 'Configures default consistency for all requests.', +'Cassandra\Cluster\Builder::withDefaultPageSize' => 'Configures default page size for all results. +Set to `null` to disable paging altogether.', +'Cassandra\Cluster\Builder::withDefaultTimeout' => 'Configures default timeout for future resolution in blocking operations +Set to null to disable (default).', +'Cassandra\Cluster\Builder::withHostnameResolution' => 'Enables/disables Hostname Resolution. + +If enabled the driver will resolve hostnames for IP addresses using +reverse IP lookup. This is useful for authentication (Kerberos) or +encryption SSL services that require a valid hostname for verification. + +Important: It\'s possible that the underlying C/C++ driver does not +support hostname resolution. A PHP warning will be emitted if the driver +does not support hostname resolution.', +'Cassandra\Cluster\Builder::withIOThreads' => 'Total number of IO threads to use for handling the requests. + +Note: number of io threads * core connections per host <= total number + of connections <= number of io threads * max connections per host', +'Cassandra\Cluster\Builder::withLatencyAwareRouting' => 'Enables/disables latency-aware routing.', +'Cassandra\Cluster\Builder::withPersistentSessions' => 'Enable persistent sessions and clusters.', +'Cassandra\Cluster\Builder::withPort' => 'Specify a different port to be used when connecting to the cluster.', +'Cassandra\Cluster\Builder::withProtocolVersion' => 'Force the driver to use a specific binary protocol version. + +Apache Cassandra 1.2+ supports protocol version 1 +Apache Cassandra 2.0+ supports protocol version 2 +Apache Cassandra 2.1+ supports protocol version 3 +Apache Cassandra 2.2+ supports protocol version 4 + +NOTE: Apache Cassandra 3.x supports protocol version 3 and 4 only', +'Cassandra\Cluster\Builder::withRandomizedContactPoints' => 'Enables/disables Randomized Contact Points. + +If enabled this allows the driver randomly use contact points in order +to evenly spread the load across the cluster and prevent +hotspots/load spikes during notifications (e.g. massive schema change). + +Note: This setting should only be disabled for debugging and testing.', +'Cassandra\Cluster\Builder::withReconnectInterval' => 'Specify interval in seconds that the driver should wait before attempting +to re-establish a closed connection.', +'Cassandra\Cluster\Builder::withRequestTimeout' => 'Timeout used for waiting for a response from a node.', +'Cassandra\Cluster\Builder::withRetryPolicy' => 'Configures the retry policy.', +'Cassandra\Cluster\Builder::withRoundRobinLoadBalancingPolicy' => 'Configures this cluster to use a round robin load balancing policy.', +'Cassandra\Cluster\Builder::withSchemaMetadata' => 'Enables/disables Schema Metadata. + +If disabled this allows the driver to skip over retrieving and +updating schema metadata, but it also disables the usage of token-aware +routing and $session->schema() will always return an empty object. This +can be useful for reducing the startup overhead of short-lived sessions.', +'Cassandra\Cluster\Builder::withSSL' => 'Set up ssl context.', +'Cassandra\Cluster\Builder::withTCPKeepalive' => 'Enables/disables TCP keepalive.', +'Cassandra\Cluster\Builder::withTCPNodelay' => 'Disables nagle algorithm for lower latency.', +'Cassandra\Cluster\Builder::withTimestampGenerator' => 'Sets the timestamp generator.', +'Cassandra\Cluster\Builder::withTokenAwareRouting' => 'Enable token aware routing.', +'Cassandra\Cluster\Builder::withWhiteListDCs' => 'Sets the whitelist datacenters. Any host not in a whitelisted datacenter +will be ignored. This policy is useful for ensuring the driver will only +connect to hosts in specific datacenters.', +'Cassandra\Cluster\Builder::withWhiteListHosts' => 'Sets the whitelist hosts. Any host not in the whitelist will be ignored +and a connection will not be established. This policy is useful for +ensuring that the driver will only connect to a predefined set of hosts.', +'Cassandra\Collection::__construct' => 'Creates a new collection of a given type.', +'Cassandra\Collection::add' => 'Adds one or more values to this collection.', +'Cassandra\Collection::count' => 'Total number of elements in this collection', +'Cassandra\Collection::current' => 'Current element for iteration', +'Cassandra\Collection::find' => 'Finds index of a value in this collection.', +'Cassandra\Collection::get' => 'Retrieves the value at a given index.', +'Cassandra\Collection::key' => 'Current key for iteration', +'Cassandra\Collection::next' => 'Move internal iterator forward', +'Cassandra\Collection::remove' => 'Deletes the value at a given index', +'Cassandra\Collection::rewind' => 'Rewind internal iterator', +'Cassandra\Collection::type' => 'The type of this collection.', +'Cassandra\Collection::valid' => 'Check whether a current value exists', +'Cassandra\Collection::values' => 'Array of values in this collection.', +'Cassandra\Column::indexName' => 'Returns name of the index if defined.', +'Cassandra\Column::indexOptions' => 'Returns index options if present.', +'Cassandra\Column::isFrozen' => 'Returns true for frozen columns.', +'Cassandra\Column::isReversed' => 'Returns whether the column is in descending or ascending order.', +'Cassandra\Column::isStatic' => 'Returns true for static columns.', +'Cassandra\Column::name' => 'Returns the name of the column.', +'Cassandra\Column::type' => 'Returns the type of the column.', +'Cassandra\Custom::type' => 'The type of this value.', +'Cassandra\Date::__construct' => 'Creates a new Date object', +'Cassandra\Date::__toString' => '`@return string` this date in string format: Date(seconds=$seconds)', +'Cassandra\Date::fromDateTime' => 'Creates a new Date object from a \DateTime object.', +'Cassandra\Date::seconds' => '`@return int` Absolute seconds from epoch (1970, 1, 1), can be negative', +'Cassandra\Date::toDateTime' => 'Converts current date to PHP DateTime.', +'Cassandra\Date::type' => 'The type of this date.', +'Cassandra\Decimal::__construct' => 'Creates a decimal from a given decimal string: + +~~~{.php} + 'String representation of this decimal.', +'Cassandra\Decimal::abs' => '`@return \Cassandra\Numeric` absolute value', +'Cassandra\Decimal::add' => '`@return \Cassandra\Numeric` sum', +'Cassandra\Decimal::div' => '`@return \Cassandra\Numeric` quotient', +'Cassandra\Decimal::mod' => '`@return \Cassandra\Numeric` remainder', +'Cassandra\Decimal::mul' => '`@return \Cassandra\Numeric` product', +'Cassandra\Decimal::neg' => '`@return \Cassandra\Numeric` negative value', +'Cassandra\Decimal::scale' => 'Scale of this decimal as int.', +'Cassandra\Decimal::sqrt' => '`@return \Cassandra\Numeric` square root', +'Cassandra\Decimal::sub' => '`@return \Cassandra\Numeric` difference', +'Cassandra\Decimal::toDouble' => '`@return float` this number as float', +'Cassandra\Decimal::toInt' => '`@return int` this number as int', +'Cassandra\Decimal::type' => 'The type of this decimal.', +'Cassandra\Decimal::value' => 'Numeric value of this decimal as string.', +'Cassandra\DefaultAggregate::argumentTypes' => 'Returns the argument types of the aggregate', +'Cassandra\DefaultAggregate::finalFunction' => 'Returns the final function of the aggregate', +'Cassandra\DefaultAggregate::initialCondition' => 'Returns the initial condition of the aggregate', +'Cassandra\DefaultAggregate::name' => 'Returns the full name of the aggregate', +'Cassandra\DefaultAggregate::returnType' => 'Returns the return type of the aggregate', +'Cassandra\DefaultAggregate::signature' => 'Returns the signature of the aggregate', +'Cassandra\DefaultAggregate::simpleName' => 'Returns the simple name of the aggregate', +'Cassandra\DefaultAggregate::stateFunction' => 'Returns the state function of the aggregate', +'Cassandra\DefaultAggregate::stateType' => 'Returns the state type of the aggregate', +'Cassandra\DefaultCluster::connect' => 'Creates a new Session instance.', +'Cassandra\DefaultCluster::connectAsync' => 'Creates a new Session instance.', +'Cassandra\DefaultColumn::indexName' => 'Returns name of the index if defined.', +'Cassandra\DefaultColumn::indexOptions' => 'Returns index options if present.', +'Cassandra\DefaultColumn::isFrozen' => 'Returns true for frozen columns.', +'Cassandra\DefaultColumn::isReversed' => 'Returns whether the column is in descending or ascending order.', +'Cassandra\DefaultColumn::isStatic' => 'Returns true for static columns.', +'Cassandra\DefaultColumn::name' => 'Returns the name of the column.', +'Cassandra\DefaultColumn::type' => 'Returns the type of the column.', +'Cassandra\DefaultFunction::arguments' => 'Returns the arguments of the function', +'Cassandra\DefaultFunction::body' => 'Returns the body of the function', +'Cassandra\DefaultFunction::isCalledOnNullInput' => 'Determines if a function is called when the value is null.', +'Cassandra\DefaultFunction::language' => 'Returns the lanuage of the function', +'Cassandra\DefaultFunction::name' => 'Returns the full name of the function', +'Cassandra\DefaultFunction::returnType' => 'Returns the return type of the function', +'Cassandra\DefaultFunction::signature' => 'Returns the signature of the function', +'Cassandra\DefaultFunction::simpleName' => 'Returns the simple name of the function', +'Cassandra\DefaultIndex::className' => 'Returns the class name of the index', +'Cassandra\DefaultIndex::isCustom' => 'Determines if the index is a custom index.', +'Cassandra\DefaultIndex::kind' => 'Returns the kind of index', +'Cassandra\DefaultIndex::name' => 'Returns the name of the index', +'Cassandra\DefaultIndex::option' => 'Return a column\'s option by name', +'Cassandra\DefaultIndex::options' => 'Returns all the index\'s options', +'Cassandra\DefaultIndex::target' => 'Returns the target column of the index', +'Cassandra\DefaultKeyspace::aggregate' => 'Get an aggregate by name and signature', +'Cassandra\DefaultKeyspace::aggregates' => 'Get all aggregates', +'Cassandra\DefaultKeyspace::function_' => 'Get a function by name and signature', +'Cassandra\DefaultKeyspace::functions' => 'Get all functions', +'Cassandra\DefaultKeyspace::hasDurableWrites' => 'Returns whether the keyspace has durable writes enabled', +'Cassandra\DefaultKeyspace::materializedView' => 'Get materialized view by name', +'Cassandra\DefaultKeyspace::materializedViews' => 'Gets all materialized views', +'Cassandra\DefaultKeyspace::name' => 'Returns keyspace name', +'Cassandra\DefaultKeyspace::replicationClassName' => 'Returns replication class name', +'Cassandra\DefaultKeyspace::replicationOptions' => 'Returns replication options', +'Cassandra\DefaultKeyspace::table' => 'Returns a table by name', +'Cassandra\DefaultKeyspace::tables' => 'Returns all tables defined in this keyspace', +'Cassandra\DefaultKeyspace::userType' => 'Get user type by name', +'Cassandra\DefaultKeyspace::userTypes' => 'Get all user types', +'Cassandra\DefaultMaterializedView::baseTable' => 'Returns the base table of the view', +'Cassandra\DefaultMaterializedView::bloomFilterFPChance' => 'Returns bloom filter FP chance', +'Cassandra\DefaultMaterializedView::caching' => 'Returns caching options', +'Cassandra\DefaultMaterializedView::clusteringKey' => 'Returns the clustering key columns of the view', +'Cassandra\DefaultMaterializedView::clusteringOrder' => '`@return array` A list of cluster column orders (\'asc\' and \'desc\')', +'Cassandra\DefaultMaterializedView::column' => 'Returns column by name', +'Cassandra\DefaultMaterializedView::columns' => 'Returns all columns in this view', +'Cassandra\DefaultMaterializedView::comment' => 'Description of the view, if any', +'Cassandra\DefaultMaterializedView::compactionStrategyClassName' => 'Returns compaction strategy class name', +'Cassandra\DefaultMaterializedView::compactionStrategyOptions' => 'Returns compaction strategy options', +'Cassandra\DefaultMaterializedView::compressionParameters' => 'Returns compression parameters', +'Cassandra\DefaultMaterializedView::defaultTTL' => 'Returns default TTL.', +'Cassandra\DefaultMaterializedView::gcGraceSeconds' => 'Returns GC grace seconds', +'Cassandra\DefaultMaterializedView::indexInterval' => 'Returns index interval', +'Cassandra\DefaultMaterializedView::localReadRepairChance' => 'Returns local read repair chance', +'Cassandra\DefaultMaterializedView::maxIndexInterval' => 'Returns the value of `max_index_interval`', +'Cassandra\DefaultMaterializedView::memtableFlushPeriodMs' => 'Returns memtable flush period in milliseconds', +'Cassandra\DefaultMaterializedView::minIndexInterval' => 'Returns the value of `min_index_interval`', +'Cassandra\DefaultMaterializedView::name' => 'Returns the name of this view', +'Cassandra\DefaultMaterializedView::option' => 'Return a view\'s option by name', +'Cassandra\DefaultMaterializedView::options' => 'Returns all the view\'s options', +'Cassandra\DefaultMaterializedView::partitionKey' => 'Returns the partition key columns of the view', +'Cassandra\DefaultMaterializedView::populateIOCacheOnFlush' => 'Returns whether or not the `populate_io_cache_on_flush` is true', +'Cassandra\DefaultMaterializedView::primaryKey' => 'Returns both the partition and clustering key columns of the view', +'Cassandra\DefaultMaterializedView::readRepairChance' => 'Returns read repair chance', +'Cassandra\DefaultMaterializedView::replicateOnWrite' => 'Returns whether or not the `replicate_on_write` is true', +'Cassandra\DefaultMaterializedView::speculativeRetry' => 'Returns speculative retry.', +'Cassandra\DefaultSchema::keyspace' => 'Returns a Keyspace instance by name.', +'Cassandra\DefaultSchema::keyspaces' => 'Returns all keyspaces defined in the schema.', +'Cassandra\DefaultSchema::version' => 'Get the version of the schema snapshot', +'Cassandra\DefaultSession::close' => 'Close the session and all its connections.', +'Cassandra\DefaultSession::closeAsync' => 'Asynchronously close the session and all its connections.', +'Cassandra\DefaultSession::execute' => 'Execute a query. + +Available execution options: +| Option Name | Option **Type** | Option Details | +|--------------------|-----------------|----------------------------------------------------------------------------------------------------------| +| arguments | array | An array or positional or named arguments | +| consistency | int | A consistency constant e.g Dse::CONSISTENCY_ONE, Dse::CONSISTENCY_QUORUM, etc. | +| timeout | int | A number of rows to include in result for paging | +| paging_state_token | string | A string token use to resume from the state of a previous result set | +| retry_policy | RetryPolicy | A retry policy that is used to handle server-side failures for this request | +| serial_consistency | int | Either Dse::CONSISTENCY_SERIAL or Dse::CONSISTENCY_LOCAL_SERIAL | +| timestamp | int\|string | Either an integer or integer string timestamp that represents the number of microseconds since the epoch | +| execute_as | string | User to execute statement as |', +'Cassandra\DefaultSession::executeAsync' => 'Execute a query asynchronously. This method returns immediately, but +the query continues execution in the background.', +'Cassandra\DefaultSession::metrics' => 'Get performance and diagnostic metrics.', +'Cassandra\DefaultSession::prepare' => 'Prepare a query for execution.', +'Cassandra\DefaultSession::prepareAsync' => 'Asynchronously prepare a query for execution.', +'Cassandra\DefaultSession::schema' => 'Get a snapshot of the cluster\'s current schema.', +'Cassandra\DefaultTable::bloomFilterFPChance' => 'Returns bloom filter FP chance', +'Cassandra\DefaultTable::caching' => 'Returns caching options', +'Cassandra\DefaultTable::clusteringKey' => 'Returns the clustering key columns of the table', +'Cassandra\DefaultTable::clusteringOrder' => '`@return array` A list of cluster column orders (\'asc\' and \'desc\')', +'Cassandra\DefaultTable::column' => 'Returns column by name', +'Cassandra\DefaultTable::columns' => 'Returns all columns in this table', +'Cassandra\DefaultTable::comment' => 'Description of the table, if any', +'Cassandra\DefaultTable::compactionStrategyClassName' => 'Returns compaction strategy class name', +'Cassandra\DefaultTable::compactionStrategyOptions' => 'Returns compaction strategy options', +'Cassandra\DefaultTable::compressionParameters' => 'Returns compression parameters', +'Cassandra\DefaultTable::defaultTTL' => 'Returns default TTL.', +'Cassandra\DefaultTable::gcGraceSeconds' => 'Returns GC grace seconds', +'Cassandra\DefaultTable::index' => 'Get an index by name', +'Cassandra\DefaultTable::indexes' => 'Gets all indexes', +'Cassandra\DefaultTable::indexInterval' => 'Returns index interval', +'Cassandra\DefaultTable::localReadRepairChance' => 'Returns local read repair chance', +'Cassandra\DefaultTable::materializedView' => 'Get materialized view by name', +'Cassandra\DefaultTable::materializedViews' => 'Gets all materialized views', +'Cassandra\DefaultTable::maxIndexInterval' => 'Returns the value of `max_index_interval`', +'Cassandra\DefaultTable::memtableFlushPeriodMs' => 'Returns memtable flush period in milliseconds', +'Cassandra\DefaultTable::minIndexInterval' => 'Returns the value of `min_index_interval`', +'Cassandra\DefaultTable::name' => 'Returns the name of this table', +'Cassandra\DefaultTable::option' => 'Return a table\'s option by name', +'Cassandra\DefaultTable::options' => 'Returns all the table\'s options', +'Cassandra\DefaultTable::partitionKey' => 'Returns the partition key columns of the table', +'Cassandra\DefaultTable::populateIOCacheOnFlush' => 'Returns whether or not the `populate_io_cache_on_flush` is true', +'Cassandra\DefaultTable::primaryKey' => 'Returns both the partition and clustering key columns of the table', +'Cassandra\DefaultTable::readRepairChance' => 'Returns read repair chance', +'Cassandra\DefaultTable::replicateOnWrite' => 'Returns whether or not the `replicate_on_write` is true', +'Cassandra\DefaultTable::speculativeRetry' => 'Returns speculative retry.', +'Cassandra\Duration::__toString' => '`@return string` string representation of this Duration; may be used as a literal parameter in CQL queries.', +'Cassandra\Duration::days' => '`@return string` the days attribute of this Duration', +'Cassandra\Duration::months' => '`@return string` the months attribute of this Duration', +'Cassandra\Duration::nanos' => '`@return string` the nanoseconds attribute of this Duration', +'Cassandra\Duration::type' => 'The type of represented by the value.', +'Cassandra\Exception\AlreadyExistsException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\AlreadyExistsException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\AlreadyExistsException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\AlreadyExistsException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\AlreadyExistsException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\AlreadyExistsException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\AlreadyExistsException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\AlreadyExistsException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\AuthenticationException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\AuthenticationException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\AuthenticationException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\AuthenticationException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\AuthenticationException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\AuthenticationException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\AuthenticationException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\AuthenticationException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\ConfigurationException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\ConfigurationException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\ConfigurationException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\ConfigurationException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\ConfigurationException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\ConfigurationException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\ConfigurationException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\ConfigurationException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\DivideByZeroException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\DivideByZeroException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\DivideByZeroException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\DivideByZeroException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\DivideByZeroException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\DivideByZeroException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\DivideByZeroException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\DivideByZeroException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\DomainException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\DomainException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\DomainException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\DomainException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\DomainException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\DomainException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\DomainException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\DomainException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\ExecutionException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\ExecutionException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\ExecutionException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\ExecutionException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\ExecutionException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\ExecutionException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\ExecutionException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\ExecutionException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\InvalidArgumentException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\InvalidArgumentException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\InvalidArgumentException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\InvalidArgumentException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\InvalidArgumentException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\InvalidArgumentException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\InvalidArgumentException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\InvalidArgumentException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\InvalidQueryException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\InvalidQueryException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\InvalidQueryException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\InvalidQueryException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\InvalidQueryException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\InvalidQueryException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\InvalidQueryException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\InvalidQueryException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\InvalidSyntaxException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\InvalidSyntaxException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\InvalidSyntaxException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\InvalidSyntaxException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\InvalidSyntaxException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\InvalidSyntaxException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\InvalidSyntaxException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\InvalidSyntaxException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\IsBootstrappingException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\IsBootstrappingException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\IsBootstrappingException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\IsBootstrappingException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\IsBootstrappingException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\IsBootstrappingException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\IsBootstrappingException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\IsBootstrappingException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\LogicException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\LogicException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\LogicException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\LogicException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\LogicException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\LogicException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\LogicException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\LogicException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\OverloadedException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\OverloadedException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\OverloadedException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\OverloadedException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\OverloadedException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\OverloadedException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\OverloadedException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\OverloadedException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\ProtocolException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\ProtocolException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\ProtocolException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\ProtocolException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\ProtocolException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\ProtocolException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\ProtocolException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\ProtocolException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\RangeException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\RangeException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\RangeException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\RangeException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\RangeException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\RangeException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\RangeException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\RangeException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\ReadTimeoutException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\ReadTimeoutException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\ReadTimeoutException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\ReadTimeoutException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\ReadTimeoutException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\ReadTimeoutException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\ReadTimeoutException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\ReadTimeoutException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\RuntimeException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\RuntimeException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\RuntimeException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\RuntimeException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\RuntimeException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\RuntimeException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\RuntimeException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\RuntimeException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\ServerException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\ServerException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\ServerException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\ServerException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\ServerException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\ServerException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\ServerException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\ServerException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\TimeoutException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\TimeoutException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\TimeoutException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\TimeoutException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\TimeoutException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\TimeoutException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\TimeoutException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\TimeoutException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\TruncateException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\TruncateException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\TruncateException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\TruncateException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\TruncateException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\TruncateException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\TruncateException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\TruncateException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\UnauthorizedException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\UnauthorizedException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\UnauthorizedException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\UnauthorizedException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\UnauthorizedException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\UnauthorizedException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\UnauthorizedException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\UnauthorizedException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\UnavailableException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\UnavailableException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\UnavailableException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\UnavailableException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\UnavailableException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\UnavailableException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\UnavailableException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\UnavailableException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\UnpreparedException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\UnpreparedException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\UnpreparedException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\UnpreparedException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\UnpreparedException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\UnpreparedException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\UnpreparedException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\UnpreparedException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\ValidationException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\ValidationException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\ValidationException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\ValidationException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\ValidationException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\ValidationException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\ValidationException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\ValidationException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\Exception\WriteTimeoutException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Cassandra\Exception\WriteTimeoutException::getCode' => 'Gets the Exception code', +'Cassandra\Exception\WriteTimeoutException::getFile' => 'Gets the file in which the exception occurred', +'Cassandra\Exception\WriteTimeoutException::getLine' => 'Gets the line in which the exception occurred', +'Cassandra\Exception\WriteTimeoutException::getMessage' => 'Gets the Exception message', +'Cassandra\Exception\WriteTimeoutException::getPrevious' => 'Returns previous Exception', +'Cassandra\Exception\WriteTimeoutException::getTrace' => 'Gets the stack trace', +'Cassandra\Exception\WriteTimeoutException::getTraceAsString' => 'Gets the stack trace as a string', +'Cassandra\ExecutionOptions::__construct' => 'Creates a new options object for execution.', +'Cassandra\Float_::__construct' => 'Creates a new float.', +'Cassandra\Float_::__toString' => 'Returns string representation of the float value.', +'Cassandra\Float_::abs' => '`@return \Cassandra\Numeric` absolute value', +'Cassandra\Float_::add' => '`@return \Cassandra\Numeric` sum', +'Cassandra\Float_::div' => '`@return \Cassandra\Numeric` quotient', +'Cassandra\Float_::max' => 'Maximum possible Float value', +'Cassandra\Float_::min' => 'Minimum possible Float value', +'Cassandra\Float_::mod' => '`@return \Cassandra\Numeric` remainder', +'Cassandra\Float_::mul' => '`@return \Cassandra\Numeric` product', +'Cassandra\Float_::neg' => '`@return \Cassandra\Numeric` negative value', +'Cassandra\Float_::sqrt' => '`@return \Cassandra\Numeric` square root', +'Cassandra\Float_::sub' => '`@return \Cassandra\Numeric` difference', +'Cassandra\Float_::toDouble' => '`@return float` this number as float', +'Cassandra\Float_::toInt' => '`@return int` this number as int', +'Cassandra\Float_::type' => 'The type of this float.', +'Cassandra\Float_::value' => 'Returns the float value.', +'Cassandra\Function_::arguments' => 'Returns the arguments of the function', +'Cassandra\Function_::body' => 'Returns the body of the function', +'Cassandra\Function_::isCalledOnNullInput' => 'Determines if a function is called when the value is null.', +'Cassandra\Function_::language' => 'Returns the lanuage of the function', +'Cassandra\Function_::name' => 'Returns the full name of the function', +'Cassandra\Function_::returnType' => 'Returns the return type of the function', +'Cassandra\Function_::signature' => 'Returns the signature of the function', +'Cassandra\Function_::simpleName' => 'Returns the simple name of the function', +'Cassandra\Future::get' => 'Waits for a given future resource to resolve and throws errors if any.', +'Cassandra\FutureClose::get' => 'Waits for a given future resource to resolve and throws errors if any.', +'Cassandra\FuturePreparedStatement::get' => 'Waits for a given future resource to resolve and throws errors if any.', +'Cassandra\FutureRows::get' => 'Waits for a given future resource to resolve and throws errors if any.', +'Cassandra\FutureSession::get' => 'Waits for a given future resource to resolve and throws errors if any.', +'Cassandra\FutureValue::get' => 'Waits for a given future resource to resolve and throws errors if any.', +'Cassandra\Index::className' => 'Returns the class name of the index', +'Cassandra\Index::isCustom' => 'Determines if the index is a custom index.', +'Cassandra\Index::kind' => 'Returns the kind of index', +'Cassandra\Index::name' => 'Returns the name of the index', +'Cassandra\Index::option' => 'Return a column\'s option by name', +'Cassandra\Index::options' => 'Returns all the index\'s options', +'Cassandra\Index::target' => 'Returns the target column of the index', +'Cassandra\Inet::__construct' => 'Creates a new IPv4 or IPv6 inet address.', +'Cassandra\Inet::__toString' => 'Returns the normalized string representation of the address.', +'Cassandra\Inet::address' => 'Returns the normalized string representation of the address.', +'Cassandra\Inet::type' => 'The type of this inet.', +'Cassandra\Keyspace::aggregate' => 'Get an aggregate by name and signature', +'Cassandra\Keyspace::aggregates' => 'Get all aggregates', +'Cassandra\Keyspace::function_' => 'Get a function by name and signature', +'Cassandra\Keyspace::functions' => 'Get all functions', +'Cassandra\Keyspace::hasDurableWrites' => 'Returns whether the keyspace has durable writes enabled', +'Cassandra\Keyspace::materializedView' => 'Get materialized view by name', +'Cassandra\Keyspace::materializedViews' => 'Gets all materialized views', +'Cassandra\Keyspace::name' => 'Returns keyspace name', +'Cassandra\Keyspace::replicationClassName' => 'Returns replication class name', +'Cassandra\Keyspace::replicationOptions' => 'Returns replication options', +'Cassandra\Keyspace::table' => 'Returns a table by name', +'Cassandra\Keyspace::tables' => 'Returns all tables defined in this keyspace', +'Cassandra\Keyspace::userType' => 'Get user type by name', +'Cassandra\Keyspace::userTypes' => 'Get all user types', +'Cassandra\Map::__construct' => 'Creates a new map of a given key and value type.', +'Cassandra\Map::count' => 'Total number of elements in this map', +'Cassandra\Map::current' => 'Current value for iteration', +'Cassandra\Map::get' => 'Gets the value of the key in the map.', +'Cassandra\Map::has' => 'Returns whether the key is in the map.', +'Cassandra\Map::key' => 'Current key for iteration', +'Cassandra\Map::keys' => 'Returns all keys in the map as an array.', +'Cassandra\Map::next' => 'Move internal iterator forward', +'Cassandra\Map::offsetExists' => 'Returns whether the value a given key is present', +'Cassandra\Map::offsetGet' => 'Retrieves the value at a given key', +'Cassandra\Map::offsetSet' => 'Sets the value at a given key', +'Cassandra\Map::offsetUnset' => 'Deletes the value at a given key', +'Cassandra\Map::remove' => 'Removes the key from the map.', +'Cassandra\Map::rewind' => 'Rewind internal iterator', +'Cassandra\Map::set' => 'Sets key/value in the map.', +'Cassandra\Map::type' => 'The type of this map.', +'Cassandra\Map::valid' => 'Check whether a current value exists', +'Cassandra\Map::values' => 'Returns all values in the map as an array.', +'Cassandra\MaterializedView::baseTable' => 'Returns the base table of the view', +'Cassandra\MaterializedView::bloomFilterFPChance' => 'Returns bloom filter FP chance', +'Cassandra\MaterializedView::caching' => 'Returns caching options', +'Cassandra\MaterializedView::clusteringKey' => 'Returns the clustering key columns of the view', +'Cassandra\MaterializedView::clusteringOrder' => '`@return array` A list of cluster column orders (\'asc\' and \'desc\')', +'Cassandra\MaterializedView::column' => 'Returns column by name', +'Cassandra\MaterializedView::columns' => 'Returns all columns in this view', +'Cassandra\MaterializedView::comment' => 'Description of the view, if any', +'Cassandra\MaterializedView::compactionStrategyClassName' => 'Returns compaction strategy class name', +'Cassandra\MaterializedView::compactionStrategyOptions' => 'Returns compaction strategy options', +'Cassandra\MaterializedView::compressionParameters' => 'Returns compression parameters', +'Cassandra\MaterializedView::defaultTTL' => 'Returns default TTL.', +'Cassandra\MaterializedView::gcGraceSeconds' => 'Returns GC grace seconds', +'Cassandra\MaterializedView::indexInterval' => 'Returns index interval', +'Cassandra\MaterializedView::localReadRepairChance' => 'Returns local read repair chance', +'Cassandra\MaterializedView::maxIndexInterval' => 'Returns the value of `max_index_interval`', +'Cassandra\MaterializedView::memtableFlushPeriodMs' => 'Returns memtable flush period in milliseconds', +'Cassandra\MaterializedView::minIndexInterval' => 'Returns the value of `min_index_interval`', +'Cassandra\MaterializedView::name' => 'Returns the name of this view', +'Cassandra\MaterializedView::option' => 'Return a view\'s option by name', +'Cassandra\MaterializedView::options' => 'Returns all the view\'s options', +'Cassandra\MaterializedView::partitionKey' => 'Returns the partition key columns of the view', +'Cassandra\MaterializedView::populateIOCacheOnFlush' => 'Returns whether or not the `populate_io_cache_on_flush` is true', +'Cassandra\MaterializedView::primaryKey' => 'Returns both the partition and clustering key columns of the view', +'Cassandra\MaterializedView::readRepairChance' => 'Returns read repair chance', +'Cassandra\MaterializedView::replicateOnWrite' => 'Returns whether or not the `replicate_on_write` is true', +'Cassandra\MaterializedView::speculativeRetry' => 'Returns speculative retry.', +'Cassandra\Numeric::abs' => '`@return \Cassandra\Numeric` absolute value', +'Cassandra\Numeric::add' => '`@return \Cassandra\Numeric` sum', +'Cassandra\Numeric::div' => '`@return \Cassandra\Numeric` quotient', +'Cassandra\Numeric::mod' => '`@return \Cassandra\Numeric` remainder', +'Cassandra\Numeric::mul' => '`@return \Cassandra\Numeric` product', +'Cassandra\Numeric::neg' => '`@return \Cassandra\Numeric` negative value', +'Cassandra\Numeric::sqrt' => '`@return \Cassandra\Numeric` square root', +'Cassandra\Numeric::sub' => '`@return \Cassandra\Numeric` difference', +'Cassandra\Numeric::toDouble' => '`@return float` this number as float', +'Cassandra\Numeric::toInt' => '`@return int` this number as int', +'Cassandra\RetryPolicy\Logging::__construct' => 'Creates a new Logging retry policy.', +'Cassandra\Rows::count' => 'Returns the number of rows.', +'Cassandra\Rows::current' => 'Returns current row.', +'Cassandra\Rows::first' => 'Get the first row.', +'Cassandra\Rows::isLastPage' => 'Check for the last page when paging.', +'Cassandra\Rows::key' => 'Returns current index.', +'Cassandra\Rows::next' => 'Advances the rows iterator by one.', +'Cassandra\Rows::nextPage' => 'Get the next page of results.', +'Cassandra\Rows::nextPageAsync' => 'Get the next page of results asynchronously.', +'Cassandra\Rows::offsetExists' => 'Returns existence of a given row.', +'Cassandra\Rows::offsetGet' => 'Returns a row at given index.', +'Cassandra\Rows::offsetSet' => 'Sets a row at given index.', +'Cassandra\Rows::offsetUnset' => 'Removes a row at given index.', +'Cassandra\Rows::pagingStateToken' => 'Returns the raw paging state token.', +'Cassandra\Rows::rewind' => 'Resets the rows iterator.', +'Cassandra\Rows::valid' => 'Returns existence of more rows being available.', +'Cassandra\Schema::keyspace' => 'Returns a Keyspace instance by name.', +'Cassandra\Schema::keyspaces' => 'Returns all keyspaces defined in the schema.', +'Cassandra\Session::close' => 'Close the session and all its connections.', +'Cassandra\Session::closeAsync' => 'Asynchronously close the session and all its connections.', +'Cassandra\Session::execute' => 'Execute a query. + +Available execution options: +| Option Name | Option **Type** | Option Details | +|--------------------|-----------------|----------------------------------------------------------------------------------------------------------| +| arguments | array | An array or positional or named arguments | +| consistency | int | A consistency constant e.g Dse::CONSISTENCY_ONE, Dse::CONSISTENCY_QUORUM, etc. | +| timeout | int | A number of rows to include in result for paging | +| paging_state_token | string | A string token use to resume from the state of a previous result set | +| retry_policy | RetryPolicy | A retry policy that is used to handle server-side failures for this request | +| serial_consistency | int | Either Dse::CONSISTENCY_SERIAL or Dse::CONSISTENCY_LOCAL_SERIAL | +| timestamp | int\|string | Either an integer or integer string timestamp that represents the number of microseconds since the epoch | +| execute_as | string | User to execute statement as |', +'Cassandra\Session::executeAsync' => 'Execute a query asynchronously. This method returns immediately, but +the query continues execution in the background.', +'Cassandra\Session::metrics' => 'Get performance and diagnostic metrics.', +'Cassandra\Session::prepare' => 'Prepare a query for execution.', +'Cassandra\Session::prepareAsync' => 'Asynchronously prepare a query for execution.', +'Cassandra\Session::schema' => 'Get a snapshot of the cluster\'s current schema.', +'Cassandra\Set::__construct' => 'Creates a new collection of a given type.', +'Cassandra\Set::add' => 'Adds a value to this set.', +'Cassandra\Set::count' => 'Total number of elements in this set', +'Cassandra\Set::current' => 'Current element for iteration', +'Cassandra\Set::has' => 'Returns whether a value is in this set.', +'Cassandra\Set::key' => 'Current key for iteration', +'Cassandra\Set::next' => 'Move internal iterator forward', +'Cassandra\Set::remove' => 'Removes a value to this set.', +'Cassandra\Set::rewind' => 'Rewind internal iterator', +'Cassandra\Set::type' => 'The type of this set.', +'Cassandra\Set::valid' => 'Check whether a current value exists', +'Cassandra\Set::values' => 'Array of values in this set.', +'Cassandra\SimpleStatement::__construct' => 'Creates a new simple statement with the provided CQL.', +'Cassandra\Smallint::__construct' => 'Creates a new 16-bit signed integer.', +'Cassandra\Smallint::abs' => '`@return \Cassandra\Numeric` absolute value', +'Cassandra\Smallint::add' => '`@return \Cassandra\Numeric` sum', +'Cassandra\Smallint::div' => '`@return \Cassandra\Numeric` quotient', +'Cassandra\Smallint::max' => 'Maximum possible Smallint value', +'Cassandra\Smallint::min' => 'Minimum possible Smallint value', +'Cassandra\Smallint::mod' => '`@return \Cassandra\Numeric` remainder', +'Cassandra\Smallint::mul' => '`@return \Cassandra\Numeric` product', +'Cassandra\Smallint::neg' => '`@return \Cassandra\Numeric` negative value', +'Cassandra\Smallint::sqrt' => '`@return \Cassandra\Numeric` square root', +'Cassandra\Smallint::sub' => '`@return \Cassandra\Numeric` difference', +'Cassandra\Smallint::toDouble' => '`@return float` this number as float', +'Cassandra\Smallint::toInt' => '`@return int` this number as int', +'Cassandra\Smallint::type' => 'The type of this value (smallint).', +'Cassandra\Smallint::value' => 'Returns the integer value.', +'Cassandra\SSLOptions\Builder::build' => 'Builds SSL options.', +'Cassandra\SSLOptions\Builder::withClientCert' => 'Set client-side certificate chain. + +This is used to authenticate the client on the server-side. This should contain the entire Certificate +chain starting with the certificate itself.', +'Cassandra\SSLOptions\Builder::withPrivateKey' => 'Set client-side private key. This is used to authenticate the client on +the server-side.', +'Cassandra\SSLOptions\Builder::withTrustedCerts' => 'Adds a trusted certificate. This is used to verify node\'s identity.', +'Cassandra\SSLOptions\Builder::withVerifyFlags' => 'Disable certificate verification.', +'Cassandra\Table::bloomFilterFPChance' => 'Returns bloom filter FP chance', +'Cassandra\Table::caching' => 'Returns caching options', +'Cassandra\Table::clusteringKey' => 'Returns the clustering key columns of the table', +'Cassandra\Table::clusteringOrder' => '`@return array` A list of cluster column orders (\'asc\' and \'desc\')', +'Cassandra\Table::column' => 'Returns column by name', +'Cassandra\Table::columns' => 'Returns all columns in this table', +'Cassandra\Table::comment' => 'Description of the table, if any', +'Cassandra\Table::compactionStrategyClassName' => 'Returns compaction strategy class name', +'Cassandra\Table::compactionStrategyOptions' => 'Returns compaction strategy options', +'Cassandra\Table::compressionParameters' => 'Returns compression parameters', +'Cassandra\Table::defaultTTL' => 'Returns default TTL.', +'Cassandra\Table::gcGraceSeconds' => 'Returns GC grace seconds', +'Cassandra\Table::indexInterval' => 'Returns index interval', +'Cassandra\Table::localReadRepairChance' => 'Returns local read repair chance', +'Cassandra\Table::maxIndexInterval' => 'Returns the value of `max_index_interval`', +'Cassandra\Table::memtableFlushPeriodMs' => 'Returns memtable flush period in milliseconds', +'Cassandra\Table::minIndexInterval' => 'Returns the value of `min_index_interval`', +'Cassandra\Table::name' => 'Returns the name of this table', +'Cassandra\Table::option' => 'Return a table\'s option by name', +'Cassandra\Table::options' => 'Returns all the table\'s options', +'Cassandra\Table::partitionKey' => 'Returns the partition key columns of the table', +'Cassandra\Table::populateIOCacheOnFlush' => 'Returns whether or not the `populate_io_cache_on_flush` is true', +'Cassandra\Table::primaryKey' => 'Returns both the partition and clustering key columns of the table', +'Cassandra\Table::readRepairChance' => 'Returns read repair chance', +'Cassandra\Table::replicateOnWrite' => 'Returns whether or not the `replicate_on_write` is true', +'Cassandra\Table::speculativeRetry' => 'Returns speculative retry.', +'Cassandra\Time::__construct' => 'Creates a new Time object', +'Cassandra\Time::__toString' => '`@return string` this date in string format: Time(nanoseconds=$nanoseconds)', +'Cassandra\Time::type' => 'The type of this date.', +'Cassandra\Timestamp::__construct' => 'Creates a new timestamp from either unix timestamp and microseconds or +from the current time by default.', +'Cassandra\Timestamp::__toString' => 'Returns a string representation of this timestamp.', +'Cassandra\Timestamp::microtime' => 'Microtime from this timestamp', +'Cassandra\Timestamp::time' => 'Unix timestamp.', +'Cassandra\Timestamp::toDateTime' => 'Converts current timestamp to PHP DateTime.', +'Cassandra\Timestamp::type' => 'The type of this timestamp.', +'Cassandra\Timeuuid::__construct' => 'Creates a timeuuid from a given timestamp or current time.', +'Cassandra\Timeuuid::__toString' => 'Returns this timeuuid as string.', +'Cassandra\Timeuuid::time' => 'Unix timestamp.', +'Cassandra\Timeuuid::toDateTime' => 'Converts current timeuuid to PHP DateTime.', +'Cassandra\Timeuuid::type' => 'The type of this timeuuid.', +'Cassandra\Timeuuid::uuid' => 'Returns this timeuuid as string.', +'Cassandra\Timeuuid::version' => 'Returns the version of this timeuuid.', +'Cassandra\Tinyint::__construct' => 'Creates a new 8-bit signed integer.', +'Cassandra\Tinyint::abs' => '`@return \Cassandra\Numeric` absolute value', +'Cassandra\Tinyint::add' => '`@return \Cassandra\Numeric` sum', +'Cassandra\Tinyint::div' => '`@return \Cassandra\Numeric` quotient', +'Cassandra\Tinyint::max' => 'Maximum possible Tinyint value', +'Cassandra\Tinyint::min' => 'Minimum possible Tinyint value', +'Cassandra\Tinyint::mod' => '`@return \Cassandra\Numeric` remainder', +'Cassandra\Tinyint::mul' => '`@return \Cassandra\Numeric` product', +'Cassandra\Tinyint::neg' => '`@return \Cassandra\Numeric` negative value', +'Cassandra\Tinyint::sqrt' => '`@return \Cassandra\Numeric` square root', +'Cassandra\Tinyint::sub' => '`@return \Cassandra\Numeric` difference', +'Cassandra\Tinyint::toDouble' => '`@return float` this number as float', +'Cassandra\Tinyint::toInt' => '`@return int` this number as int', +'Cassandra\Tinyint::type' => 'The type of this value (tinyint).', +'Cassandra\Tinyint::value' => 'Returns the integer value.', +'Cassandra\Tuple::__construct' => 'Creates a new tuple with the given types.', +'Cassandra\Tuple::count' => 'Total number of elements in this tuple', +'Cassandra\Tuple::current' => 'Current element for iteration', +'Cassandra\Tuple::get' => 'Retrieves the value at a given index.', +'Cassandra\Tuple::key' => 'Current key for iteration', +'Cassandra\Tuple::next' => 'Move internal iterator forward', +'Cassandra\Tuple::rewind' => 'Rewind internal iterator', +'Cassandra\Tuple::set' => 'Sets the value at index in this tuple .', +'Cassandra\Tuple::type' => 'The type of this tuple.', +'Cassandra\Tuple::valid' => 'Check whether a current value exists', +'Cassandra\Tuple::values' => 'Array of values in this tuple.', +'Cassandra\Type::__toString' => 'Returns string representation of this type.', +'Cassandra\Type::ascii' => 'Get representation of ascii type', +'Cassandra\Type::bigint' => 'Get representation of bigint type', +'Cassandra\Type::blob' => 'Get representation of blob type', +'Cassandra\Type::boolean' => 'Get representation of boolean type', +'Cassandra\Type::collection' => 'Initialize a Collection type', +'Cassandra\Type::counter' => 'Get representation of counter type', +'Cassandra\Type::date' => 'Get representation of date type', +'Cassandra\Type::decimal' => 'Get representation of decimal type', +'Cassandra\Type::double' => 'Get representation of double type', +'Cassandra\Type::duration' => 'Get representation of duration type', +'Cassandra\Type::float' => 'Get representation of float type', +'Cassandra\Type::inet' => 'Get representation of inet type', +'Cassandra\Type::int' => 'Get representation of int type', +'Cassandra\Type::map' => 'Initialize a map type', +'Cassandra\Type::name' => 'Returns the name of this type as string.', +'Cassandra\Type::set' => 'Initialize a set type', +'Cassandra\Type::smallint' => 'Get representation of smallint type', +'Cassandra\Type::text' => 'Get representation of text type', +'Cassandra\Type::time' => 'Get representation of time type', +'Cassandra\Type::timestamp' => 'Get representation of timestamp type', +'Cassandra\Type::timeuuid' => 'Get representation of timeuuid type', +'Cassandra\Type::tinyint' => 'Get representation of tinyint type', +'Cassandra\Type::tuple' => 'Initialize a tuple type', +'Cassandra\Type::userType' => 'Initialize a user type', +'Cassandra\Type::uuid' => 'Get representation of uuid type', +'Cassandra\Type::varchar' => 'Get representation of varchar type', +'Cassandra\Type::varint' => 'Get representation of varint type', +'Cassandra\Type\Collection::__toString' => 'Returns type representation in CQL, e.g. `list`', +'Cassandra\Type\Collection::ascii' => 'Get representation of ascii type', +'Cassandra\Type\Collection::bigint' => 'Get representation of bigint type', +'Cassandra\Type\Collection::blob' => 'Get representation of blob type', +'Cassandra\Type\Collection::boolean' => 'Get representation of boolean type', +'Cassandra\Type\Collection::collection' => 'Initialize a Collection type', +'Cassandra\Type\Collection::counter' => 'Get representation of counter type', +'Cassandra\Type\Collection::create' => 'Creates a new Collection from the given values. When no values +given, creates an empty list.', +'Cassandra\Type\Collection::date' => 'Get representation of date type', +'Cassandra\Type\Collection::decimal' => 'Get representation of decimal type', +'Cassandra\Type\Collection::double' => 'Get representation of double type', +'Cassandra\Type\Collection::duration' => 'Get representation of duration type', +'Cassandra\Type\Collection::float' => 'Get representation of float type', +'Cassandra\Type\Collection::inet' => 'Get representation of inet type', +'Cassandra\Type\Collection::int' => 'Get representation of int type', +'Cassandra\Type\Collection::map' => 'Initialize a map type', +'Cassandra\Type\Collection::name' => 'Returns "list"', +'Cassandra\Type\Collection::set' => 'Initialize a set type', +'Cassandra\Type\Collection::smallint' => 'Get representation of smallint type', +'Cassandra\Type\Collection::text' => 'Get representation of text type', +'Cassandra\Type\Collection::time' => 'Get representation of time type', +'Cassandra\Type\Collection::timestamp' => 'Get representation of timestamp type', +'Cassandra\Type\Collection::timeuuid' => 'Get representation of timeuuid type', +'Cassandra\Type\Collection::tinyint' => 'Get representation of tinyint type', +'Cassandra\Type\Collection::tuple' => 'Initialize a tuple type', +'Cassandra\Type\Collection::userType' => 'Initialize a user type', +'Cassandra\Type\Collection::uuid' => 'Get representation of uuid type', +'Cassandra\Type\Collection::valueType' => 'Returns type of values', +'Cassandra\Type\Collection::varchar' => 'Get representation of varchar type', +'Cassandra\Type\Collection::varint' => 'Get representation of varint type', +'Cassandra\Type\Custom::__toString' => 'Returns string representation of this type.', +'Cassandra\Type\Custom::ascii' => 'Get representation of ascii type', +'Cassandra\Type\Custom::bigint' => 'Get representation of bigint type', +'Cassandra\Type\Custom::blob' => 'Get representation of blob type', +'Cassandra\Type\Custom::boolean' => 'Get representation of boolean type', +'Cassandra\Type\Custom::collection' => 'Initialize a Collection type', +'Cassandra\Type\Custom::counter' => 'Get representation of counter type', +'Cassandra\Type\Custom::date' => 'Get representation of date type', +'Cassandra\Type\Custom::decimal' => 'Get representation of decimal type', +'Cassandra\Type\Custom::double' => 'Get representation of double type', +'Cassandra\Type\Custom::duration' => 'Get representation of duration type', +'Cassandra\Type\Custom::float' => 'Get representation of float type', +'Cassandra\Type\Custom::inet' => 'Get representation of inet type', +'Cassandra\Type\Custom::int' => 'Get representation of int type', +'Cassandra\Type\Custom::map' => 'Initialize a map type', +'Cassandra\Type\Custom::name' => 'Returns the name of this type as string.', +'Cassandra\Type\Custom::set' => 'Initialize a set type', +'Cassandra\Type\Custom::smallint' => 'Get representation of smallint type', +'Cassandra\Type\Custom::text' => 'Get representation of text type', +'Cassandra\Type\Custom::time' => 'Get representation of time type', +'Cassandra\Type\Custom::timestamp' => 'Get representation of timestamp type', +'Cassandra\Type\Custom::timeuuid' => 'Get representation of timeuuid type', +'Cassandra\Type\Custom::tinyint' => 'Get representation of tinyint type', +'Cassandra\Type\Custom::tuple' => 'Initialize a tuple type', +'Cassandra\Type\Custom::userType' => 'Initialize a user type', +'Cassandra\Type\Custom::uuid' => 'Get representation of uuid type', +'Cassandra\Type\Custom::varchar' => 'Get representation of varchar type', +'Cassandra\Type\Custom::varint' => 'Get representation of varint type', +'Cassandra\Type\Map::__toString' => 'Returns type representation in CQL, e.g. `map`', +'Cassandra\Type\Map::ascii' => 'Get representation of ascii type', +'Cassandra\Type\Map::bigint' => 'Get representation of bigint type', +'Cassandra\Type\Map::blob' => 'Get representation of blob type', +'Cassandra\Type\Map::boolean' => 'Get representation of boolean type', +'Cassandra\Type\Map::collection' => 'Initialize a Collection type', +'Cassandra\Type\Map::counter' => 'Get representation of counter type', +'Cassandra\Type\Map::create' => 'Creates a new Map from the given values.', +'Cassandra\Type\Map::date' => 'Get representation of date type', +'Cassandra\Type\Map::decimal' => 'Get representation of decimal type', +'Cassandra\Type\Map::double' => 'Get representation of double type', +'Cassandra\Type\Map::duration' => 'Get representation of duration type', +'Cassandra\Type\Map::float' => 'Get representation of float type', +'Cassandra\Type\Map::inet' => 'Get representation of inet type', +'Cassandra\Type\Map::int' => 'Get representation of int type', +'Cassandra\Type\Map::keyType' => 'Returns type of keys', +'Cassandra\Type\Map::map' => 'Initialize a map type', +'Cassandra\Type\Map::name' => 'Returns "map"', +'Cassandra\Type\Map::set' => 'Initialize a set type', +'Cassandra\Type\Map::smallint' => 'Get representation of smallint type', +'Cassandra\Type\Map::text' => 'Get representation of text type', +'Cassandra\Type\Map::time' => 'Get representation of time type', +'Cassandra\Type\Map::timestamp' => 'Get representation of timestamp type', +'Cassandra\Type\Map::timeuuid' => 'Get representation of timeuuid type', +'Cassandra\Type\Map::tinyint' => 'Get representation of tinyint type', +'Cassandra\Type\Map::tuple' => 'Initialize a tuple type', +'Cassandra\Type\Map::userType' => 'Initialize a user type', +'Cassandra\Type\Map::uuid' => 'Get representation of uuid type', +'Cassandra\Type\Map::valueType' => 'Returns type of values', +'Cassandra\Type\Map::varchar' => 'Get representation of varchar type', +'Cassandra\Type\Map::varint' => 'Get representation of varint type', +'Cassandra\Type\Scalar::__toString' => 'Returns string representation of this type.', +'Cassandra\Type\Scalar::ascii' => 'Get representation of ascii type', +'Cassandra\Type\Scalar::bigint' => 'Get representation of bigint type', +'Cassandra\Type\Scalar::blob' => 'Get representation of blob type', +'Cassandra\Type\Scalar::boolean' => 'Get representation of boolean type', +'Cassandra\Type\Scalar::collection' => 'Initialize a Collection type', +'Cassandra\Type\Scalar::counter' => 'Get representation of counter type', +'Cassandra\Type\Scalar::date' => 'Get representation of date type', +'Cassandra\Type\Scalar::decimal' => 'Get representation of decimal type', +'Cassandra\Type\Scalar::double' => 'Get representation of double type', +'Cassandra\Type\Scalar::duration' => 'Get representation of duration type', +'Cassandra\Type\Scalar::float' => 'Get representation of float type', +'Cassandra\Type\Scalar::inet' => 'Get representation of inet type', +'Cassandra\Type\Scalar::int' => 'Get representation of int type', +'Cassandra\Type\Scalar::map' => 'Initialize a map type', +'Cassandra\Type\Scalar::name' => 'Returns the name of this type as string.', +'Cassandra\Type\Scalar::set' => 'Initialize a set type', +'Cassandra\Type\Scalar::smallint' => 'Get representation of smallint type', +'Cassandra\Type\Scalar::text' => 'Get representation of text type', +'Cassandra\Type\Scalar::time' => 'Get representation of time type', +'Cassandra\Type\Scalar::timestamp' => 'Get representation of timestamp type', +'Cassandra\Type\Scalar::timeuuid' => 'Get representation of timeuuid type', +'Cassandra\Type\Scalar::tinyint' => 'Get representation of tinyint type', +'Cassandra\Type\Scalar::tuple' => 'Initialize a tuple type', +'Cassandra\Type\Scalar::userType' => 'Initialize a user type', +'Cassandra\Type\Scalar::uuid' => 'Get representation of uuid type', +'Cassandra\Type\Scalar::varchar' => 'Get representation of varchar type', +'Cassandra\Type\Scalar::varint' => 'Get representation of varint type', +'Cassandra\Type\Set::__toString' => 'Returns type representation in CQL, e.g. `set`', +'Cassandra\Type\Set::ascii' => 'Get representation of ascii type', +'Cassandra\Type\Set::bigint' => 'Get representation of bigint type', +'Cassandra\Type\Set::blob' => 'Get representation of blob type', +'Cassandra\Type\Set::boolean' => 'Get representation of boolean type', +'Cassandra\Type\Set::collection' => 'Initialize a Collection type', +'Cassandra\Type\Set::counter' => 'Get representation of counter type', +'Cassandra\Type\Set::create' => 'Creates a new Set from the given values.', +'Cassandra\Type\Set::date' => 'Get representation of date type', +'Cassandra\Type\Set::decimal' => 'Get representation of decimal type', +'Cassandra\Type\Set::double' => 'Get representation of double type', +'Cassandra\Type\Set::duration' => 'Get representation of duration type', +'Cassandra\Type\Set::float' => 'Get representation of float type', +'Cassandra\Type\Set::inet' => 'Get representation of inet type', +'Cassandra\Type\Set::int' => 'Get representation of int type', +'Cassandra\Type\Set::map' => 'Initialize a map type', +'Cassandra\Type\Set::name' => 'Returns "set"', +'Cassandra\Type\Set::set' => 'Initialize a set type', +'Cassandra\Type\Set::smallint' => 'Get representation of smallint type', +'Cassandra\Type\Set::text' => 'Get representation of text type', +'Cassandra\Type\Set::time' => 'Get representation of time type', +'Cassandra\Type\Set::timestamp' => 'Get representation of timestamp type', +'Cassandra\Type\Set::timeuuid' => 'Get representation of timeuuid type', +'Cassandra\Type\Set::tinyint' => 'Get representation of tinyint type', +'Cassandra\Type\Set::tuple' => 'Initialize a tuple type', +'Cassandra\Type\Set::userType' => 'Initialize a user type', +'Cassandra\Type\Set::uuid' => 'Get representation of uuid type', +'Cassandra\Type\Set::valueType' => 'Returns type of values', +'Cassandra\Type\Set::varchar' => 'Get representation of varchar type', +'Cassandra\Type\Set::varint' => 'Get representation of varint type', +'Cassandra\Type\Tuple::__toString' => 'Returns type representation in CQL, e.g. `tuple`', +'Cassandra\Type\Tuple::ascii' => 'Get representation of ascii type', +'Cassandra\Type\Tuple::bigint' => 'Get representation of bigint type', +'Cassandra\Type\Tuple::blob' => 'Get representation of blob type', +'Cassandra\Type\Tuple::boolean' => 'Get representation of boolean type', +'Cassandra\Type\Tuple::collection' => 'Initialize a Collection type', +'Cassandra\Type\Tuple::counter' => 'Get representation of counter type', +'Cassandra\Type\Tuple::create' => 'Creates a new Tuple from the given values. When no values given, +creates a tuple with null for the values.', +'Cassandra\Type\Tuple::date' => 'Get representation of date type', +'Cassandra\Type\Tuple::decimal' => 'Get representation of decimal type', +'Cassandra\Type\Tuple::double' => 'Get representation of double type', +'Cassandra\Type\Tuple::duration' => 'Get representation of duration type', +'Cassandra\Type\Tuple::float' => 'Get representation of float type', +'Cassandra\Type\Tuple::inet' => 'Get representation of inet type', +'Cassandra\Type\Tuple::int' => 'Get representation of int type', +'Cassandra\Type\Tuple::map' => 'Initialize a map type', +'Cassandra\Type\Tuple::name' => 'Returns "tuple"', +'Cassandra\Type\Tuple::set' => 'Initialize a set type', +'Cassandra\Type\Tuple::smallint' => 'Get representation of smallint type', +'Cassandra\Type\Tuple::text' => 'Get representation of text type', +'Cassandra\Type\Tuple::time' => 'Get representation of time type', +'Cassandra\Type\Tuple::timestamp' => 'Get representation of timestamp type', +'Cassandra\Type\Tuple::timeuuid' => 'Get representation of timeuuid type', +'Cassandra\Type\Tuple::tinyint' => 'Get representation of tinyint type', +'Cassandra\Type\Tuple::tuple' => 'Initialize a tuple type', +'Cassandra\Type\Tuple::types' => 'Returns types of values', +'Cassandra\Type\Tuple::userType' => 'Initialize a user type', +'Cassandra\Type\Tuple::uuid' => 'Get representation of uuid type', +'Cassandra\Type\Tuple::varchar' => 'Get representation of varchar type', +'Cassandra\Type\Tuple::varint' => 'Get representation of varint type', +'Cassandra\Type\UserType::__toString' => 'Returns type representation in CQL, e.g. keyspace1.type_name1 or +`userType`.', +'Cassandra\Type\UserType::ascii' => 'Get representation of ascii type', +'Cassandra\Type\UserType::bigint' => 'Get representation of bigint type', +'Cassandra\Type\UserType::blob' => 'Get representation of blob type', +'Cassandra\Type\UserType::boolean' => 'Get representation of boolean type', +'Cassandra\Type\UserType::collection' => 'Initialize a Collection type', +'Cassandra\Type\UserType::counter' => 'Get representation of counter type', +'Cassandra\Type\UserType::create' => 'Creates a new UserTypeValue from the given name/value pairs. When +no values given, creates an empty user type.', +'Cassandra\Type\UserType::date' => 'Get representation of date type', +'Cassandra\Type\UserType::decimal' => 'Get representation of decimal type', +'Cassandra\Type\UserType::double' => 'Get representation of double type', +'Cassandra\Type\UserType::duration' => 'Get representation of duration type', +'Cassandra\Type\UserType::float' => 'Get representation of float type', +'Cassandra\Type\UserType::inet' => 'Get representation of inet type', +'Cassandra\Type\UserType::int' => 'Get representation of int type', +'Cassandra\Type\UserType::keyspace' => 'Returns keyspace for the user type', +'Cassandra\Type\UserType::map' => 'Initialize a map type', +'Cassandra\Type\UserType::name' => 'Returns type name for the user type', +'Cassandra\Type\UserType::set' => 'Initialize a set type', +'Cassandra\Type\UserType::smallint' => 'Get representation of smallint type', +'Cassandra\Type\UserType::text' => 'Get representation of text type', +'Cassandra\Type\UserType::time' => 'Get representation of time type', +'Cassandra\Type\UserType::timestamp' => 'Get representation of timestamp type', +'Cassandra\Type\UserType::timeuuid' => 'Get representation of timeuuid type', +'Cassandra\Type\UserType::tinyint' => 'Get representation of tinyint type', +'Cassandra\Type\UserType::tuple' => 'Initialize a tuple type', +'Cassandra\Type\UserType::types' => 'Returns types of values', +'Cassandra\Type\UserType::userType' => 'Initialize a user type', +'Cassandra\Type\UserType::uuid' => 'Get representation of uuid type', +'Cassandra\Type\UserType::varchar' => 'Get representation of varchar type', +'Cassandra\Type\UserType::varint' => 'Get representation of varint type', +'Cassandra\Type\UserType::withKeyspace' => 'Associate the user type with a keyspace.', +'Cassandra\Type\UserType::withName' => 'Associate the user type with a name.', +'Cassandra\UserTypeValue::__construct' => 'Creates a new user type value with the given name/type pairs.', +'Cassandra\UserTypeValue::count' => 'Total number of elements in this user type value.', +'Cassandra\UserTypeValue::current' => 'Current element for iteration', +'Cassandra\UserTypeValue::get' => 'Retrieves the value at a given name.', +'Cassandra\UserTypeValue::key' => 'Current key for iteration', +'Cassandra\UserTypeValue::next' => 'Move internal iterator forward', +'Cassandra\UserTypeValue::rewind' => 'Rewind internal iterator', +'Cassandra\UserTypeValue::set' => 'Sets the value at name in this user type value.', +'Cassandra\UserTypeValue::type' => 'The type of this user type value.', +'Cassandra\UserTypeValue::valid' => 'Check whether a current value exists', +'Cassandra\UserTypeValue::values' => 'Array of values in this user type value.', +'Cassandra\Uuid::__construct' => 'Creates a uuid from a given uuid string or a random one.', +'Cassandra\Uuid::__toString' => 'Returns this uuid as string.', +'Cassandra\Uuid::type' => 'The type of this uuid.', +'Cassandra\Uuid::uuid' => 'Returns this uuid as string.', +'Cassandra\Uuid::version' => 'Returns the version of this uuid.', +'Cassandra\UuidInterface::uuid' => 'Returns this uuid as string.', +'Cassandra\UuidInterface::version' => 'Returns the version of this uuid.', +'Cassandra\Value::type' => 'The type of represented by the value.', +'Cassandra\Varint::__construct' => 'Creates a new variable length integer.', +'Cassandra\Varint::__toString' => 'Returns the integer value.', +'Cassandra\Varint::abs' => '`@return \Cassandra\Numeric` absolute value', +'Cassandra\Varint::add' => '`@return \Cassandra\Numeric` sum', +'Cassandra\Varint::div' => '`@return \Cassandra\Numeric` quotient', +'Cassandra\Varint::mod' => '`@return \Cassandra\Numeric` remainder', +'Cassandra\Varint::mul' => '`@return \Cassandra\Numeric` product', +'Cassandra\Varint::neg' => '`@return \Cassandra\Numeric` negative value', +'Cassandra\Varint::sqrt' => '`@return \Cassandra\Numeric` square root', +'Cassandra\Varint::sub' => '`@return \Cassandra\Numeric` difference', +'Cassandra\Varint::toDouble' => '`@return float` this number as float', +'Cassandra\Varint::toInt' => '`@return int` this number as int', +'Cassandra\Varint::type' => 'The type of this varint.', +'Cassandra\Varint::value' => 'Returns the integer value.', +'ceil' => 'Round fractions up', +'chdb::__construct' => 'Creates a chdb instance', +'chdb::get' => 'Gets the value associated with a key', +'chdb_create' => 'Creates a chdb file', +'chdir' => 'Change directory', +'checkdate' => 'Validate a Gregorian date', +'checkdnsrr' => 'Check DNS records corresponding to a given Internet host name or IP address', +'chgrp' => 'Changes file group', +'chmod' => 'Changes file mode', +'chop' => 'Alias of rtrim', +'chown' => 'Changes file owner', +'chr' => 'Generate a single-byte string from a number', +'chroot' => 'Change the root directory', +'chunk_split' => 'Split a string into smaller chunks', +'class_alias' => 'Creates an alias for a class', +'class_exists' => 'Checks if the class has been defined', +'class_implements' => 'Return the interfaces which are implemented by the given class or interface', +'class_parents' => 'Return the parent classes of the given class', +'class_uses' => 'Return the traits used by the given class', +'classkit_import' => 'Import new class method definitions from a file', +'classkit_method_add' => 'Dynamically adds a new method to a given class', +'classkit_method_copy' => 'Copies a method from class to another', +'classkit_method_redefine' => 'Dynamically changes the code of the given method', +'classkit_method_remove' => 'Dynamically removes the given method', +'classkit_method_rename' => 'Dynamically changes the name of the given method', +'classObj::__construct' => 'The second argument class is optional. If given, the new class +created will be a copy of this class.', +'classObj::addLabel' => 'Add a labelObj to the classObj and return its index in the labels +array. +.. versionadded:: 6.2', +'classObj::convertToString' => 'Saves the object to a string. Provides the inverse option for +updateFromString.', +'classObj::createLegendIcon' => 'Draw the legend icon and return a new imageObj.', +'classObj::deletestyle' => 'Delete the style specified by the style index. If there are any +style that follow the deleted style, their index will decrease by 1.', +'classObj::drawLegendIcon' => 'Draw the legend icon on im object at dstX, dstY. +Returns MS_SUCCESS/MS_FAILURE.', +'classObj::free' => 'Free the object properties and break the internal references. +Note that you have to unset the php variable to free totally the +resources.', +'classObj::getExpressionString' => 'Returns the :ref:`expression ` string for the class +object.', +'classObj::getLabel' => 'Return a reference to the labelObj at *index* in the labels array. +See the labelObj_ section for more details on multiple class +labels. +.. versionadded:: 6.2', +'classObj::getMetaData' => 'Fetch class metadata entry by name. Returns "" if no entry +matches the name. Note that the search is case sensitive. +.. note:: +getMetaData\'s query is case sensitive.', +'classObj::getStyle' => 'Return the style object using an index. index >= 0 && +index < class->numstyles.', +'classObj::getTextString' => 'Returns the text string for the class object.', +'classObj::movestyledown' => 'The style specified by the style index will be moved down into +the array of classes. Returns MS_SUCCESS or MS_FAILURE. +ex class->movestyledown(0) will have the effect of moving style 0 +up to position 1, and the style at position 1 will be moved +to position 0.', +'classObj::movestyleup' => 'The style specified by the style index will be moved up into +the array of classes. Returns MS_SUCCESS or MS_FAILURE. +ex class->movestyleup(1) will have the effect of moving style 1 +up to position 0, and the style at position 0 will be moved +to position 1.', +'classObj::ms_newClassObj' => 'Old style constructor', +'classObj::removeLabel' => 'Remove the labelObj at *index* from the labels array and return a +reference to the labelObj. numlabels is decremented, and the +array is updated. +.. versionadded:: 6.2', +'classObj::removeMetaData' => 'Remove a metadata entry for the class. Returns MS_SUCCESS/MS_FAILURE.', +'classObj::set' => 'Set object property to a new value.', +'classObj::setExpression' => 'Set the :ref:`expression ` string for the class +object.', +'classObj::setMetaData' => 'Set a metadata entry for the class. Returns MS_SUCCESS/MS_FAILURE.', +'classObj::settext' => 'Set the text string for the class object.', +'classObj::updateFromString' => 'Update a class from a string snippet. Returns MS_SUCCESS/MS_FAILURE. +.. code-block:: php +set the color +$oClass->updateFromString(\'CLASS STYLE COLOR 255 0 255 END END\');', +'clearstatcache' => 'Clears file status cache', +'cli_get_process_title' => 'Returns the current process title', +'cli_set_process_title' => 'Sets the process title', +'ClosedGeneratorException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'ClosedGeneratorException::__toString' => 'String representation of the exception', +'ClosedGeneratorException::getCode' => 'Gets the Exception code', +'ClosedGeneratorException::getFile' => 'Gets the file in which the exception occurred', +'ClosedGeneratorException::getLine' => 'Gets the line in which the exception occurred', +'ClosedGeneratorException::getMessage' => 'Gets the Exception message', +'ClosedGeneratorException::getPrevious' => 'Returns previous Exception', +'ClosedGeneratorException::getTrace' => 'Gets the stack trace', +'ClosedGeneratorException::getTraceAsString' => 'Gets the stack trace as a string', +'closedir' => 'Close directory handle', +'closelog' => 'Close connection to system logger', +'Closure::__construct' => 'This method exists only to disallow instantiation of the Closure class. +Objects of this class are created in the fashion described on the anonymous functions page.', +'Closure::__invoke' => 'This is for consistency with other classes that implement calling magic, +as this method is not used for calling the function.', +'Closure::bind' => 'This method is a static version of Closure::bindTo(). +See the documentation of that method for more information.', +'Closure::bindTo' => 'Duplicates the closure with a new bound object and class scope', +'Closure::call' => 'Temporarily binds the closure to newthis, and calls it with any given parameters.', +'clusterObj::convertToString' => 'Saves the object to a string. Provides the inverse option for +updateFromString.', +'clusterObj::getFilterString' => 'Returns the :ref:`expression ` for this cluster +filter or NULL on error.', +'clusterObj::getGroupString' => 'Returns the :ref:`expression ` for this cluster group +or NULL on error.', +'clusterObj::setFilter' => 'Set layer filter :ref:`expression `.', +'clusterObj::setGroup' => 'Set layer group :ref:`expression `.', +'collator::__construct' => 'Create a collator', +'collator::asort' => 'Sort array maintaining index association', +'collator::compare' => 'Compare two Unicode strings', +'collator::create' => 'Create a collator', +'collator::getAttribute' => 'Get collation attribute value', +'collator::getErrorCode' => 'Get collator\'s last error code', +'collator::getErrorMessage' => 'Get text for collator\'s last error code', +'collator::getLocale' => 'Get the locale name of the collator', +'collator::getSortKey' => 'Get sorting key for a string', +'collator::getStrength' => 'Get current collation strength', +'collator::setAttribute' => 'Set collation attribute', +'collator::setStrength' => 'Set collation strength', +'collator::sort' => 'Sort array using specified collator', +'collator::sortWithSortKeys' => 'Sort array using specified collator and sort keys', +'collectable::isGarbage' => 'Determine whether an object has been marked as garbage', +'collectable::setGarbage' => 'Mark an object as garbage', +'colorObj::setHex' => 'Set red, green, blue and alpha values. The hex string should have the form +"#rrggbb" (alpha will be set to 255) or "#rrggbbaa". Returns MS_SUCCESS.', +'colorObj::toHex' => 'Get the color as a hex string "#rrggbb" or (if alpha is not 255) +"#rrggbbaa".', +'COM::__construct' => 'COM class constructor.', +'com_create_guid' => 'Generate a globally unique identifier (GUID)', +'com_event_sink' => 'Connect events from a COM object to a PHP object', +'com_get_active_object' => 'Returns a handle to an already running instance of a COM object', +'com_load_typelib' => 'Loads a Typelib', +'com_message_pump' => 'Process COM messages, sleeping for up to timeoutms milliseconds', +'com_print_typeinfo' => 'Print out a PHP class definition for a dispatchable interface', +'commonmark\cql::__construct' => 'CQL Construction', +'commonmark\cql::__invoke' => 'CQL Execution', +'commonmark\interfaces\ivisitable::accept' => 'Visitation', +'commonmark\interfaces\ivisitor::enter' => 'Visitation', +'commonmark\interfaces\ivisitor::leave' => 'Visitation', +'commonmark\node::accept' => 'Visitation', +'commonmark\node::appendChild' => 'AST Manipulation', +'commonmark\node::insertAfter' => 'AST Manipulation', +'commonmark\node::insertBefore' => 'AST Manipulation', +'commonmark\node::prependChild' => 'AST Manipulation', +'commonmark\node::replace' => 'AST Manipulation', +'commonmark\node::unlink' => 'AST Manipulation', +'commonmark\node\bulletlist::__construct' => 'BulletList Construction', +'commonmark\node\codeblock::__construct' => 'CodeBlock Construction', +'commonmark\node\heading::__construct' => 'Heading Construction', +'commonmark\node\image::__construct' => 'Image Construction', +'commonmark\node\link::__construct' => 'Link Construction', +'commonmark\node\orderedlist::__construct' => 'OrderedList Construction', +'commonmark\node\text::__construct' => 'Text Construction', +'commonmark\parse' => 'Parsing', +'commonmark\parser::__construct' => 'Parsing', +'commonmark\parser::finish' => 'Parsing', +'commonmark\parser::parse' => 'Parsing', +'commonmark\render' => 'Rendering', +'commonmark\render\html' => 'Rendering', +'commonmark\render\latex' => 'Rendering', +'commonmark\render\man' => 'Rendering', +'commonmark\render\xml' => 'Rendering', +'compact' => 'Create array containing variables and their values', +'compersisthelper::__construct' => 'Construct a COMPersistHelper object', +'compersisthelper::GetCurFileName' => 'Get current filename', +'compersisthelper::GetMaxStreamSize' => 'Get maximum stream size', +'compersisthelper::InitNew' => 'Initialize object to default state', +'compersisthelper::LoadFromFile' => 'Load object from file', +'compersisthelper::LoadFromStream' => 'Load object from stream', +'compersisthelper::SaveToFile' => 'Save object to file', +'compersisthelper::SaveToStream' => 'Save object to stream', +'CompileError::__clone' => 'Clone the error +Error can not be clone, so this method results in fatal error.', +'CompileError::__toString' => 'Gets a string representation of the thrown object', +'CompileError::getCode' => 'Gets the exception code', +'CompileError::getFile' => 'Gets the file in which the exception occurred', +'CompileError::getLine' => 'Gets the line on which the object was instantiated', +'CompileError::getPrevious' => 'Returns the previous Throwable', +'CompileError::getTrace' => 'Gets the stack trace', +'CompileError::getTraceAsString' => 'Gets the stack trace as a string', +'componere\abstract\definition::addInterface' => 'Add Interface', +'componere\abstract\definition::addMethod' => 'Add Method', +'componere\abstract\definition::addTrait' => 'Add Trait', +'componere\abstract\definition::getReflector' => 'Reflection', +'componere\cast' => 'Casting', +'componere\cast_by_ref' => 'Casting', +'componere\definition::__construct' => 'Definition Construction', +'componere\definition::addConstant' => 'Add Constant', +'componere\definition::addProperty' => 'Add Property', +'componere\definition::getClosure' => 'Get Closure', +'componere\definition::getClosures' => 'Get Closures', +'componere\definition::isRegistered' => 'State Detection', +'componere\definition::register' => 'Registration', +'componere\method::__construct' => 'Method Construction', +'componere\method::getReflector' => 'Reflection', +'componere\method::setPrivate' => 'Accessibility Modification', +'componere\method::setProtected' => 'Accessibility Modification', +'componere\method::setStatic' => 'Accessibility Modification', +'componere\patch::__construct' => 'Patch Construction', +'componere\patch::apply' => 'Application', +'componere\patch::derive' => 'Patch Derivation', +'componere\patch::getClosure' => 'Get Closure', +'componere\patch::getClosures' => 'Get Closures', +'componere\patch::isApplied' => 'State Detection', +'componere\patch::revert' => 'Reversal', +'componere\value::__construct' => 'Value Construction', +'componere\value::hasDefault' => 'Value Interaction', +'componere\value::isPrivate' => 'Accessibility Detection', +'componere\value::isProtected' => 'Accessibility Detection', +'componere\value::isStatic' => 'Accessibility Detection', +'componere\value::setPrivate' => 'Accessibility Modification', +'componere\value::setProtected' => 'Accessibility Modification', +'componere\value::setStatic' => 'Accessibility Modification', +'cond::broadcast' => 'Broadcast a Condition', +'cond::create' => 'Create a Condition', +'cond::destroy' => 'Destroy a Condition', +'cond::signal' => 'Signal a Condition', +'cond::wait' => 'Wait for Condition', +'connection_aborted' => 'Check whether client disconnected', +'connection_status' => 'Returns connection status bitfield', +'constant' => 'Returns the value of a constant', +'convert_cyr_string' => 'Convert from one Cyrillic character set to another', +'convert_uudecode' => 'Decode a uuencoded string', +'convert_uuencode' => 'Uuencode a string', +'copy' => 'Copies file', +'cos' => 'Cosine', +'cosh' => 'Hyperbolic cosine', +'Couchbase\AnalyticsQuery::fromString' => 'Creates new AnalyticsQuery instance directly from the string.', +'Couchbase\basicDecoderV1' => 'Decodes value according to Common Flags (RFC-20)', +'Couchbase\basicEncoderV1' => 'Encodes value according to Common Flags (RFC-20)', +'Couchbase\Bucket::append' => 'Appends content to a document. + +On the server side it just contatenate passed value to the existing one. +Note that this might make the value un-decodable. Consider sub-document API +for partial updates of the JSON documents.', +'Couchbase\Bucket::counter' => 'Increments or decrements a key (based on $delta)', +'Couchbase\Bucket::decryptFields' => 'Decrypt fields inside specified document.', +'Couchbase\Bucket::diag' => 'Collect and return information about state of internal network connections.', +'Couchbase\Bucket::encryptFields' => 'Encrypt fields inside specified document.', +'Couchbase\Bucket::get' => 'Retrieves a document', +'Couchbase\Bucket::getAndLock' => 'Retrieves a document and locks it. + +After the document has been locked on the server, its CAS would be masked, +and all mutations of it will be rejected until the server unlocks the document +automatically or it will be done manually with \Couchbase\Bucket::unlock() operation.', +'Couchbase\Bucket::getAndTouch' => 'Retrieves a document and updates its expiration time.', +'Couchbase\Bucket::getFromReplica' => 'Retrieves a document from a replica.', +'Couchbase\Bucket::getName' => 'Returns the name of the bucket for current connection', +'Couchbase\Bucket::insert' => 'Inserts a document. This operation will fail if the document already exists on the cluster.', +'Couchbase\Bucket::listExists' => 'Check if the list contains specified value', +'Couchbase\Bucket::listGet' => 'Get an element at the given position', +'Couchbase\Bucket::listPush' => 'Add an element to the end of the list', +'Couchbase\Bucket::listRemove' => 'Remove an element at the given position', +'Couchbase\Bucket::listSet' => 'Set an element at the given position', +'Couchbase\Bucket::listShift' => 'Add an element to the beginning of the list', +'Couchbase\Bucket::listSize' => 'Returns size of the list', +'Couchbase\Bucket::lookupIn' => 'Returns a builder for reading subdocument API.', +'Couchbase\Bucket::manager' => 'Returns an instance of a CouchbaseBucketManager for performing management operations against a bucket.', +'Couchbase\Bucket::mapAdd' => 'Add key to the map', +'Couchbase\Bucket::mapGet' => 'Get an item from a map', +'Couchbase\Bucket::mapRemove' => 'Removes key from the map', +'Couchbase\Bucket::mapSize' => 'Returns size of the map', +'Couchbase\Bucket::mutateIn' => 'Returns a builder for writing subdocument API.', +'Couchbase\Bucket::ping' => 'Try to reach specified services, and measure network latency.', +'Couchbase\Bucket::prepend' => 'Prepends content to a document. + +On the server side it just contatenate existing value to the passed one. +Note that this might make the value un-decodable. Consider sub-document API +for partial updates of the JSON documents.', +'Couchbase\Bucket::query' => 'Performs a query to Couchbase Server', +'Couchbase\Bucket::queueAdd' => 'Add an element to the beginning of the queue', +'Couchbase\Bucket::queueExists' => 'Checks if the queue contains specified value', +'Couchbase\Bucket::queueRemove' => 'Remove the element at the end of the queue and return it', +'Couchbase\Bucket::queueSize' => 'Returns size of the queue', +'Couchbase\Bucket::remove' => 'Removes the document.', +'Couchbase\Bucket::replace' => 'Replaces a document. This operation will fail if the document does not exists on the cluster.', +'Couchbase\Bucket::retrieveIn' => 'Retrieves specified paths in JSON document + +This is essentially a shortcut for `lookupIn($id)->get($paths)->execute()`.', +'Couchbase\Bucket::setAdd' => 'Add value to the set + +Note, that currently only primitive values could be stored in the set (strings, integers and booleans).', +'Couchbase\Bucket::setExists' => 'Check if the value exists in the set', +'Couchbase\Bucket::setRemove' => 'Remove value from the set', +'Couchbase\Bucket::setSize' => 'Returns size of the set', +'Couchbase\Bucket::setTranscoder' => 'Sets custom encoder and decoder functions for handling serialization.', +'Couchbase\Bucket::touch' => 'Updates document\'s expiration time.', +'Couchbase\Bucket::unlock' => 'Unlocks previously locked document', +'Couchbase\Bucket::upsert' => 'Inserts or updates a document, depending on whether the document already exists on the cluster.', +'Couchbase\BucketManager::createN1qlIndex' => 'Create secondary N1QL index.', +'Couchbase\BucketManager::createN1qlPrimaryIndex' => 'Create a primary N1QL index.', +'Couchbase\BucketManager::dropN1qlIndex' => 'Drop the given secondary index', +'Couchbase\BucketManager::dropN1qlPrimaryIndex' => 'Drop the given primary index', +'Couchbase\BucketManager::flush' => 'Flushes the bucket (clears all data)', +'Couchbase\BucketManager::getDesignDocument' => 'Get design document by its name', +'Couchbase\BucketManager::info' => 'Returns information about the bucket + +Returns an associative array of status information as seen by the cluster for +this bucket. The exact structure of the returned data can be seen in the Couchbase +Manual by looking at the bucket /info endpoint.', +'Couchbase\BucketManager::insertDesignDocument' => 'Inserts design document and fails if it is exist already.', +'Couchbase\BucketManager::listDesignDocuments' => 'Returns all design documents of the bucket.', +'Couchbase\BucketManager::listN1qlIndexes' => 'List all N1QL indexes that are registered for the current bucket.', +'Couchbase\BucketManager::removeDesignDocument' => 'Removes design document by its name', +'Couchbase\BucketManager::upsertDesignDocument' => 'Creates or replaces design document.', +'Couchbase\ClassicAuthenticator::bucket' => 'Registers bucket credentials in the container', +'Couchbase\ClassicAuthenticator::cluster' => 'Registers cluster management credentials in the container', +'Couchbase\Cluster::__construct' => 'Create cluster object', +'Couchbase\Cluster::authenticate' => 'Associate authenticator with Cluster', +'Couchbase\Cluster::authenticateAs' => 'Create \Couchbase\PasswordAuthenticator from given credentials and associate it with Cluster', +'Couchbase\Cluster::manager' => 'Open management connection to the Couchbase cluster.', +'Couchbase\Cluster::openBucket' => 'Open connection to the Couchbase bucket', +'Couchbase\ClusterManager::createBucket' => 'Creates new bucket', +'Couchbase\ClusterManager::getUser' => 'Fetch single user by its name', +'Couchbase\ClusterManager::info' => 'Provides information about the cluster. + +Returns an associative array of status information as seen on the cluster. The exact structure of the returned +data can be seen in the Couchbase Manual by looking at the cluster /info endpoint.', +'Couchbase\ClusterManager::listBuckets' => 'Lists all buckets on this cluster.', +'Couchbase\ClusterManager::listUsers' => 'Lists all users on this cluster.', +'Couchbase\ClusterManager::removeBucket' => 'Removes a bucket identified by its name.', +'Couchbase\ClusterManager::removeUser' => 'Removes a user identified by its name.', +'Couchbase\ClusterManager::upsertUser' => 'Creates new user', +'Couchbase\defaultDecoder' => 'Decodes value using \Couchbase\basicDecoderV1. + +It passes `couchbase.decoder.*` INI properties as $options.', +'Couchbase\defaultEncoder' => 'Encodes value using \Couchbase\basicDecoderV1. + +It passes `couchbase.encoder.*` INI properties as $options.', +'Couchbase\fastlzCompress' => 'Compress input using FastLZ algorithm.', +'Couchbase\fastlzDecompress' => 'Decompress input using FastLZ algorithm.', +'Couchbase\LookupInBuilder::execute' => 'Perform several lookup operations inside a single existing JSON document, using a specific timeout', +'Couchbase\LookupInBuilder::exists' => 'Check if a value exists inside the document. + +This doesn\'t transmit the value on the wire if it exists, saving the corresponding byte overhead.', +'Couchbase\LookupInBuilder::get' => 'Get a value inside the JSON document.', +'Couchbase\LookupInBuilder::getCount' => 'Get a count of values inside the JSON document. + +This method is only available with Couchbase Server 5.0 and later.', +'Couchbase\MutateInBuilder::arrayAddUnique' => 'Insert a value in an existing array only if the value +isn\'t already contained in the array (by way of string comparison).', +'Couchbase\MutateInBuilder::arrayAppend' => 'Append to an existing array, pushing the value to the back/last position in the array.', +'Couchbase\MutateInBuilder::arrayAppendAll' => 'Append multiple values at once in an existing array. + +Push all values in the collection\'s iteration order to the back/end of the array. +For example given an array [A, B, C], appending the values X and Y yields [A, B, C, X, Y] +and not [A, B, C, [X, Y]].', +'Couchbase\MutateInBuilder::arrayInsert' => 'Insert into an existing array at a specific position + +Position denoted in the path, eg. "sub.array[2]".', +'Couchbase\MutateInBuilder::arrayInsertAll' => 'Insert multiple values at once in an existing array at a specified position. + +Position denoted in the path, eg. "sub.array[2]"), inserting all values in the collection\'s iteration order +at the given position and shifting existing values beyond the position by the number of elements in the +collection. + +For example given an array [A, B, C], inserting the values X and Y at position 1 yields [A, B, X, Y, C] +and not [A, B, [X, Y], C].', +'Couchbase\MutateInBuilder::arrayPrepend' => 'Prepend to an existing array, pushing the value to the front/first position in the array.', +'Couchbase\MutateInBuilder::arrayPrependAll' => 'Prepend multiple values at once in an existing array. + +Push all values in the collection\'s iteration order to the front/start of the array. +For example given an array [A, B, C], prepending the values X and Y yields [X, Y, A, B, C] +and not [[X, Y], A, B, C].', +'Couchbase\MutateInBuilder::counter' => 'Increment/decrement a numerical fragment in a JSON document. + +If the value (last element of the path) doesn\'t exist the counter +is created and takes the value of the delta.', +'Couchbase\MutateInBuilder::execute' => 'Perform several mutation operations inside a single existing JSON document.', +'Couchbase\MutateInBuilder::insert' => 'Insert a fragment provided the last element of the path doesn\'t exists.', +'Couchbase\MutateInBuilder::modeDocument' => 'Select mode for new full-document operations. + +It defines behaviour of MutateInBuilder#upsert() method. The $mode +could take one of three modes: + * FULLDOC_REPLACE: complain when document does not exist + * FULLDOC_INSERT: complain when document does exist + * FULLDOC_UPSERT: unconditionally set value for the document', +'Couchbase\MutateInBuilder::remove' => 'Remove an entry in a JSON document. + +Scalar, array element, dictionary entry, whole array or dictionary, depending on the path.', +'Couchbase\MutateInBuilder::replace' => 'Replace an existing value by the given fragment', +'Couchbase\MutateInBuilder::upsert' => 'Insert a fragment, replacing the old value if the path exists. + +When only one argument supplied, the library will handle it as full-document +upsert, and treat this argument as value. See MutateInBuilder#modeDocument()', +'Couchbase\MutateInBuilder::withExpiry' => 'Change the expiry of the enclosing document as part of the mutation.', +'Couchbase\MutationState::add' => 'Update container with the given mutation token holders.', +'Couchbase\MutationState::from' => 'Create container from the given mutation token holders.', +'Couchbase\MutationToken::bucketName' => 'Returns bucket name', +'Couchbase\MutationToken::from' => 'Creates new mutation token', +'Couchbase\MutationToken::sequenceNumber' => 'Returns the sequence number inside partition', +'Couchbase\MutationToken::vbucketId' => 'Returns partition number', +'Couchbase\MutationToken::vbucketUuid' => 'Returns UUID of the partition', +'Couchbase\N1qlQuery::adhoc' => 'Allows to specify if this query is adhoc or not. + +If it is not adhoc (so performed often), the client will try to perform optimizations +transparently based on the server capabilities, like preparing the statement and +then executing a query plan instead of the raw query.', +'Couchbase\N1qlQuery::consistency' => 'Specifies the consistency level for this query', +'Couchbase\N1qlQuery::consistentWith' => 'Sets mutation state the query should be consistent with', +'Couchbase\N1qlQuery::crossBucket' => 'Allows to pull credentials from the Authenticator', +'Couchbase\N1qlQuery::fromString' => 'Creates new N1qlQuery instance directly from the N1QL string.', +'Couchbase\N1qlQuery::maxParallelism' => 'Allows to override the default maximum parallelism for the query execution on the server side.', +'Couchbase\N1qlQuery::namedParams' => 'Specify associative array of named parameters + +The supplied array of key/value pairs will be merged with already existing named parameters. +Note: carefully choose type of quotes for the query string, because PHP also uses `$` +(dollar sign) for variable interpolation. If you are using double quotes, make sure +that N1QL parameters properly escaped.', +'Couchbase\N1qlQuery::pipelineBatch' => 'Advanced: Controls the number of items execution operators can batch for Fetch from the KV.', +'Couchbase\N1qlQuery::pipelineCap' => 'Advanced: Maximum number of items each execution operator can buffer between various operators.', +'Couchbase\N1qlQuery::positionalParams' => 'Specify array of positional parameters + +Previously specified positional parameters will be replaced. +Note: carefully choose type of quotes for the query string, because PHP also uses `$` +(dollar sign) for variable interpolation. If you are using double quotes, make sure +that N1QL parameters properly escaped.', +'Couchbase\N1qlQuery::profile' => 'Controls the profiling mode used during query execution', +'Couchbase\N1qlQuery::readonly' => 'If set to true, it will signal the query engine on the server that only non-data modifying requests +are allowed. Note that this rule is enforced on the server and not the SDK side. + +Controls whether a query can change a resulting record set. + +If readonly is true, then the following statements are not allowed: + - CREATE INDEX + - DROP INDEX + - INSERT + - MERGE + - UPDATE + - UPSERT + - DELETE', +'Couchbase\N1qlQuery::scanCap' => 'Advanced: Maximum buffered channel size between the indexer client and the query service for index scans. + +This parameter controls when to use scan backfill. Use 0 or a negative number to disable.', +'Couchbase\passthruDecoder' => 'Returns value as it received from the server without any transformations. + +It is useful for debug purpose to inspect bare value.', +'Couchbase\passthruEncoder' => 'Returns the value, which has been passed and zero as flags and datatype. + +It is useful for debug purposes, or when the value known to be a string, otherwise behavior is not defined (most +likely it will generate error).', +'Couchbase\PasswordAuthenticator::password' => 'Sets password', +'Couchbase\PasswordAuthenticator::username' => 'Sets username', +'Couchbase\SearchQuery::__construct' => 'Prepare an FTS SearchQuery on an index. + +Top level query parameters can be set after that by using the fluent API.', +'Couchbase\SearchQuery::addFacet' => 'Adds one SearchFacet to the query + +This is an additive operation (the given facets are added to any facet previously requested), +but if an existing facet has the same name it will be replaced. + +Note that to be faceted, a field\'s value must be stored in the FTS index.', +'Couchbase\SearchQuery::boolean' => 'Prepare boolean search query', +'Couchbase\SearchQuery::booleanField' => 'Prepare boolean field search query', +'Couchbase\SearchQuery::conjuncts' => 'Prepare compound conjunction search query', +'Couchbase\SearchQuery::consistentWith' => 'Sets the consistency to consider for this FTS query to AT_PLUS and +uses the MutationState to parameterize the consistency. + +This replaces any consistency tuning previously set.', +'Couchbase\SearchQuery::dateRange' => 'Prepare date range search query', +'Couchbase\SearchQuery::dateRangeFacet' => 'Prepare date range search facet', +'Couchbase\SearchQuery::disjuncts' => 'Prepare compound disjunction search query', +'Couchbase\SearchQuery::docId' => 'Prepare document ID search query', +'Couchbase\SearchQuery::explain' => 'Activates the explanation of each result hit in the response', +'Couchbase\SearchQuery::fields' => 'Configures the list of fields for which the whole value should be included in the response. + +If empty, no field values are included. This drives the inclusion of the fields in each hit. +Note that to be highlighted, the fields must be stored in the FTS index.', +'Couchbase\SearchQuery::geoBoundingBox' => 'Prepare geo bounding box search query', +'Couchbase\SearchQuery::geoDistance' => 'Prepare geo distance search query', +'Couchbase\SearchQuery::highlight' => 'Configures the highlighting of matches in the response', +'Couchbase\SearchQuery::limit' => 'Add a limit to the query on the number of hits it can return', +'Couchbase\SearchQuery::match' => 'Prepare match search query', +'Couchbase\SearchQuery::matchAll' => 'Prepare match all search query', +'Couchbase\SearchQuery::matchNone' => 'Prepare match non search query', +'Couchbase\SearchQuery::matchPhrase' => 'Prepare phrase search query', +'Couchbase\SearchQuery::numericRange' => 'Prepare numeric range search query', +'Couchbase\SearchQuery::numericRangeFacet' => 'Prepare numeric range search facet', +'Couchbase\SearchQuery::prefix' => 'Prepare prefix search query', +'Couchbase\SearchQuery::queryString' => 'Prepare query string search query', +'Couchbase\SearchQuery::regexp' => 'Prepare regexp search query', +'Couchbase\SearchQuery::serverSideTimeout' => 'Sets the server side timeout in milliseconds', +'Couchbase\SearchQuery::skip' => 'Set the number of hits to skip (eg. for pagination).', +'Couchbase\SearchQuery::sort' => 'Configures the list of fields (including special fields) which are used for sorting purposes. +If empty, the default sorting (descending by score) is used by the server. + +The list of sort fields can include actual fields (like "firstname" but then they must be stored in the +index, configured in the server side mapping). Fields provided first are considered first and in a "tie" case +the next sort field is considered. So sorting by "firstname" and then "lastname" will first sort ascending by +the firstname and if the names are equal then sort ascending by lastname. Special fields like "_id" and +"_score" can also be used. If prefixed with "-" the sort order is set to descending. + +If no sort is provided, it is equal to sort("-_score"), since the server will sort it by score in descending +order.', +'Couchbase\SearchQuery::term' => 'Prepare term search query', +'Couchbase\SearchQuery::termFacet' => 'Prepare term search facet', +'Couchbase\SearchQuery::termRange' => 'Prepare term range search query', +'Couchbase\SearchQuery::wildcard' => 'Prepare wildcard search query', +'Couchbase\SearchSort::field' => 'Sort by a field in the hits.', +'Couchbase\SearchSort::geoDistance' => 'Sort by geo location.', +'Couchbase\SearchSort::id' => 'Sort by the document identifier.', +'Couchbase\SearchSort::score' => 'Sort by the hit score.', +'Couchbase\SearchSortField::descending' => 'Direction of the sort', +'Couchbase\SearchSortField::field' => 'Sort by a field in the hits.', +'Couchbase\SearchSortField::geoDistance' => 'Sort by geo location.', +'Couchbase\SearchSortField::id' => 'Sort by the document identifier.', +'Couchbase\SearchSortField::jsonSerialize' => 'Specify data which should be serialized to JSON', +'Couchbase\SearchSortField::missing' => 'Set where the hits with missing field will be inserted', +'Couchbase\SearchSortField::mode' => 'Set mode of the sort', +'Couchbase\SearchSortField::score' => 'Sort by the hit score.', +'Couchbase\SearchSortField::type' => 'Set type of the field', +'Couchbase\SearchSortGeoDistance::descending' => 'Direction of the sort', +'Couchbase\SearchSortGeoDistance::field' => 'Sort by a field in the hits.', +'Couchbase\SearchSortGeoDistance::geoDistance' => 'Sort by geo location.', +'Couchbase\SearchSortGeoDistance::id' => 'Sort by the document identifier.', +'Couchbase\SearchSortGeoDistance::jsonSerialize' => 'Specify data which should be serialized to JSON', +'Couchbase\SearchSortGeoDistance::score' => 'Sort by the hit score.', +'Couchbase\SearchSortGeoDistance::unit' => 'Name of the units', +'Couchbase\SearchSortId::descending' => 'Direction of the sort', +'Couchbase\SearchSortId::field' => 'Sort by a field in the hits.', +'Couchbase\SearchSortId::geoDistance' => 'Sort by geo location.', +'Couchbase\SearchSortId::id' => 'Sort by the document identifier.', +'Couchbase\SearchSortId::jsonSerialize' => 'Specify data which should be serialized to JSON', +'Couchbase\SearchSortId::score' => 'Sort by the hit score.', +'Couchbase\SearchSortScore::descending' => 'Direction of the sort', +'Couchbase\SearchSortScore::field' => 'Sort by a field in the hits.', +'Couchbase\SearchSortScore::geoDistance' => 'Sort by geo location.', +'Couchbase\SearchSortScore::id' => 'Sort by the document identifier.', +'Couchbase\SearchSortScore::jsonSerialize' => 'Specify data which should be serialized to JSON', +'Couchbase\SearchSortScore::score' => 'Sort by the hit score.', +'Couchbase\SpatialViewQuery::bbox' => 'Specifies the bounding box to search within. + +Note, using bbox() is discouraged, startRange/endRange is more flexible and should be preferred.', +'Couchbase\SpatialViewQuery::consistency' => 'Specifies the mode of updating to perorm before and after executing the query', +'Couchbase\SpatialViewQuery::custom' => 'Specifies custom options to pass to the server. + +Note that these options are expected to be already encoded.', +'Couchbase\SpatialViewQuery::encode' => 'Returns associative array, representing the View query.', +'Couchbase\SpatialViewQuery::endRange' => 'Specify end range for query', +'Couchbase\SpatialViewQuery::limit' => 'Limits the result set to a specified number rows.', +'Couchbase\SpatialViewQuery::order' => 'Orders the results by key as specified', +'Couchbase\SpatialViewQuery::skip' => 'Skips a number o records rom the beginning of the result set', +'Couchbase\SpatialViewQuery::startRange' => 'Specify start range for query', +'Couchbase\UserSettings::fullName' => 'Sets full name of the user (optional).', +'Couchbase\UserSettings::password' => 'Sets password of the user.', +'Couchbase\UserSettings::role' => 'Adds role to the list of the accessible roles of the user.', +'Couchbase\ViewQuery::consistency' => 'Specifies the mode of updating to perorm before and after executing the query', +'Couchbase\ViewQuery::custom' => 'Specifies custom options to pass to the server. + +Note that these options are expected to be already encoded.', +'Couchbase\ViewQuery::encode' => 'Returns associative array, representing the View query.', +'Couchbase\ViewQuery::from' => 'Creates a new Couchbase ViewQuery instance for performing a view query.', +'Couchbase\ViewQuery::fromSpatial' => 'Creates a new Couchbase ViewQuery instance for performing a spatial query.', +'Couchbase\ViewQuery::group' => 'Group the results using the reduce function to a group or single row. + +Important: this setter and groupLevel should not be used together in the +same ViewQuery. It is sufficient to only set the grouping level only and +use this setter in cases where you always want the highest group level +implicitly.', +'Couchbase\ViewQuery::groupLevel' => 'Specify the group level to be used. + +Important: group() and this setter should not be used together in the +same ViewQuery. It is sufficient to only use this setter and use group() +in cases where you always want the highest group level implicitly.', +'Couchbase\ViewQuery::idRange' => 'Specifies start and end document IDs in addition to range limits. + +This might be needed for more precise pagination with a lot of documents +with the same key selected into the same page.', +'Couchbase\ViewQuery::key' => 'Restict results of the query to the specified key', +'Couchbase\ViewQuery::keys' => 'Restict results of the query to the specified set of keys', +'Couchbase\ViewQuery::limit' => 'Limits the result set to a specified number rows.', +'Couchbase\ViewQuery::order' => 'Orders the results by key as specified', +'Couchbase\ViewQuery::range' => 'Specifies a range of the keys to return from the index.', +'Couchbase\ViewQuery::reduce' => 'Specifies whether the reduction function should be applied to results of the query.', +'Couchbase\ViewQuery::skip' => 'Skips a number o records rom the beginning of the result set', +'Couchbase\ViewQueryEncodable::encode' => 'Returns associative array, representing the View query.', +'Couchbase\zlibCompress' => 'Compress input using zlib. Raises Exception when extension compiled without zlib support.', +'Couchbase\zlibDecompress' => 'Compress input using zlib. Raises Exception when extension compiled without zlib support.', +'count' => 'Count all elements in an array, or something in an object', +'count_chars' => 'Return information about characters used in a string', +'countable::count' => 'Count elements of an object', +'crack_check' => 'Performs an obscure check with the given password', +'crack_closedict' => 'Closes an open CrackLib dictionary', +'crack_getlastmessage' => 'Returns the message from the last obscure check', +'crack_opendict' => 'Opens a new CrackLib dictionary', +'crc32' => 'Calculates the crc32 polynomial of a string', +'create_function' => 'Create an anonymous (lambda-style) function', +'crypt' => 'One-way string hashing', +'Crypto\Base64::__construct' => 'Base64 constructor', +'Crypto\Base64::decode' => 'Decodes base64 string $data to raw encoding', +'Crypto\Base64::decodeFinish' => 'Decodes characters that left in the encoding context', +'Crypto\Base64::decodeUpdate' => 'Decodes block of characters from $data and saves the reminder of the last block +to the encoding context', +'Crypto\Base64::encode' => 'Encodes string $data to base64 encoding', +'Crypto\Base64::encodeFinish' => 'Encodes characters that left in the encoding context', +'Crypto\Base64::encodeUpdate' => 'Encodes block of characters from $data and saves the reminder of the last block +to the encoding context', +'Crypto\Cipher::__callStatic' => 'Cipher magic method for calling static methods', +'Crypto\Cipher::__construct' => 'Cipher constructor', +'Crypto\Cipher::decrypt' => 'Decrypts ciphertext to decrypted text', +'Crypto\Cipher::decryptFinish' => 'Finalizes cipher decryption', +'Crypto\Cipher::decryptInit' => 'Initializes cipher decryption', +'Crypto\Cipher::decryptUpdate' => 'Updates cipher decryption', +'Crypto\Cipher::encrypt' => 'Encrypts text to ciphertext', +'Crypto\Cipher::encryptFinish' => 'Finalizes cipher encryption', +'Crypto\Cipher::encryptInit' => 'Initializes cipher encryption', +'Crypto\Cipher::encryptUpdate' => 'Updates cipher encryption', +'Crypto\Cipher::getAlgorithmName' => 'Returns cipher algorithm string', +'Crypto\Cipher::getAlgorithms' => 'Returns cipher algorithms', +'Crypto\Cipher::getBlockSize' => 'Returns cipher block size', +'Crypto\Cipher::getIVLength' => 'Returns cipher IV length', +'Crypto\Cipher::getKeyLength' => 'Returns cipher key length', +'Crypto\Cipher::getMode' => 'Returns cipher mode', +'Crypto\Cipher::getTag' => 'Returns authentication tag', +'Crypto\Cipher::hasAlgorithm' => 'Finds out whether algorithm exists', +'Crypto\Cipher::hasMode' => 'Finds out whether the cipher mode is defined in the used OpenSSL library', +'Crypto\Cipher::setAAD' => 'Sets additional application data for authenticated encryption', +'Crypto\Cipher::setTag' => 'Sets authentication tag', +'Crypto\Cipher::setTagLength' => 'Set authentication tag length', +'Crypto\Hash::__callStatic' => 'Hash magic method for calling static methods', +'Crypto\Hash::__construct' => 'Hash constructor', +'Crypto\Hash::digest' => 'Return hash digest in raw foramt', +'Crypto\Hash::getAlgorithmName' => 'Returns hash algorithm string', +'Crypto\Hash::getAlgorithms' => 'Returns hash algorithms', +'Crypto\Hash::getBlockSize' => 'Returns hash block size', +'Crypto\Hash::getSize' => 'Returns hash size', +'Crypto\Hash::hasAlgorithm' => 'Finds out whether algorithm exists', +'Crypto\Hash::hexdigest' => 'Return hash digest in hex format', +'Crypto\Hash::update' => 'Updates hash', +'Crypto\KDF::__construct' => 'KDF constructor', +'Crypto\KDF::getLength' => 'Get key length', +'Crypto\KDF::getSalt' => 'Get salt', +'Crypto\KDF::setLength' => 'Set key length', +'Crypto\KDF::setSalt' => 'Set salt', +'Crypto\MAC::__callStatic' => 'Hash magic method for calling static methods', +'Crypto\MAC::__construct' => 'Create a MAC (used by MAC subclasses - HMAC and CMAC)', +'Crypto\MAC::digest' => 'Return hash digest in raw foramt', +'Crypto\MAC::getAlgorithmName' => 'Returns hash algorithm string', +'Crypto\MAC::getAlgorithms' => 'Returns hash algorithms', +'Crypto\MAC::getBlockSize' => 'Returns hash block size', +'Crypto\MAC::getSize' => 'Returns hash size', +'Crypto\MAC::hasAlgorithm' => 'Finds out whether algorithm exists', +'Crypto\MAC::hexdigest' => 'Return hash digest in hex format', +'Crypto\MAC::update' => 'Updates hash', +'Crypto\PBKDF2::__construct' => 'KDF constructor', +'Crypto\PBKDF2::derive' => 'Deriver hash for password', +'Crypto\PBKDF2::getHashAlgorithm' => 'Get hash algorithm', +'Crypto\PBKDF2::getIterations' => 'Get iterations', +'Crypto\PBKDF2::getLength' => 'Get key length', +'Crypto\PBKDF2::getSalt' => 'Get salt', +'Crypto\PBKDF2::setHashAlgorithm' => 'Set hash algorithm', +'Crypto\PBKDF2::setIterations' => 'Set iterations', +'Crypto\PBKDF2::setLength' => 'Set key length', +'Crypto\PBKDF2::setSalt' => 'Set salt', +'Crypto\Rand::cleanup' => 'Cleans up PRNG state', +'Crypto\Rand::generate' => 'Generates pseudo random bytes', +'Crypto\Rand::loadFile' => 'Reads a number of bytes from file $filename and adds them to the PRNG. If +max_bytes is non-negative, up to to max_bytes are read; if $max_bytes is +negative, the complete file is read', +'Crypto\Rand::seed' => 'Mixes bytes in $buf into PRNG state', +'Crypto\Rand::writeFile' => 'Writes a number of random bytes (currently 1024) to file $filename which can be +used to initializethe PRNG by calling Crypto\Rand::loadFile() in a later session', +'ctype_alnum' => 'Check for alphanumeric character(s)', +'ctype_alpha' => 'Check for alphabetic character(s)', +'ctype_cntrl' => 'Check for control character(s)', +'ctype_digit' => 'Check for numeric character(s)', +'ctype_graph' => 'Check for any printable character(s) except space', +'ctype_lower' => 'Check for lowercase character(s)', +'ctype_print' => 'Check for printable character(s)', +'ctype_punct' => 'Check for any printable character which is not whitespace or an alphanumeric character', +'ctype_space' => 'Check for whitespace character(s)', +'ctype_upper' => 'Check for uppercase character(s)', +'ctype_xdigit' => 'Check for character(s) representing a hexadecimal digit', +'cubrid_bind' => 'Bind variables to a prepared statement as parameters', +'cubrid_close_prepare' => 'Close the request handle', +'cubrid_close_request' => 'Close the request handle', +'cubrid_col_get' => 'Get contents of collection type column using OID', +'cubrid_col_size' => 'Get the number of elements in collection type column using OID', +'cubrid_column_names' => 'Get the column names in result', +'cubrid_column_types' => 'Get column types in result', +'cubrid_commit' => 'Commit a transaction', +'cubrid_connect' => 'Open a connection to a CUBRID Server', +'cubrid_connect_with_url' => 'Establish the environment for connecting to CUBRID server', +'cubrid_current_oid' => 'Get OID of the current cursor location', +'cubrid_disconnect' => 'Close a database connection', +'cubrid_drop' => 'Delete an instance using OID', +'cubrid_error_code' => 'Get error code for the most recent function call', +'cubrid_error_code_facility' => 'Get the facility code of error', +'cubrid_error_msg' => 'Get last error message for the most recent function call', +'cubrid_execute' => 'Execute a prepared SQL statement', +'cubrid_fetch' => 'Fetch the next row from a result set', +'cubrid_free_result' => 'Free the memory occupied by the result data', +'cubrid_get' => 'Get a column using OID', +'cubrid_get_autocommit' => 'Get auto-commit mode of the connection', +'cubrid_get_charset' => 'Return the current CUBRID connection charset', +'cubrid_get_class_name' => 'Get the class name using OID', +'cubrid_get_client_info' => 'Return the client library version', +'cubrid_get_db_parameter' => 'Returns the CUBRID database parameters', +'cubrid_get_query_timeout' => 'Get the query timeout value of the request', +'cubrid_get_server_info' => 'Return the CUBRID server version', +'cubrid_insert_id' => 'Return the ID generated for the last updated AUTO_INCREMENT column', +'cubrid_is_instance' => 'Check whether the instance pointed by OID exists', +'cubrid_lob2_bind' => 'Bind a lob object or a string as a lob object to a prepared statement as parameters', +'cubrid_lob2_close' => 'Close LOB object', +'cubrid_lob2_export' => 'Export the lob object to a file', +'cubrid_lob2_import' => 'Import BLOB/CLOB data from a file', +'cubrid_lob2_new' => 'Create a lob object', +'cubrid_lob2_read' => 'Read from BLOB/CLOB data', +'cubrid_lob2_seek' => 'Move the cursor of a lob object', +'cubrid_lob2_seek64' => 'Move the cursor of a lob object', +'cubrid_lob2_size' => 'Get a lob object\'s size', +'cubrid_lob2_size64' => 'Get a lob object\'s size', +'cubrid_lob2_tell' => 'Tell the cursor position of the LOB object', +'cubrid_lob2_tell64' => 'Tell the cursor position of the LOB object', +'cubrid_lob2_write' => 'Write to a lob object', +'cubrid_lob_close' => 'Close BLOB/CLOB data', +'cubrid_lob_export' => 'Export BLOB/CLOB data to file', +'cubrid_lob_get' => 'Get BLOB/CLOB data', +'cubrid_lob_send' => 'Read BLOB/CLOB data and send straight to browser', +'cubrid_lob_size' => 'Get BLOB/CLOB data size', +'cubrid_lock_read' => 'Set a read lock on the given OID', +'cubrid_lock_write' => 'Set a write lock on the given OID', +'cubrid_move_cursor' => 'Move the cursor in the result', +'cubrid_next_result' => 'Get result of next query when executing multiple SQL statements', +'cubrid_num_cols' => 'Return the number of columns in the result set', +'cubrid_num_rows' => 'Get the number of rows in the result set', +'cubrid_pconnect' => 'Open a persistent connection to a CUBRID server', +'cubrid_pconnect_with_url' => 'Open a persistent connection to CUBRID server', +'cubrid_prepare' => 'Prepare a SQL statement for execution', +'cubrid_put' => 'Update a column using OID', +'cubrid_rollback' => 'Roll back a transaction', +'cubrid_schema' => 'Get the requested schema information', +'cubrid_seq_drop' => 'Delete an element from sequence type column using OID', +'cubrid_seq_insert' => 'Insert an element to a sequence type column using OID', +'cubrid_seq_put' => 'Update the element value of sequence type column using OID', +'cubrid_set_add' => 'Insert a single element to set type column using OID', +'cubrid_set_autocommit' => 'Set autocommit mode of the connection', +'cubrid_set_db_parameter' => 'Sets the CUBRID database parameters', +'cubrid_set_drop' => 'Delete an element from set type column using OID', +'cubrid_set_query_timeout' => 'Set the timeout time of query execution', +'cubrid_version' => 'Get the CUBRID PHP module\'s version', +'curl_close' => 'Close a cURL session', +'curl_copy_handle' => 'Copy a cURL handle along with all of its preferences', +'curl_errno' => 'Return the last error number', +'curl_error' => 'Return a string containing the last error for the current session', +'curl_escape' => 'URL encodes the given string', +'curl_exec' => 'Perform a cURL session', +'curl_file_create' => 'Create a CURLFile object', +'curl_getinfo' => 'Get information regarding a specific transfer', +'curl_init' => 'Initialize a cURL session', +'curl_multi_add_handle' => 'Add a normal cURL handle to a cURL multi handle', +'curl_multi_close' => 'Close a set of cURL handles', +'curl_multi_errno' => 'Return the last multi curl error number', +'curl_multi_exec' => 'Run the sub-connections of the current cURL handle', +'curl_multi_getcontent' => 'Return the content of a cURL handle if CURLOPT_RETURNTRANSFER is set', +'curl_multi_info_read' => 'Get information about the current transfers', +'curl_multi_init' => 'Returns a new cURL multi handle', +'curl_multi_remove_handle' => 'Remove a multi handle from a set of cURL handles', +'curl_multi_select' => 'Wait for activity on any curl_multi connection', +'curl_multi_setopt' => 'Set an option for the cURL multi handle', +'curl_multi_strerror' => 'Return string describing error code', +'curl_pause' => 'Pause and unpause a connection', +'curl_reset' => 'Reset all options of a libcurl session handle', +'curl_setopt' => 'Set an option for a cURL transfer', +'curl_setopt_array' => 'Set multiple options for a cURL transfer', +'curl_share_close' => 'Close a cURL share handle', +'curl_share_errno' => 'Return the last share curl error number', +'curl_share_init' => 'Initialize a cURL share handle', +'curl_share_setopt' => 'Set an option for a cURL share handle', +'curl_share_strerror' => 'Return string describing the given error code', +'curl_strerror' => 'Return string describing the given error code', +'curl_unescape' => 'Decodes the given URL encoded string', +'curl_version' => 'Gets cURL version information', +'curlfile::__construct' => 'Create a CURLFile object', +'curlfile::__wakeup' => 'Unserialization handler', +'curlfile::getFilename' => 'Get file name', +'curlfile::getMimeType' => 'Get MIME type', +'curlfile::getPostFilename' => 'Get file name for POST', +'curlfile::setMimeType' => 'Set MIME type', +'curlfile::setPostFilename' => 'Set file name for POST', +'current' => 'Return the current element in an array', +'cyrus_authenticate' => 'Authenticate against a Cyrus IMAP server', +'cyrus_bind' => 'Bind callbacks to a Cyrus IMAP connection', +'cyrus_close' => 'Close connection to a Cyrus IMAP server', +'cyrus_connect' => 'Connect to a Cyrus IMAP server', +'cyrus_query' => 'Send a query to a Cyrus IMAP server', +'cyrus_unbind' => 'Unbind ...', +'date' => 'Format a local time/date', +'date_add' => 'Alias of DateTime::add', +'date_create' => 'Alias of DateTime::__construct', +'date_create_from_format' => 'Alias of DateTime::createFromFormat', +'date_create_immutable' => 'Alias of DateTimeImmutable::__construct', +'date_create_immutable_from_format' => 'Alias of DateTimeImmutable::createFromFormat', +'date_date_set' => 'Alias of DateTime::setDate', +'date_default_timezone_get' => 'Gets the default timezone used by all date/time functions in a script', +'date_default_timezone_set' => 'Sets the default timezone used by all date/time functions in a script', +'date_diff' => 'Alias of DateTime::diff', +'date_format' => 'Alias of DateTime::format', +'date_get_last_errors' => 'Alias of DateTime::getLastErrors', +'date_interval_create_from_date_string' => 'Alias of DateInterval::createFromDateString', +'date_interval_format' => 'Alias of DateInterval::format', +'date_isodate_set' => 'Alias of DateTime::setISODate', +'date_modify' => 'Alias of DateTime::modify', +'date_offset_get' => 'Alias of DateTime::getOffset', +'date_parse' => 'Returns associative array with detailed info about given date', +'date_parse_from_format' => 'Get info about given date formatted according to the specified format', +'date_sub' => 'Alias of DateTime::sub', +'date_sun_info' => 'Returns an array with information about sunset/sunrise and twilight begin/end', +'date_sunrise' => 'Returns time of sunrise for a given day and location', +'date_sunset' => 'Returns time of sunset for a given day and location', +'date_time_set' => 'Alias of DateTime::setTime', +'date_timestamp_get' => 'Alias of DateTime::getTimestamp', +'date_timestamp_set' => 'Alias of DateTime::setTimestamp', +'date_timezone_get' => 'Alias of DateTime::getTimezone', +'date_timezone_set' => 'Alias of DateTime::setTimezone', +'dateinterval::__construct' => 'Creates a new DateInterval object', +'dateinterval::createFromDateString' => 'Sets up a DateInterval from the relative parts of the string', +'dateinterval::format' => 'Formats the interval', +'dateperiod::__construct' => 'Creates a new DatePeriod object', +'dateperiod::getDateInterval' => 'Gets the interval', +'dateperiod::getEndDate' => 'Gets the end date', +'dateperiod::getRecurrences' => 'Gets the number of recurrences', +'dateperiod::getStartDate' => 'Gets the start date', +'datetime::__construct' => 'Returns new DateTime object', +'datetime::__set_state' => 'The __set_state handler', +'datetime::add' => 'Adds an amount of days, months, years, hours, minutes and seconds to a DateTime object', +'datetime::createFromFormat' => 'Parses a time string according to a specified format', +'datetime::createFromImmutable' => 'Returns new DateTime object encapsulating the given DateTimeImmutable object', +'DateTime::diff' => 'Returns the difference between two DateTime objects represented as a DateInterval.', +'DateTime::format' => 'Returns date formatted according to given format.', +'datetime::getLastErrors' => 'Returns the warnings and errors', +'DateTime::getOffset' => 'Returns the timezone offset', +'DateTime::getTimestamp' => 'Gets the Unix timestamp.', +'DateTime::getTimezone' => 'Get the TimeZone associated with the DateTime', +'datetime::modify' => 'Alters the timestamp', +'datetime::setDate' => 'Sets the date', +'datetime::setISODate' => 'Sets the ISO date', +'datetime::setTime' => 'Sets the time', +'datetime::setTimestamp' => 'Sets the date and time based on an Unix timestamp', +'datetime::setTimezone' => 'Sets the time zone for the DateTime object', +'datetime::sub' => 'Subtracts an amount of days, months, years, hours, minutes and seconds from a DateTime object', +'datetimeimmutable::__construct' => 'Returns new DateTimeImmutable object', +'datetimeimmutable::__set_state' => 'The __set_state handler', +'DateTimeImmutable::__wakeup' => 'The __wakeup handler', +'datetimeimmutable::add' => 'Adds an amount of days, months, years, hours, minutes and seconds', +'datetimeimmutable::createFromFormat' => 'Parses a time string according to a specified format', +'datetimeimmutable::createFromMutable' => 'Returns new DateTimeImmutable object encapsulating the given DateTime object', +'DateTimeImmutable::diff' => 'Returns the difference between two DateTime objects', +'DateTimeImmutable::format' => 'Returns date formatted according to given format', +'datetimeimmutable::getLastErrors' => 'Returns the warnings and errors', +'DateTimeImmutable::getOffset' => 'Returns the timezone offset', +'DateTimeImmutable::getTimestamp' => 'Gets the Unix timestamp', +'DateTimeImmutable::getTimezone' => 'Return time zone relative to given DateTime', +'datetimeimmutable::modify' => 'Creates a new object with modified timestamp', +'datetimeimmutable::setDate' => 'Sets the date', +'datetimeimmutable::setISODate' => 'Sets the ISO date', +'datetimeimmutable::setTime' => 'Sets the time', +'datetimeimmutable::setTimestamp' => 'Sets the date and time based on a Unix timestamp', +'datetimeimmutable::setTimezone' => 'Sets the time zone', +'datetimeimmutable::sub' => 'Subtracts an amount of days, months, years, hours, minutes and seconds', +'DateTimeInterface::__wakeup' => 'The __wakeup handler', +'DateTimeInterface::diff' => 'Returns the difference between two DateTime objects', +'DateTimeInterface::format' => 'Returns date formatted according to given format', +'DateTimeInterface::getOffset' => 'Returns the timezone offset', +'DateTimeInterface::getTimestamp' => 'Gets the Unix timestamp', +'DateTimeInterface::getTimezone' => 'Return time zone relative to given DateTime', +'datetimezone::__construct' => 'Creates new DateTimeZone object', +'datetimezone::getLocation' => 'Returns location information for a timezone', +'datetimezone::getName' => 'Returns the name of the timezone', +'datetimezone::getOffset' => 'Returns the timezone offset from GMT', +'datetimezone::getTransitions' => 'Returns all transitions for the timezone', +'datetimezone::listAbbreviations' => 'Returns associative array containing dst, offset and the timezone name', +'datetimezone::listIdentifiers' => 'Returns a numerically indexed array containing all defined timezone identifiers', +'db2_autocommit' => 'Returns or sets the AUTOCOMMIT state for a database connection', +'db2_bind_param' => 'Binds a PHP variable to an SQL statement parameter', +'db2_client_info' => 'Returns an object with properties that describe the DB2 database client', +'db2_close' => 'Closes a database connection', +'db2_column_privileges' => 'Returns a result set listing the columns and associated privileges for a table', +'db2_columns' => 'Returns a result set listing the columns and associated metadata for a table', +'db2_commit' => 'Commits a transaction', +'db2_conn_error' => 'Returns a string containing the SQLSTATE returned by the last connection attempt', +'db2_conn_errormsg' => 'Returns the last connection error message and SQLCODE value', +'db2_connect' => 'Returns a connection to a database', +'db2_cursor_type' => 'Returns the cursor type used by a statement resource', +'db2_escape_string' => 'Used to escape certain characters', +'db2_exec' => 'Executes an SQL statement directly', +'db2_execute' => 'Executes a prepared SQL statement', +'db2_fetch_array' => 'Returns an array, indexed by column position, representing a row in a result set', +'db2_fetch_assoc' => 'Returns an array, indexed by column name, representing a row in a result set', +'db2_fetch_both' => 'Returns an array, indexed by both column name and position, representing a row in a result set', +'db2_fetch_object' => 'Returns an object with properties representing columns in the fetched row', +'db2_fetch_row' => 'Sets the result set pointer to the next row or requested row', +'db2_field_display_size' => 'Returns the maximum number of bytes required to display a column', +'db2_field_name' => 'Returns the name of the column in the result set', +'db2_field_num' => 'Returns the position of the named column in a result set', +'db2_field_precision' => 'Returns the precision of the indicated column in a result set', +'db2_field_scale' => 'Returns the scale of the indicated column in a result set', +'db2_field_type' => 'Returns the data type of the indicated column in a result set', +'db2_field_width' => 'Returns the width of the current value of the indicated column in a result set', +'db2_foreign_keys' => 'Returns a result set listing the foreign keys for a table', +'db2_free_result' => 'Frees resources associated with a result set', +'db2_free_stmt' => 'Frees resources associated with the indicated statement resource', +'db2_get_option' => 'Retrieves an option value for a statement resource or a connection resource', +'db2_last_insert_id' => 'Returns the auto generated ID of the last insert query that successfully executed on this connection', +'db2_lob_read' => 'Gets a user defined size of LOB files with each invocation', +'db2_next_result' => 'Requests the next result set from a stored procedure', +'db2_num_fields' => 'Returns the number of fields contained in a result set', +'db2_num_rows' => 'Returns the number of rows affected by an SQL statement', +'db2_pclose' => 'Closes a persistent database connection', +'db2_pconnect' => 'Returns a persistent connection to a database', +'db2_prepare' => 'Prepares an SQL statement to be executed', +'db2_primary_keys' => 'Returns a result set listing primary keys for a table', +'db2_procedure_columns' => 'Returns a result set listing stored procedure parameters', +'db2_procedures' => 'Returns a result set listing the stored procedures registered in a database', +'db2_result' => 'Returns a single column from a row in the result set', +'db2_rollback' => 'Rolls back a transaction', +'db2_server_info' => 'Returns an object with properties that describe the DB2 database server', +'db2_set_option' => 'Set options for connection or statement resources', +'db2_special_columns' => 'Returns a result set listing the unique row identifier columns for a table', +'db2_statistics' => 'Returns a result set listing the index and statistics for a table', +'db2_stmt_error' => 'Returns a string containing the SQLSTATE returned by an SQL statement', +'db2_stmt_errormsg' => 'Returns a string containing the last SQL statement error message', +'db2_table_privileges' => 'Returns a result set listing the tables and associated privileges in a database', +'db2_tables' => 'Returns a result set listing the tables and associated metadata in a database', +'dba_close' => 'Close a DBA database', +'dba_delete' => 'Delete DBA entry specified by key', +'dba_exists' => 'Check whether key exists', +'dba_fetch' => 'Fetch data specified by key', +'dba_firstkey' => 'Fetch first key', +'dba_handlers' => 'List all the handlers available', +'dba_insert' => 'Insert entry', +'dba_key_split' => 'Splits a key in string representation into array representation', +'dba_list' => 'List all open database files', +'dba_nextkey' => 'Fetch next key', +'dba_open' => 'Open database', +'dba_optimize' => 'Optimize database', +'dba_popen' => 'Open database persistently', +'dba_replace' => 'Replace or insert entry', +'dba_sync' => 'Synchronize database', +'dbase_add_record' => 'Adds a record to a database', +'dbase_close' => 'Closes a database', +'dbase_create' => 'Creates a database', +'dbase_delete_record' => 'Deletes a record from a database', +'dbase_get_header_info' => 'Gets the header info of a database', +'dbase_get_record' => 'Gets a record from a database as an indexed array', +'dbase_get_record_with_names' => 'Gets a record from a database as an associative array', +'dbase_numfields' => 'Gets the number of fields of a database', +'dbase_numrecords' => 'Gets the number of records in a database', +'dbase_open' => 'Opens a database', +'dbase_pack' => 'Packs a database', +'dbase_replace_record' => 'Replaces a record in a database', +'dbplus_add' => 'Add a tuple to a relation', +'dbplus_aql' => 'Perform AQL query', +'dbplus_chdir' => 'Get/Set database virtual current directory', +'dbplus_close' => 'Close a relation', +'dbplus_curr' => 'Get current tuple from relation', +'dbplus_errcode' => 'Get error string for given errorcode or last error', +'dbplus_errno' => 'Get error code for last operation', +'dbplus_find' => 'Set a constraint on a relation', +'dbplus_first' => 'Get first tuple from relation', +'dbplus_flush' => 'Flush all changes made on a relation', +'dbplus_freealllocks' => 'Free all locks held by this client', +'dbplus_freelock' => 'Release write lock on tuple', +'dbplus_freerlocks' => 'Free all tuple locks on given relation', +'dbplus_getlock' => 'Get a write lock on a tuple', +'dbplus_getunique' => 'Get an id number unique to a relation', +'dbplus_info' => 'Get information about a relation', +'dbplus_last' => 'Get last tuple from relation', +'dbplus_lockrel' => 'Request write lock on relation', +'dbplus_next' => 'Get next tuple from relation', +'dbplus_open' => 'Open relation file', +'dbplus_prev' => 'Get previous tuple from relation', +'dbplus_rchperm' => 'Change relation permissions', +'dbplus_rcreate' => 'Creates a new DB++ relation', +'dbplus_rcrtexact' => 'Creates an exact but empty copy of a relation including indices', +'dbplus_rcrtlike' => 'Creates an empty copy of a relation with default indices', +'dbplus_resolve' => 'Resolve host information for relation', +'dbplus_restorepos' => 'Restore position', +'dbplus_rkeys' => 'Specify new primary key for a relation', +'dbplus_ropen' => 'Open relation file local', +'dbplus_rquery' => 'Perform local (raw) AQL query', +'dbplus_rrename' => 'Rename a relation', +'dbplus_rsecindex' => 'Create a new secondary index for a relation', +'dbplus_runlink' => 'Remove relation from filesystem', +'dbplus_rzap' => 'Remove all tuples from relation', +'dbplus_savepos' => 'Save position', +'dbplus_setindex' => 'Set index', +'dbplus_setindexbynumber' => 'Set index by number', +'dbplus_sql' => 'Perform SQL query', +'dbplus_tcl' => 'Execute TCL code on server side', +'dbplus_tremove' => 'Remove tuple and return new current tuple', +'dbplus_undo' => 'Undo', +'dbplus_undoprepare' => 'Prepare undo', +'dbplus_unlockrel' => 'Give up write lock on relation', +'dbplus_unselect' => 'Remove a constraint from relation', +'dbplus_update' => 'Update specified tuple in relation', +'dbplus_xlockrel' => 'Request exclusive lock on relation', +'dbplus_xunlockrel' => 'Free exclusive lock on relation', +'dbx_close' => 'Close an open connection/database', +'dbx_compare' => 'Compare two rows for sorting purposes', +'dbx_connect' => 'Open a connection/database', +'dbx_error' => 'Report the error message of the latest function call in the module', +'dbx_escape_string' => 'Escape a string so it can safely be used in an sql-statement', +'dbx_fetch_row' => 'Fetches rows from a query-result that had the DBX_RESULT_UNBUFFERED flag set', +'dbx_query' => 'Send a query and fetch all results (if any)', +'dbx_sort' => 'Sort a result from a dbx_query by a custom sort function', +'dcgettext' => 'Overrides the domain for a single lookup', +'dcngettext' => 'Plural version of dcgettext', +'debug_backtrace' => 'Generates a backtrace', +'debug_print_backtrace' => 'Prints a backtrace', +'debug_zval_dump' => 'Dumps a string representation of an internal zend value to output', +'decbin' => 'Decimal to binary', +'dechex' => 'Decimal to hexadecimal', +'decoct' => 'Decimal to octal', +'define' => 'Defines a named constant', +'define_syslog_variables' => 'Initializes all syslog related variables', +'defined' => 'Checks whether a given named constant exists', +'deflate_add' => 'Incrementally deflate data', +'deflate_init' => 'Initialize an incremental deflate context', +'deg2rad' => 'Converts the number in degrees to the radian equivalent', +'delete' => 'See unlink or unset', +'dgettext' => 'Override the current domain', +'die' => 'Equivalent to exit', +'dio_close' => 'Closes the file descriptor given by fd', +'dio_fcntl' => 'Performs a c library fcntl on fd', +'dio_open' => 'Opens a file (creating it if necessary) at a lower level than the C library input/output stream functions allow', +'dio_read' => 'Reads bytes from a file descriptor', +'dio_seek' => 'Seeks to pos on fd from whence', +'dio_stat' => 'Gets stat information about the file descriptor fd', +'dio_tcsetattr' => 'Sets terminal attributes and baud rate for a serial port', +'dio_truncate' => 'Truncates file descriptor fd to offset bytes', +'dio_write' => 'Writes data to fd with optional truncation at length', +'dir' => 'Return an instance of the Directory class', +'directory::close' => 'Close directory handle', +'directory::read' => 'Read entry from directory handle', +'directory::rewind' => 'Rewind directory handle', +'DirectoryIterator::__construct' => 'Constructs a new directory iterator from a path', +'DirectoryIterator::__toString' => 'Get file name as a string', +'DirectoryIterator::current' => 'Return the current DirectoryIterator item', +'DirectoryIterator::getATime' => 'Get last access time of the current DirectoryIterator item', +'DirectoryIterator::getBasename' => 'Get base name of current DirectoryIterator item', +'DirectoryIterator::getCTime' => 'Get inode change time of the current DirectoryIterator item', +'DirectoryIterator::getExtension' => 'Gets the file extension', +'DirectoryIterator::getFileInfo' => 'Gets an SplFileInfo object for the file', +'DirectoryIterator::getFilename' => 'Return file name of current DirectoryIterator item', +'DirectoryIterator::getGroup' => 'Get group for the current DirectoryIterator item', +'DirectoryIterator::getInode' => 'Get inode for the current DirectoryIterator item', +'DirectoryIterator::getLinkTarget' => 'Gets the target of a link', +'DirectoryIterator::getMTime' => 'Get last modification time of current DirectoryIterator item', +'DirectoryIterator::getOwner' => 'Get owner of current DirectoryIterator item', +'DirectoryIterator::getPath' => 'Get path of current Iterator item without filename', +'DirectoryIterator::getPathInfo' => 'Gets an SplFileInfo object for the path', +'DirectoryIterator::getPathname' => 'Return path and file name of current DirectoryIterator item', +'DirectoryIterator::getPerms' => 'Get the permissions of current DirectoryIterator item', +'DirectoryIterator::getRealPath' => 'Gets absolute path to file', +'DirectoryIterator::getSize' => 'Get size of current DirectoryIterator item', +'DirectoryIterator::getType' => 'Determine the type of the current DirectoryIterator item', +'DirectoryIterator::isDir' => 'Determine if current DirectoryIterator item is a directory', +'DirectoryIterator::isDot' => 'Determine if current DirectoryIterator item is \'.\' or \'..\'', +'DirectoryIterator::isExecutable' => 'Determine if current DirectoryIterator item is executable', +'DirectoryIterator::isFile' => 'Determine if current DirectoryIterator item is a regular file', +'DirectoryIterator::isLink' => 'Determine if current DirectoryIterator item is a symbolic link', +'DirectoryIterator::isReadable' => 'Determine if current DirectoryIterator item can be read', +'DirectoryIterator::isWritable' => 'Determine if current DirectoryIterator item can be written to', +'DirectoryIterator::key' => 'Return the key for the current DirectoryIterator item', +'DirectoryIterator::next' => 'Move forward to next DirectoryIterator item', +'DirectoryIterator::openFile' => 'Gets an SplFileObject object for the file', +'DirectoryIterator::rewind' => 'Rewind the DirectoryIterator back to the start', +'DirectoryIterator::seek' => 'Seek to a DirectoryIterator item', +'DirectoryIterator::setFileClass' => 'Sets the class used with SplFileInfo::openFile', +'DirectoryIterator::setInfoClass' => 'Sets the class used with SplFileInfo::getFileInfo and SplFileInfo::getPathInfo', +'DirectoryIterator::valid' => 'Check whether current DirectoryIterator position is a valid file', +'dirname' => 'Returns a parent directory\'s path', +'disk_free_space' => 'Returns available space on filesystem or disk partition', +'disk_total_space' => 'Returns the total size of a filesystem or disk partition', +'diskfreespace' => 'Alias of disk_free_space', +'DivisionByZeroError::__clone' => 'Clone the error +Error can not be clone, so this method results in fatal error.', +'DivisionByZeroError::__toString' => 'Gets a string representation of the thrown object', +'DivisionByZeroError::getCode' => 'Gets the exception code', +'DivisionByZeroError::getFile' => 'Gets the file in which the exception occurred', +'DivisionByZeroError::getLine' => 'Gets the line on which the object was instantiated', +'DivisionByZeroError::getPrevious' => 'Returns the previous Throwable', +'DivisionByZeroError::getTrace' => 'Gets the stack trace', +'DivisionByZeroError::getTraceAsString' => 'Gets the stack trace as a string', +'dl' => 'Loads a PHP extension at runtime', +'dngettext' => 'Plural version of dgettext', +'dns_check_record' => 'Alias of checkdnsrr', +'dns_get_mx' => 'Alias of getmxrr', +'dns_get_record' => 'Fetch DNS Resource Records associated with a hostname', +'dom_import_simplexml' => 'Gets a DOMElement object from a SimpleXMLElement object', +'DomainException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'DomainException::__toString' => 'String representation of the exception', +'DomainException::getCode' => 'Gets the Exception code', +'DomainException::getFile' => 'Gets the file in which the exception occurred', +'DomainException::getLine' => 'Gets the line in which the exception occurred', +'DomainException::getMessage' => 'Gets the Exception message', +'DomainException::getPrevious' => 'Returns previous Exception', +'DomainException::getTrace' => 'Gets the stack trace', +'DomainException::getTraceAsString' => 'Gets the stack trace as a string', +'domattr::__construct' => 'Creates a new DOMAttr object', +'DOMAttr::appendChild' => 'Adds new child at the end of the children', +'DOMAttr::C14N' => 'Canonicalize nodes to a string', +'DOMAttr::C14NFile' => 'Canonicalize nodes to a file', +'DOMAttr::cloneNode' => 'Clones a node', +'DOMAttr::getLineNo' => 'Get line number for a node', +'DOMAttr::getNodePath' => 'Get an XPath for a node', +'DOMAttr::hasAttributes' => 'Checks if node has attributes', +'DOMAttr::hasChildNodes' => 'Checks if node has children', +'DOMAttr::insertBefore' => 'Adds a new child before a reference node', +'DOMAttr::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'domattr::isId' => 'Checks if attribute is a defined ID', +'DOMAttr::isSameNode' => 'Indicates if two nodes are the same node', +'DOMAttr::isSupported' => 'Checks if feature is supported for specified version', +'DOMAttr::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMAttr::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMAttr::normalize' => 'Normalizes the node', +'DOMAttr::removeChild' => 'Removes child from list of children', +'DOMAttr::replaceChild' => 'Replaces a child', +'domcdatasection::__construct' => 'Constructs a new DOMCdataSection object', +'DOMCdataSection::appendChild' => 'Adds new child at the end of the children', +'DOMCdataSection::appendData' => 'Append the string to the end of the character data of the node', +'DOMCdataSection::C14N' => 'Canonicalize nodes to a string', +'DOMCdataSection::C14NFile' => 'Canonicalize nodes to a file', +'DOMCdataSection::cloneNode' => 'Clones a node', +'DOMCdataSection::deleteData' => 'Remove a range of characters from the node', +'DOMCdataSection::getLineNo' => 'Get line number for a node', +'DOMCdataSection::getNodePath' => 'Get an XPath for a node', +'DOMCdataSection::hasAttributes' => 'Checks if node has attributes', +'DOMCdataSection::hasChildNodes' => 'Checks if node has children', +'DOMCdataSection::insertBefore' => 'Adds a new child before a reference node', +'DOMCdataSection::insertData' => 'Insert a string at the specified 16-bit unit offset', +'DOMCdataSection::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMCdataSection::isElementContentWhitespace' => 'Returns whether this text node contains whitespace in element content', +'DOMCdataSection::isSameNode' => 'Indicates if two nodes are the same node', +'DOMCdataSection::isSupported' => 'Checks if feature is supported for specified version', +'DOMCdataSection::isWhitespaceInElementContent' => 'Indicates whether this text node contains whitespace', +'DOMCdataSection::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMCdataSection::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMCdataSection::normalize' => 'Normalizes the node', +'DOMCdataSection::removeChild' => 'Removes child from list of children', +'DOMCdataSection::replaceChild' => 'Replaces a child', +'DOMCdataSection::replaceData' => 'Replace a substring within the DOMCharacterData node', +'DOMCdataSection::splitText' => 'Breaks this node into two nodes at the specified offset', +'DOMCdataSection::substringData' => 'Extracts a range of data from the node', +'DOMCharacterData::appendChild' => 'Adds new child at the end of the children', +'domcharacterdata::appendData' => 'Append the string to the end of the character data of the node', +'DOMCharacterData::C14N' => 'Canonicalize nodes to a string', +'DOMCharacterData::C14NFile' => 'Canonicalize nodes to a file', +'DOMCharacterData::cloneNode' => 'Clones a node', +'domcharacterdata::deleteData' => 'Remove a range of characters from the node', +'DOMCharacterData::getLineNo' => 'Get line number for a node', +'DOMCharacterData::getNodePath' => 'Get an XPath for a node', +'DOMCharacterData::hasAttributes' => 'Checks if node has attributes', +'DOMCharacterData::hasChildNodes' => 'Checks if node has children', +'DOMCharacterData::insertBefore' => 'Adds a new child before a reference node', +'domcharacterdata::insertData' => 'Insert a string at the specified 16-bit unit offset', +'DOMCharacterData::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMCharacterData::isSameNode' => 'Indicates if two nodes are the same node', +'DOMCharacterData::isSupported' => 'Checks if feature is supported for specified version', +'DOMCharacterData::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMCharacterData::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMCharacterData::normalize' => 'Normalizes the node', +'DOMCharacterData::removeChild' => 'Removes child from list of children', +'DOMCharacterData::replaceChild' => 'Replaces a child', +'domcharacterdata::replaceData' => 'Replace a substring within the DOMCharacterData node', +'domcharacterdata::substringData' => 'Extracts a range of data from the node', +'domcomment::__construct' => 'Creates a new DOMComment object', +'DOMComment::appendChild' => 'Adds new child at the end of the children', +'DOMComment::appendData' => 'Append the string to the end of the character data of the node', +'DOMComment::C14N' => 'Canonicalize nodes to a string', +'DOMComment::C14NFile' => 'Canonicalize nodes to a file', +'DOMComment::cloneNode' => 'Clones a node', +'DOMComment::deleteData' => 'Remove a range of characters from the node', +'DOMComment::getLineNo' => 'Get line number for a node', +'DOMComment::getNodePath' => 'Get an XPath for a node', +'DOMComment::hasAttributes' => 'Checks if node has attributes', +'DOMComment::hasChildNodes' => 'Checks if node has children', +'DOMComment::insertBefore' => 'Adds a new child before a reference node', +'DOMComment::insertData' => 'Insert a string at the specified 16-bit unit offset', +'DOMComment::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMComment::isSameNode' => 'Indicates if two nodes are the same node', +'DOMComment::isSupported' => 'Checks if feature is supported for specified version', +'DOMComment::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMComment::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMComment::normalize' => 'Normalizes the node', +'DOMComment::removeChild' => 'Removes child from list of children', +'DOMComment::replaceChild' => 'Replaces a child', +'DOMComment::replaceData' => 'Replace a substring within the DOMCharacterData node', +'DOMComment::substringData' => 'Extracts a range of data from the node', +'domdocument::__construct' => 'Creates a new DOMDocument object', +'DOMDocument::appendChild' => 'Adds new child at the end of the children', +'DOMDocument::C14N' => 'Canonicalize nodes to a string', +'DOMDocument::C14NFile' => 'Canonicalize nodes to a file', +'DOMDocument::cloneNode' => 'Clones a node', +'domdocument::createAttribute' => 'Create new attribute', +'domdocument::createAttributeNS' => 'Create new attribute node with an associated namespace', +'domdocument::createCDATASection' => 'Create new cdata node', +'domdocument::createComment' => 'Create new comment node', +'domdocument::createDocumentFragment' => 'Create new document fragment', +'domdocument::createElement' => 'Create new element node', +'domdocument::createElementNS' => 'Create new element node with an associated namespace', +'domdocument::createEntityReference' => 'Create new entity reference node', +'domdocument::createProcessingInstruction' => 'Creates new PI node', +'domdocument::createTextNode' => 'Create new text node', +'domdocument::getElementById' => 'Searches for an element with a certain id', +'domdocument::getElementsByTagName' => 'Searches for all elements with given local tag name', +'domdocument::getElementsByTagNameNS' => 'Searches for all elements with given tag name in specified namespace', +'DOMDocument::getLineNo' => 'Get line number for a node', +'DOMDocument::getNodePath' => 'Get an XPath for a node', +'DOMDocument::hasAttributes' => 'Checks if node has attributes', +'DOMDocument::hasChildNodes' => 'Checks if node has children', +'domdocument::importNode' => 'Import node into current document', +'DOMDocument::insertBefore' => 'Adds a new child before a reference node', +'DOMDocument::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMDocument::isSameNode' => 'Indicates if two nodes are the same node', +'DOMDocument::isSupported' => 'Checks if feature is supported for specified version', +'domdocument::load' => 'Load XML from a file', +'domdocument::loadHTML' => 'Load HTML from a string', +'domdocument::loadHTMLFile' => 'Load HTML from a file', +'domdocument::loadXML' => 'Load XML from a string', +'DOMDocument::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMDocument::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMDocument::normalize' => 'Normalizes the node', +'domdocument::normalizeDocument' => 'Normalizes the document', +'domdocument::registerNodeClass' => 'Register extended class used to create base node type', +'domdocument::relaxNGValidate' => 'Performs relaxNG validation on the document', +'domdocument::relaxNGValidateSource' => 'Performs relaxNG validation on the document', +'DOMDocument::removeChild' => 'Removes child from list of children', +'DOMDocument::replaceChild' => 'Replaces a child', +'domdocument::save' => 'Dumps the internal XML tree back into a file', +'domdocument::saveHTML' => 'Dumps the internal document into a string using HTML formatting', +'domdocument::saveHTMLFile' => 'Dumps the internal document into a file using HTML formatting', +'domdocument::saveXML' => 'Dumps the internal XML tree back into a string', +'domdocument::schemaValidate' => 'Validates a document based on a schema', +'domdocument::schemaValidateSource' => 'Validates a document based on a schema', +'domdocument::validate' => 'Validates the document based on its DTD', +'domdocument::xinclude' => 'Substitutes XIncludes in a DOMDocument Object', +'DOMDocumentFragment::appendChild' => 'Adds new child at the end of the children', +'domdocumentfragment::appendXML' => 'Append raw XML data', +'DOMDocumentFragment::C14N' => 'Canonicalize nodes to a string', +'DOMDocumentFragment::C14NFile' => 'Canonicalize nodes to a file', +'DOMDocumentFragment::cloneNode' => 'Clones a node', +'DOMDocumentFragment::getLineNo' => 'Get line number for a node', +'DOMDocumentFragment::getNodePath' => 'Get an XPath for a node', +'DOMDocumentFragment::hasAttributes' => 'Checks if node has attributes', +'DOMDocumentFragment::hasChildNodes' => 'Checks if node has children', +'DOMDocumentFragment::insertBefore' => 'Adds a new child before a reference node', +'DOMDocumentFragment::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMDocumentFragment::isSameNode' => 'Indicates if two nodes are the same node', +'DOMDocumentFragment::isSupported' => 'Checks if feature is supported for specified version', +'DOMDocumentFragment::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMDocumentFragment::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMDocumentFragment::normalize' => 'Normalizes the node', +'DOMDocumentFragment::removeChild' => 'Removes child from list of children', +'DOMDocumentFragment::replaceChild' => 'Replaces a child', +'DOMDocumentType::appendChild' => 'Adds new child at the end of the children', +'DOMDocumentType::C14N' => 'Canonicalize nodes to a string', +'DOMDocumentType::C14NFile' => 'Canonicalize nodes to a file', +'DOMDocumentType::cloneNode' => 'Clones a node', +'DOMDocumentType::getLineNo' => 'Get line number for a node', +'DOMDocumentType::getNodePath' => 'Get an XPath for a node', +'DOMDocumentType::hasAttributes' => 'Checks if node has attributes', +'DOMDocumentType::hasChildNodes' => 'Checks if node has children', +'DOMDocumentType::insertBefore' => 'Adds a new child before a reference node', +'DOMDocumentType::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMDocumentType::isSameNode' => 'Indicates if two nodes are the same node', +'DOMDocumentType::isSupported' => 'Checks if feature is supported for specified version', +'DOMDocumentType::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMDocumentType::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMDocumentType::normalize' => 'Normalizes the node', +'DOMDocumentType::removeChild' => 'Removes child from list of children', +'DOMDocumentType::replaceChild' => 'Replaces a child', +'domelement::__construct' => 'Creates a new DOMElement object', +'DOMElement::appendChild' => 'Adds new child at the end of the children', +'DOMElement::C14N' => 'Canonicalize nodes to a string', +'DOMElement::C14NFile' => 'Canonicalize nodes to a file', +'DOMElement::cloneNode' => 'Clones a node', +'domelement::getAttribute' => 'Returns value of attribute', +'domelement::getAttributeNode' => 'Returns attribute node', +'domelement::getAttributeNodeNS' => 'Returns attribute node', +'domelement::getAttributeNS' => 'Returns value of attribute', +'domelement::getElementsByTagName' => 'Gets elements by tagname', +'domelement::getElementsByTagNameNS' => 'Get elements by namespaceURI and localName', +'DOMElement::getLineNo' => 'Get line number for a node', +'DOMElement::getNodePath' => 'Get an XPath for a node', +'domelement::hasAttribute' => 'Checks to see if attribute exists', +'domelement::hasAttributeNS' => 'Checks to see if attribute exists', +'DOMElement::hasAttributes' => 'Checks if node has attributes', +'DOMElement::hasChildNodes' => 'Checks if node has children', +'DOMElement::insertBefore' => 'Adds a new child before a reference node', +'DOMElement::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMElement::isSameNode' => 'Indicates if two nodes are the same node', +'DOMElement::isSupported' => 'Checks if feature is supported for specified version', +'DOMElement::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMElement::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMElement::normalize' => 'Normalizes the node', +'domelement::removeAttribute' => 'Removes attribute', +'domelement::removeAttributeNode' => 'Removes attribute', +'domelement::removeAttributeNS' => 'Removes attribute', +'DOMElement::removeChild' => 'Removes child from list of children', +'DOMElement::replaceChild' => 'Replaces a child', +'domelement::setAttribute' => 'Adds new attribute', +'domelement::setAttributeNode' => 'Adds new attribute node to element', +'domelement::setAttributeNodeNS' => 'Adds new attribute node to element', +'domelement::setAttributeNS' => 'Adds new attribute', +'domelement::setIdAttribute' => 'Declares the attribute specified by name to be of type ID', +'domelement::setIdAttributeNode' => 'Declares the attribute specified by node to be of type ID', +'domelement::setIdAttributeNS' => 'Declares the attribute specified by local name and namespace URI to be of type ID', +'DOMEntity::appendChild' => 'Adds new child at the end of the children', +'DOMEntity::C14N' => 'Canonicalize nodes to a string', +'DOMEntity::C14NFile' => 'Canonicalize nodes to a file', +'DOMEntity::cloneNode' => 'Clones a node', +'DOMEntity::getLineNo' => 'Get line number for a node', +'DOMEntity::getNodePath' => 'Get an XPath for a node', +'DOMEntity::hasAttributes' => 'Checks if node has attributes', +'DOMEntity::hasChildNodes' => 'Checks if node has children', +'DOMEntity::insertBefore' => 'Adds a new child before a reference node', +'DOMEntity::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMEntity::isSameNode' => 'Indicates if two nodes are the same node', +'DOMEntity::isSupported' => 'Checks if feature is supported for specified version', +'DOMEntity::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMEntity::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMEntity::normalize' => 'Normalizes the node', +'DOMEntity::removeChild' => 'Removes child from list of children', +'DOMEntity::replaceChild' => 'Replaces a child', +'domentityreference::__construct' => 'Creates a new DOMEntityReference object', +'DOMEntityReference::appendChild' => 'Adds new child at the end of the children', +'DOMEntityReference::C14N' => 'Canonicalize nodes to a string', +'DOMEntityReference::C14NFile' => 'Canonicalize nodes to a file', +'DOMEntityReference::cloneNode' => 'Clones a node', +'DOMEntityReference::getLineNo' => 'Get line number for a node', +'DOMEntityReference::getNodePath' => 'Get an XPath for a node', +'DOMEntityReference::hasAttributes' => 'Checks if node has attributes', +'DOMEntityReference::hasChildNodes' => 'Checks if node has children', +'DOMEntityReference::insertBefore' => 'Adds a new child before a reference node', +'DOMEntityReference::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMEntityReference::isSameNode' => 'Indicates if two nodes are the same node', +'DOMEntityReference::isSupported' => 'Checks if feature is supported for specified version', +'DOMEntityReference::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMEntityReference::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMEntityReference::normalize' => 'Normalizes the node', +'DOMEntityReference::removeChild' => 'Removes child from list of children', +'DOMEntityReference::replaceChild' => 'Replaces a child', +'DOMException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'DOMException::__toString' => 'String representation of the exception', +'DOMException::getCode' => 'Gets the Exception code', +'DOMException::getFile' => 'Gets the file in which the exception occurred', +'DOMException::getLine' => 'Gets the line in which the exception occurred', +'DOMException::getMessage' => 'Gets the Exception message', +'DOMException::getPrevious' => 'Returns previous Exception', +'DOMException::getTrace' => 'Gets the stack trace', +'DOMException::getTraceAsString' => 'Gets the stack trace as a string', +'domimplementation::__construct' => 'Creates a new DOMImplementation object', +'domimplementation::createDocument' => 'Creates a DOMDocument object of the specified type with its document element', +'domimplementation::createDocumentType' => 'Creates an empty DOMDocumentType object', +'domimplementation::hasFeature' => 'Test if the DOM implementation implements a specific feature', +'domnamednodemap::count' => 'Get number of nodes in the map', +'domnamednodemap::getNamedItem' => 'Retrieves a node specified by name', +'domnamednodemap::getNamedItemNS' => 'Retrieves a node specified by local name and namespace URI', +'domnamednodemap::item' => 'Retrieves a node specified by index', +'domnode::appendChild' => 'Adds new child at the end of the children', +'domnode::C14N' => 'Canonicalize nodes to a string', +'domnode::C14NFile' => 'Canonicalize nodes to a file', +'domnode::cloneNode' => 'Clones a node', +'domnode::getLineNo' => 'Get line number for a node', +'domnode::getNodePath' => 'Get an XPath for a node', +'domnode::hasAttributes' => 'Checks if node has attributes', +'domnode::hasChildNodes' => 'Checks if node has children', +'domnode::insertBefore' => 'Adds a new child before a reference node', +'domnode::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'domnode::isSameNode' => 'Indicates if two nodes are the same node', +'domnode::isSupported' => 'Checks if feature is supported for specified version', +'domnode::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'domnode::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'domnode::normalize' => 'Normalizes the node', +'domnode::removeChild' => 'Removes child from list of children', +'domnode::replaceChild' => 'Replaces a child', +'domnodelist::count' => 'Get number of nodes in the list', +'domnodelist::item' => 'Retrieves a node specified by index', +'DOMNotation::appendChild' => 'Adds new child at the end of the children', +'DOMNotation::C14N' => 'Canonicalize nodes to a string', +'DOMNotation::C14NFile' => 'Canonicalize nodes to a file', +'DOMNotation::cloneNode' => 'Clones a node', +'DOMNotation::getLineNo' => 'Get line number for a node', +'DOMNotation::getNodePath' => 'Get an XPath for a node', +'DOMNotation::hasAttributes' => 'Checks if node has attributes', +'DOMNotation::hasChildNodes' => 'Checks if node has children', +'DOMNotation::insertBefore' => 'Adds a new child before a reference node', +'DOMNotation::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMNotation::isSameNode' => 'Indicates if two nodes are the same node', +'DOMNotation::isSupported' => 'Checks if feature is supported for specified version', +'DOMNotation::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMNotation::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMNotation::normalize' => 'Normalizes the node', +'DOMNotation::removeChild' => 'Removes child from list of children', +'DOMNotation::replaceChild' => 'Replaces a child', +'domprocessinginstruction::__construct' => 'Creates a new DOMProcessingInstruction object', +'DOMProcessingInstruction::appendChild' => 'Adds new child at the end of the children', +'DOMProcessingInstruction::C14N' => 'Canonicalize nodes to a string', +'DOMProcessingInstruction::C14NFile' => 'Canonicalize nodes to a file', +'DOMProcessingInstruction::cloneNode' => 'Clones a node', +'DOMProcessingInstruction::getLineNo' => 'Get line number for a node', +'DOMProcessingInstruction::getNodePath' => 'Get an XPath for a node', +'DOMProcessingInstruction::hasAttributes' => 'Checks if node has attributes', +'DOMProcessingInstruction::hasChildNodes' => 'Checks if node has children', +'DOMProcessingInstruction::insertBefore' => 'Adds a new child before a reference node', +'DOMProcessingInstruction::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'DOMProcessingInstruction::isSameNode' => 'Indicates if two nodes are the same node', +'DOMProcessingInstruction::isSupported' => 'Checks if feature is supported for specified version', +'DOMProcessingInstruction::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMProcessingInstruction::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMProcessingInstruction::normalize' => 'Normalizes the node', +'DOMProcessingInstruction::removeChild' => 'Removes child from list of children', +'DOMProcessingInstruction::replaceChild' => 'Replaces a child', +'domtext::__construct' => 'Creates a new DOMText object', +'DOMText::appendChild' => 'Adds new child at the end of the children', +'DOMText::appendData' => 'Append the string to the end of the character data of the node', +'DOMText::C14N' => 'Canonicalize nodes to a string', +'DOMText::C14NFile' => 'Canonicalize nodes to a file', +'DOMText::cloneNode' => 'Clones a node', +'DOMText::deleteData' => 'Remove a range of characters from the node', +'DOMText::getLineNo' => 'Get line number for a node', +'DOMText::getNodePath' => 'Get an XPath for a node', +'DOMText::hasAttributes' => 'Checks if node has attributes', +'DOMText::hasChildNodes' => 'Checks if node has children', +'DOMText::insertBefore' => 'Adds a new child before a reference node', +'DOMText::insertData' => 'Insert a string at the specified 16-bit unit offset', +'DOMText::isDefaultNamespace' => 'Checks if the specified namespaceURI is the default namespace or not', +'domtext::isElementContentWhitespace' => 'Returns whether this text node contains whitespace in element content', +'DOMText::isSameNode' => 'Indicates if two nodes are the same node', +'DOMText::isSupported' => 'Checks if feature is supported for specified version', +'domtext::isWhitespaceInElementContent' => 'Indicates whether this text node contains whitespace', +'DOMText::lookupNamespaceUri' => 'Gets the namespace URI of the node based on the prefix', +'DOMText::lookupPrefix' => 'Gets the namespace prefix of the node based on the namespace URI', +'DOMText::normalize' => 'Normalizes the node', +'DOMText::removeChild' => 'Removes child from list of children', +'DOMText::replaceChild' => 'Replaces a child', +'DOMText::replaceData' => 'Replace a substring within the DOMCharacterData node', +'domtext::splitText' => 'Breaks this node into two nodes at the specified offset', +'DOMText::substringData' => 'Extracts a range of data from the node', +'domxpath::__construct' => 'Creates a new DOMXPath object', +'domxpath::evaluate' => 'Evaluates the given XPath expression and returns a typed result if possible', +'domxpath::query' => 'Evaluates the given XPath expression', +'domxpath::registerNamespace' => 'Registers the namespace with the DOMXPath object', +'domxpath::registerPhpFunctions' => 'Register PHP functions as XPath functions', +'DOTNET::__construct' => 'COM class constructor.', +'doubleval' => 'Alias of floatval', +'ds\collection::clear' => 'Removes all values', +'ds\collection::copy' => 'Returns a shallow copy of the collection', +'ds\collection::isEmpty' => 'Returns whether the collection is empty', +'ds\collection::toArray' => 'Converts the collection to an `array`', +'ds\deque::__construct' => 'Creates a new instance', +'ds\deque::allocate' => 'Allocates enough memory for a required capacity', +'ds\deque::apply' => 'Updates all values by applying a callback function to each value', +'ds\deque::capacity' => 'Returns the current capacity', +'ds\deque::clear' => 'Removes all values from the deque', +'ds\deque::contains' => 'Determines if the deque contains given values', +'ds\deque::copy' => 'Returns a shallow copy of the deque', +'ds\deque::count' => 'Returns the number of values in the collection', +'ds\deque::filter' => 'Creates a new deque using a callable to determine which values to include', +'ds\deque::find' => 'Attempts to find a value\'s index', +'ds\deque::first' => 'Returns the first value in the deque', +'ds\deque::get' => 'Returns the value at a given index', +'ds\deque::insert' => 'Inserts values at a given index', +'ds\deque::isEmpty' => 'Returns whether the deque is empty', +'ds\deque::join' => 'Joins all values together as a string', +'ds\deque::jsonSerialize' => 'Returns a representation that can be converted to JSON', +'ds\deque::last' => 'Returns the last value', +'ds\deque::map' => 'Returns the result of applying a callback to each value', +'ds\deque::merge' => 'Returns the result of adding all given values to the deque', +'ds\deque::pop' => 'Removes and returns the last value', +'ds\deque::push' => 'Adds values to the end of the deque', +'ds\deque::reduce' => 'Reduces the deque to a single value using a callback function', +'ds\deque::remove' => 'Removes and returns a value by index', +'ds\deque::reverse' => 'Reverses the deque in-place', +'ds\deque::reversed' => 'Returns a reversed copy', +'ds\deque::rotate' => 'Rotates the deque by a given number of rotations', +'ds\deque::set' => 'Updates a value at a given index', +'ds\deque::shift' => 'Removes and returns the first value', +'ds\deque::slice' => 'Returns a sub-deque of a given range', +'ds\deque::sort' => 'Sorts the deque in-place', +'ds\deque::sorted' => 'Returns a sorted copy', +'ds\deque::sum' => 'Returns the sum of all values in the deque', +'ds\deque::toArray' => 'Converts the deque to an `array`', +'ds\deque::unshift' => 'Adds values to the front of the deque', +'ds\hashable::equals' => 'Determines whether an object is equal to the current instance', +'ds\hashable::hash' => 'Returns a scalar value to be used as a hash value', +'ds\map::__construct' => 'Creates a new instance', +'ds\map::allocate' => 'Allocates enough memory for a required capacity', +'ds\map::apply' => 'Updates all values by applying a callback function to each value', +'ds\map::capacity' => 'Returns the current capacity', +'ds\map::clear' => 'Removes all values', +'ds\map::copy' => 'Returns a shallow copy of the map', +'ds\map::count' => 'Returns the number of values in the map', +'ds\map::diff' => 'Creates a new map using keys that aren\'t in another map', +'ds\map::filter' => 'Creates a new map using a callable to determine which pairs to include', +'ds\map::first' => 'Returns the first pair in the map', +'ds\map::get' => 'Returns the value for a given key', +'ds\map::hasKey' => 'Determines whether the map contains a given key', +'ds\map::hasValue' => 'Determines whether the map contains a given value', +'ds\map::intersect' => 'Creates a new map by intersecting keys with another map', +'ds\map::isEmpty' => 'Returns whether the map is empty', +'ds\map::jsonSerialize' => 'Returns a representation that can be converted to JSON', +'ds\map::keys' => 'Returns a set of the map\'s keys', +'ds\map::ksort' => 'Sorts the map in-place by key', +'ds\map::ksorted' => 'Returns a copy, sorted by key', +'ds\map::last' => 'Returns the last pair of the map', +'ds\map::map' => 'Returns the result of applying a callback to each value', +'ds\map::merge' => 'Returns the result of adding all given associations', +'ds\map::pairs' => 'Returns a sequence containing all the pairs of the map', +'ds\map::put' => 'Associates a key with a value', +'ds\map::putAll' => 'Associates all key-value pairs of a traversable object or array', +'ds\map::reduce' => 'Reduces the map to a single value using a callback function', +'ds\map::remove' => 'Removes and returns a value by key', +'ds\map::reverse' => 'Reverses the map in-place', +'ds\map::reversed' => 'Returns a reversed copy', +'ds\map::skip' => 'Returns the pair at a given positional index', +'ds\map::slice' => 'Returns a subset of the map defined by a starting index and length', +'ds\map::sort' => 'Sorts the map in-place by value', +'ds\map::sorted' => 'Returns a copy, sorted by value', +'ds\map::sum' => 'Returns the sum of all values in the map', +'ds\map::toArray' => 'Converts the map to an `array`', +'ds\map::union' => 'Creates a new map using values from the current instance and another map', +'ds\map::values' => 'Returns a sequence of the map\'s values', +'ds\map::xor' => 'Creates a new map using keys of either the current instance or of another map, but not of both', +'ds\pair::__construct' => 'Creates a new instance', +'ds\pair::clear' => 'Removes all values', +'ds\pair::copy' => 'Returns a shallow copy of the pair', +'ds\pair::isEmpty' => 'Returns whether the pair is empty', +'ds\pair::jsonSerialize' => 'Returns a representation that can be converted to JSON', +'ds\pair::toArray' => 'Converts the pair to an `array`', +'ds\priorityqueue::__construct' => 'Creates a new instance', +'ds\priorityqueue::allocate' => 'Allocates enough memory for a required capacity', +'ds\priorityqueue::capacity' => 'Returns the current capacity', +'ds\priorityqueue::clear' => 'Removes all values', +'ds\priorityqueue::copy' => 'Returns a shallow copy of the queue', +'ds\priorityqueue::count' => 'Returns the number of values in the queue', +'ds\priorityqueue::isEmpty' => 'Returns whether the queue is empty', +'ds\priorityqueue::jsonSerialize' => 'Returns a representation that can be converted to JSON', +'ds\priorityqueue::peek' => 'Returns the value at the front of the queue', +'ds\priorityqueue::pop' => 'Removes and returns the value with the highest priority', +'ds\priorityqueue::push' => 'Pushes values into the queue', +'ds\priorityqueue::toArray' => 'Converts the queue to an `array`', +'ds\queue::__construct' => 'Creates a new instance', +'ds\queue::allocate' => 'Allocates enough memory for a required capacity', +'ds\queue::capacity' => 'Returns the current capacity', +'ds\queue::clear' => 'Removes all values', +'ds\queue::copy' => 'Returns a shallow copy of the queue', +'ds\queue::count' => 'Returns the number of values in the queue', +'ds\queue::isEmpty' => 'Returns whether the queue is empty', +'ds\queue::jsonSerialize' => 'Returns a representation that can be converted to JSON', +'ds\queue::peek' => 'Returns the value at the front of the queue', +'ds\queue::pop' => 'Removes and returns the value at the front of the queue', +'ds\queue::push' => 'Pushes values into the queue', +'ds\queue::toArray' => 'Converts the queue to an `array`', +'ds\sequence::allocate' => 'Allocates enough memory for a required capacity', +'ds\sequence::apply' => 'Updates all values by applying a callback function to each value', +'ds\sequence::capacity' => 'Returns the current capacity', +'ds\sequence::contains' => 'Determines if the sequence contains given values', +'ds\sequence::filter' => 'Creates a new sequence using a callable to determine which values to include', +'ds\sequence::find' => 'Attempts to find a value\'s index', +'ds\sequence::first' => 'Returns the first value in the sequence', +'ds\sequence::get' => 'Returns the value at a given index', +'ds\sequence::insert' => 'Inserts values at a given index', +'ds\sequence::join' => 'Joins all values together as a string', +'ds\sequence::last' => 'Returns the last value', +'ds\sequence::map' => 'Returns the result of applying a callback to each value', +'ds\sequence::merge' => 'Returns the result of adding all given values to the sequence', +'ds\sequence::pop' => 'Removes and returns the last value', +'ds\sequence::push' => 'Adds values to the end of the sequence', +'ds\sequence::reduce' => 'Reduces the sequence to a single value using a callback function', +'ds\sequence::remove' => 'Removes and returns a value by index', +'ds\sequence::reverse' => 'Reverses the sequence in-place', +'ds\sequence::reversed' => 'Returns a reversed copy', +'ds\sequence::rotate' => 'Rotates the sequence by a given number of rotations', +'ds\sequence::set' => 'Updates a value at a given index', +'ds\sequence::shift' => 'Removes and returns the first value', +'ds\sequence::slice' => 'Returns a sub-sequence of a given range', +'ds\sequence::sort' => 'Sorts the sequence in-place', +'ds\sequence::sorted' => 'Returns a sorted copy', +'ds\sequence::sum' => 'Returns the sum of all values in the sequence', +'ds\sequence::unshift' => 'Adds values to the front of the sequence', +'ds\set::__construct' => 'Creates a new instance', +'ds\set::add' => 'Adds values to the set', +'ds\set::allocate' => 'Allocates enough memory for a required capacity', +'ds\set::capacity' => 'Returns the current capacity', +'ds\set::clear' => 'Removes all values', +'ds\set::contains' => 'Determines if the set contains all values', +'ds\set::copy' => 'Returns a shallow copy of the set', +'ds\set::count' => 'Returns the number of values in the set', +'ds\set::diff' => 'Creates a new set using values that aren\'t in another set', +'ds\set::filter' => 'Creates a new set using a callable to determine which values to include', +'ds\set::first' => 'Returns the first value in the set', +'ds\set::get' => 'Returns the value at a given index', +'ds\set::intersect' => 'Creates a new set by intersecting values with another set', +'ds\set::isEmpty' => 'Returns whether the set is empty', +'ds\set::join' => 'Joins all values together as a string', +'ds\set::jsonSerialize' => 'Returns a representation that can be converted to JSON', +'ds\set::last' => 'Returns the last value in the set', +'ds\set::merge' => 'Returns the result of adding all given values to the set', +'ds\set::reduce' => 'Reduces the set to a single value using a callback function', +'ds\set::remove' => 'Removes all given values from the set', +'ds\set::reverse' => 'Reverses the set in-place', +'ds\set::reversed' => 'Returns a reversed copy', +'ds\set::slice' => 'Returns a sub-set of a given range', +'ds\set::sort' => 'Sorts the set in-place', +'ds\set::sorted' => 'Returns a sorted copy', +'ds\set::sum' => 'Returns the sum of all values in the set', +'ds\set::toArray' => 'Converts the set to an `array`', +'ds\set::union' => 'Creates a new set using values from the current instance and another set', +'ds\set::xor' => 'Creates a new set using values in either the current instance or in another set, but not in both', +'ds\stack::__construct' => 'Creates a new instance', +'ds\stack::allocate' => 'Allocates enough memory for a required capacity', +'ds\stack::capacity' => 'Returns the current capacity', +'ds\stack::clear' => 'Removes all values', +'ds\stack::copy' => 'Returns a shallow copy of the stack', +'ds\stack::count' => 'Returns the number of values in the stack', +'ds\stack::isEmpty' => 'Returns whether the stack is empty', +'ds\stack::jsonSerialize' => 'Returns a representation that can be converted to JSON', +'ds\stack::peek' => 'Returns the value at the top of the stack', +'ds\stack::pop' => 'Removes and returns the value at the top of the stack', +'ds\stack::push' => 'Pushes values onto the stack', +'ds\stack::toArray' => 'Converts the stack to an `array`', +'ds\vector::__construct' => 'Creates a new instance', +'ds\vector::allocate' => 'Allocates enough memory for a required capacity', +'ds\vector::apply' => 'Updates all values by applying a callback function to each value', +'ds\vector::capacity' => 'Returns the current capacity', +'ds\vector::clear' => 'Removes all values', +'ds\vector::contains' => 'Determines if the vector contains given values', +'ds\vector::copy' => 'Returns a shallow copy of the vector', +'ds\vector::count' => 'Returns the number of values in the collection', +'ds\vector::filter' => 'Creates a new vector using a callable to determine which values to include', +'ds\vector::find' => 'Attempts to find a value\'s index', +'ds\vector::first' => 'Returns the first value in the vector', +'ds\vector::get' => 'Returns the value at a given index', +'ds\vector::insert' => 'Inserts values at a given index', +'ds\vector::isEmpty' => 'Returns whether the vector is empty', +'ds\vector::join' => 'Joins all values together as a string', +'ds\vector::jsonSerialize' => 'Returns a representation that can be converted to JSON', +'ds\vector::last' => 'Returns the last value', +'ds\vector::map' => 'Returns the result of applying a callback to each value', +'ds\vector::merge' => 'Returns the result of adding all given values to the vector', +'ds\vector::pop' => 'Removes and returns the last value', +'ds\vector::push' => 'Adds values to the end of the vector', +'ds\vector::reduce' => 'Reduces the vector to a single value using a callback function', +'ds\vector::remove' => 'Removes and returns a value by index', +'ds\vector::reverse' => 'Reverses the vector in-place', +'ds\vector::reversed' => 'Returns a reversed copy', +'ds\vector::rotate' => 'Rotates the vector by a given number of rotations', +'ds\vector::set' => 'Updates a value at a given index', +'ds\vector::shift' => 'Removes and returns the first value', +'ds\vector::slice' => 'Returns a sub-vector of a given range', +'ds\vector::sort' => 'Sorts the vector in-place', +'ds\vector::sorted' => 'Returns a sorted copy', +'ds\vector::sum' => 'Returns the sum of all values in the vector', +'ds\vector::toArray' => 'Converts the vector to an `array`', +'ds\vector::unshift' => 'Adds values to the front of the vector', +'each' => 'Return the current key and value pair from an array and advance the array cursor', +'easter_date' => 'Get Unix timestamp for midnight on Easter of a given year', +'easter_days' => 'Get number of days after March 21 on which Easter falls for a given year', +'echo' => 'Output one or more strings', +'eio_busy' => 'Artificially increase load. Could be useful in tests, benchmarking', +'eio_cancel' => 'Cancels a request', +'eio_chmod' => 'Change file/direcrory permissions', +'eio_chown' => 'Change file/direcrory permissions', +'eio_close' => 'Close file', +'eio_custom' => 'Execute custom request like any other eio_* call', +'eio_dup2' => 'Duplicate a file descriptor', +'eio_event_loop' => 'Polls libeio until all requests proceeded', +'eio_fallocate' => 'Allows the caller to directly manipulate the allocated disk space for a file', +'eio_fchmod' => 'Change file permissions', +'eio_fchown' => 'Change file ownership', +'eio_fdatasync' => 'Synchronize a file\'s in-core state with storage device', +'eio_fstat' => 'Get file status', +'eio_fstatvfs' => 'Get file system statistics', +'eio_fsync' => 'Synchronize a file\'s in-core state with storage device', +'eio_ftruncate' => 'Truncate a file', +'eio_futime' => 'Change file last access and modification times', +'eio_get_event_stream' => 'Get stream representing a variable used in internal communications with libeio', +'eio_get_last_error' => 'Returns string describing the last error associated with a request resource', +'eio_grp' => 'Creates a request group', +'eio_grp_add' => 'Adds a request to the request group', +'eio_grp_cancel' => 'Cancels a request group', +'eio_grp_limit' => 'Set group limit', +'eio_init' => '(Re-)initialize Eio', +'eio_link' => 'Create a hardlink for file', +'eio_lstat' => 'Get file status', +'eio_mkdir' => 'Create directory', +'eio_mknod' => 'Create a special or ordinary file', +'eio_nop' => 'Does nothing, except go through the whole request cycle', +'eio_npending' => 'Returns number of finished, but unhandled requests', +'eio_nready' => 'Returns number of not-yet handled requests', +'eio_nreqs' => 'Returns number of requests to be processed', +'eio_nthreads' => 'Returns number of threads currently in use', +'eio_open' => 'Opens a file', +'eio_poll' => 'Can be to be called whenever there are pending requests that need finishing', +'eio_read' => 'Read from a file descriptor at given offset', +'eio_readahead' => 'Perform file readahead into page cache', +'eio_readdir' => 'Reads through a whole directory', +'eio_readlink' => 'Read value of a symbolic link', +'eio_realpath' => 'Get the canonicalized absolute pathname', +'eio_rename' => 'Change the name or location of a file', +'eio_rmdir' => 'Remove a directory', +'eio_seek' => 'Repositions the offset of the open file associated with the fd argument to the argument offset according to the directive whence', +'eio_sendfile' => 'Transfer data between file descriptors', +'eio_set_max_idle' => 'Set maximum number of idle threads', +'eio_set_max_parallel' => 'Set maximum parallel threads', +'eio_set_max_poll_reqs' => 'Set maximum number of requests processed in a poll', +'eio_set_max_poll_time' => 'Set maximum poll time', +'eio_set_min_parallel' => 'Set minimum parallel thread number', +'eio_stat' => 'Get file status', +'eio_statvfs' => 'Get file system statistics', +'eio_symlink' => 'Create a symbolic link', +'eio_sync' => 'Commit buffer cache to disk', +'eio_sync_file_range' => 'Sync a file segment with disk', +'eio_syncfs' => 'Calls Linux\' syncfs syscall, if available', +'eio_truncate' => 'Truncate a file', +'eio_unlink' => 'Delete a name and possibly the file it refers to', +'eio_utime' => 'Change file last access and modification times', +'eio_write' => 'Write to file', +'empty' => 'Determine whether a variable is empty', +'emptyiterator::current' => 'The current() method', +'emptyiterator::key' => 'The key() method', +'emptyiterator::next' => 'The next() method', +'emptyiterator::rewind' => 'The rewind() method', +'emptyiterator::valid' => 'The valid() method', +'enchant_broker_describe' => 'Enumerates the Enchant providers', +'enchant_broker_dict_exists' => 'Whether a dictionary exists or not. Using non-empty tag', +'enchant_broker_free' => 'Free the broker resource and its dictionnaries', +'enchant_broker_free_dict' => 'Free a dictionary resource', +'enchant_broker_get_dict_path' => 'Get the directory path for a given backend', +'enchant_broker_get_error' => 'Returns the last error of the broker', +'enchant_broker_init' => 'Create a new broker object capable of requesting', +'enchant_broker_list_dicts' => 'Returns a list of available dictionaries', +'enchant_broker_request_dict' => 'Create a new dictionary using a tag', +'enchant_broker_request_pwl_dict' => 'Creates a dictionary using a PWL file', +'enchant_broker_set_dict_path' => 'Set the directory path for a given backend', +'enchant_broker_set_ordering' => 'Declares a preference of dictionaries to use for the language', +'enchant_dict_add_to_personal' => 'Add a word to personal word list', +'enchant_dict_add_to_session' => 'Add \'word\' to this spell-checking session', +'enchant_dict_check' => 'Check whether a word is correctly spelled or not', +'enchant_dict_describe' => 'Describes an individual dictionary', +'enchant_dict_get_error' => 'Returns the last error of the current spelling-session', +'enchant_dict_is_in_session' => 'Whether or not \'word\' exists in this spelling-session', +'enchant_dict_quick_check' => 'Check the word is correctly spelled and provide suggestions', +'enchant_dict_store_replacement' => 'Add a correction for a word', +'enchant_dict_suggest' => 'Will return a list of values if any of those pre-conditions are not met', +'end' => 'Set the internal pointer of an array to its last element', +'ereg' => 'Regular expression match', +'ereg_replace' => 'Replace regular expression', +'eregi' => 'Case insensitive regular expression match', +'eregi_replace' => 'Replace regular expression case insensitive', +'Error::__clone' => 'Clone the error +Error can not be clone, so this method results in fatal error.', +'Error::__construct' => 'Construct the error object.', +'Error::__toString' => 'Gets a string representation of the thrown object', +'Error::getCode' => 'Gets the exception code', +'Error::getFile' => 'Gets the file in which the exception occurred', +'Error::getLine' => 'Gets the line on which the object was instantiated', +'Error::getPrevious' => 'Returns the previous Throwable', +'Error::getTrace' => 'Gets the stack trace', +'Error::getTraceAsString' => 'Gets the stack trace as a string', +'error_clear_last' => 'Clear the most recent error', +'error_get_last' => 'Get the last occurred error', +'error_log' => 'Send an error message to the defined error handling routines', +'error_reporting' => 'Sets which PHP errors are reported', +'ErrorException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'ErrorException::__construct' => 'Constructs the exception', +'ErrorException::__toString' => 'String representation of the exception', +'ErrorException::getCode' => 'Gets the Exception code', +'ErrorException::getFile' => 'Gets the file in which the exception occurred', +'ErrorException::getLine' => 'Gets the line in which the exception occurred', +'ErrorException::getMessage' => 'Gets the Exception message', +'ErrorException::getPrevious' => 'Returns previous Exception', +'ErrorException::getSeverity' => 'Gets the exception severity', +'ErrorException::getTrace' => 'Gets the stack trace', +'ErrorException::getTraceAsString' => 'Gets the stack trace as a string', +'escapeshellarg' => 'Escape a string to be used as a shell argument', +'escapeshellcmd' => 'Escape shell metacharacters', +'ev::backend' => 'Returns an integer describing the backend used by libev', +'ev::depth' => 'Returns recursion depth', +'ev::embeddableBackends' => 'Returns the set of backends that are embeddable in other event loops', +'ev::feedSignal' => 'Feed a signal event info Ev', +'ev::feedSignalEvent' => 'Feed signal event into the default loop', +'ev::iteration' => 'Return the number of times the default event loop has polled for new events', +'ev::now' => 'Returns the time when the last iteration of the default event loop has started', +'ev::nowUpdate' => 'Establishes the current time by querying the kernel, updating the time returned by Ev::now in the progress', +'ev::recommendedBackends' => 'Returns a bit mask of recommended backends for current platform', +'ev::resume' => 'Resume previously suspended default event loop', +'ev::run' => 'Begin checking for events and calling callbacks for the default loop', +'ev::sleep' => 'Block the process for the given number of seconds', +'ev::stop' => 'Stops the default event loop', +'ev::supportedBackends' => 'Returns the set of backends supported by current libev configuration', +'ev::suspend' => 'Suspend the default event loop', +'ev::time' => 'Returns the current time in fractional seconds since the epoch', +'ev::verify' => 'Performs internal consistency checks(for debugging)', +'eval' => 'Evaluate a string as PHP code', +'evcheck::__construct' => 'Constructs the EvCheck watcher object', +'EvCheck::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evcheck::createStopped' => 'Create instance of a stopped EvCheck watcher', +'EvCheck::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvCheck::getLoop' => 'Returns the loop responsible for the watcher.', +'EvCheck::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvCheck::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'EvCheck::setCallback' => 'Sets new callback for the watcher.', +'EvCheck::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvCheck::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evchild::__construct' => 'Constructs the EvChild watcher object', +'EvChild::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evchild::createStopped' => 'Create instance of a stopped EvCheck watcher', +'EvChild::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvChild::getLoop' => 'Returns the loop responsible for the watcher.', +'EvChild::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvChild::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'evchild::set' => 'Configures the watcher', +'EvChild::setCallback' => 'Sets new callback for the watcher.', +'EvChild::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvChild::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evembed::__construct' => 'Constructs the EvEmbed object', +'EvEmbed::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evembed::createStopped' => 'Create stopped EvEmbed watcher object', +'EvEmbed::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvEmbed::getLoop' => 'Returns the loop responsible for the watcher.', +'EvEmbed::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvEmbed::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'evembed::set' => 'Configures the watcher', +'EvEmbed::setCallback' => 'Sets new callback for the watcher.', +'EvEmbed::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvEmbed::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evembed::sweep' => 'Make a single, non-blocking sweep over the embedded loop', +'event::__construct' => 'Constructs Event object', +'event::add' => 'Makes event pending', +'event::addSignal' => 'Makes signal event pending', +'event::addTimer' => 'Makes timer event pending', +'event::del' => 'Makes event non-pending', +'event::delSignal' => 'Makes signal event non-pending', +'event::delTimer' => 'Makes timer event non-pending', +'event::free' => 'Make event non-pending and free resources allocated for this event', +'event::getSupportedMethods' => 'Returns array with of the names of the methods supported in this version of Libevent', +'event::pending' => 'Detects whether event is pending or scheduled', +'event::set' => 'Re-configures event', +'event::setPriority' => 'Set event priority', +'event::setTimer' => 'Re-configures timer event', +'event::signal' => 'Constructs signal event object', +'event::timer' => 'Constructs timer event object', +'event_add' => 'Add an event to the set of monitored events', +'event_base_free' => 'Destroy event base', +'event_base_loop' => 'Handle events', +'event_base_loopbreak' => 'Abort event loop', +'event_base_loopexit' => 'Exit loop after a time', +'event_base_new' => 'Create and initialize new event base', +'event_base_priority_init' => 'Set the number of event priority levels', +'event_base_reinit' => 'Reinitialize the event base after a fork', +'event_base_set' => 'Associate event base with an event', +'event_buffer_base_set' => 'Associate buffered event with an event base', +'event_buffer_disable' => 'Disable a buffered event', +'event_buffer_enable' => 'Enable a buffered event', +'event_buffer_fd_set' => 'Change a buffered event file descriptor', +'event_buffer_free' => 'Destroy buffered event', +'event_buffer_new' => 'Create new buffered event', +'event_buffer_priority_set' => 'Assign a priority to a buffered event', +'event_buffer_read' => 'Read data from a buffered event', +'event_buffer_set_callback' => 'Set or reset callbacks for a buffered event', +'event_buffer_timeout_set' => 'Set read and write timeouts for a buffered event', +'event_buffer_watermark_set' => 'Set the watermarks for read and write events', +'event_buffer_write' => 'Write data to a buffered event', +'event_del' => 'Remove an event from the set of monitored events', +'event_free' => 'Free event resource', +'event_new' => 'Create new event', +'event_priority_set' => 'Assign a priority to an event', +'event_set' => 'Prepare an event', +'event_timer_add' => 'Alias of event_add', +'event_timer_del' => 'Alias of event_del', +'event_timer_new' => 'Alias of event_new', +'event_timer_set' => 'Prepare a timer event', +'eventbase::__construct' => 'Constructs EventBase object', +'eventbase::dispatch' => 'Dispatch pending events', +'eventbase::exit' => 'Stop dispatching events', +'eventbase::free' => 'Free resources allocated for this event base', +'eventbase::getFeatures' => 'Returns bitmask of features supported', +'eventbase::getMethod' => 'Returns event method in use', +'eventbase::getTimeOfDayCached' => 'Returns the current event base time', +'eventbase::gotExit' => 'Checks if the event loop was told to exit', +'eventbase::gotStop' => 'Checks if the event loop was told to exit', +'eventbase::loop' => 'Dispatch pending events', +'eventbase::priorityInit' => 'Sets number of priorities per event base', +'eventbase::reInit' => 'Re-initialize event base(after a fork)', +'eventbase::stop' => 'Tells event_base to stop dispatching events', +'eventbuffer::__construct' => 'Constructs EventBuffer object', +'eventbuffer::add' => 'Append data to the end of an event buffer', +'eventbuffer::addBuffer' => 'Move all data from a buffer provided to the current instance of EventBuffer', +'eventbuffer::appendFrom' => 'Moves the specified number of bytes from a source buffer to the end of the current buffer', +'eventbuffer::copyout' => 'Copies out specified number of bytes from the front of the buffer', +'eventbuffer::drain' => 'Removes specified number of bytes from the front of the buffer without copying it anywhere', +'EventBuffer::enableLocking' => 'enableLocking.', +'eventbuffer::expand' => 'Reserves space in buffer', +'eventbuffer::freeze' => 'Prevent calls that modify an event buffer from succeeding', +'eventbuffer::lock' => 'Acquires a lock on buffer', +'eventbuffer::prepend' => 'Prepend data to the front of the buffer', +'eventbuffer::prependBuffer' => 'Moves all data from source buffer to the front of current buffer', +'eventbuffer::pullup' => 'Linearizes data within buffer and returns it\'s contents as a string', +'eventbuffer::read' => 'Read data from an evbuffer and drain the bytes read', +'eventbuffer::readFrom' => 'Read data from a file onto the end of the buffer', +'eventbuffer::readLine' => 'Extracts a line from the front of the buffer', +'eventbuffer::search' => 'Scans the buffer for an occurrence of a string', +'eventbuffer::searchEol' => 'Scans the buffer for an occurrence of an end of line', +'eventbuffer::substr' => 'Substracts a portion of the buffer data', +'eventbuffer::unfreeze' => 'Re-enable calls that modify an event buffer', +'eventbuffer::unlock' => 'Releases lock acquired by EventBuffer::lock', +'eventbuffer::write' => 'Write contents of the buffer to a file or socket', +'eventbufferevent::__construct' => 'Constructs EventBufferEvent object', +'eventbufferevent::close' => 'Closes file descriptor associated with the current buffer event', +'eventbufferevent::connect' => 'Connect buffer event\'s file descriptor to given address or UNIX socket', +'eventbufferevent::connectHost' => 'Connects to a hostname with optionally asynchronous DNS resolving', +'eventbufferevent::createPair' => 'Creates two buffer events connected to each other', +'eventbufferevent::disable' => 'Disable events read, write, or both on a buffer event', +'eventbufferevent::enable' => 'Enable events read, write, or both on a buffer event', +'eventbufferevent::free' => 'Free a buffer event', +'eventbufferevent::getDnsErrorString' => 'Returns string describing the last failed DNS lookup attempt', +'eventbufferevent::getEnabled' => 'Returns bitmask of events currently enabled on the buffer event', +'eventbufferevent::getInput' => 'Returns underlying input buffer associated with current buffer event', +'eventbufferevent::getOutput' => 'Returns underlying output buffer associated with current buffer event', +'eventbufferevent::read' => 'Read buffer\'s data', +'eventbufferevent::readBuffer' => 'Drains the entire contents of the input buffer and places them into buf', +'eventbufferevent::setCallbacks' => 'Assigns read, write and event(status) callbacks', +'eventbufferevent::setPriority' => 'Assign a priority to a bufferevent', +'eventbufferevent::setTimeouts' => 'Set the read and write timeout for a buffer event', +'eventbufferevent::setWatermark' => 'Adjusts read and/or write watermarks', +'eventbufferevent::sslError' => 'Returns most recent OpenSSL error reported on the buffer event', +'eventbufferevent::sslFilter' => 'Create a new SSL buffer event to send its data over another buffer event', +'eventbufferevent::sslGetCipherInfo' => 'Returns a textual description of the cipher', +'eventbufferevent::sslGetCipherName' => 'Returns the current cipher name of the SSL connection', +'eventbufferevent::sslGetCipherVersion' => 'Returns version of cipher used by current SSL connection', +'eventbufferevent::sslGetProtocol' => 'Returns the name of the protocol used for current SSL connection', +'eventbufferevent::sslRenegotiate' => 'Tells a bufferevent to begin SSL renegotiation', +'eventbufferevent::sslSocket' => 'Creates a new SSL buffer event to send its data over an SSL on a socket', +'eventbufferevent::write' => 'Adds data to a buffer event\'s output buffer', +'eventbufferevent::writeBuffer' => 'Adds contents of the entire buffer to a buffer event\'s output buffer', +'eventconfig::__construct' => 'Constructs EventConfig object', +'eventconfig::avoidMethod' => 'Tells libevent to avoid specific event method', +'eventconfig::requireFeatures' => 'Enters a required event method feature that the application demands', +'eventconfig::setMaxDispatchInterval' => 'Prevents priority inversion', +'eventdnsbase::__construct' => 'Constructs EventDnsBase object', +'eventdnsbase::addNameserverIp' => 'Adds a nameserver to the DNS base', +'eventdnsbase::addSearch' => 'Adds a domain to the list of search domains', +'eventdnsbase::clearSearch' => 'Removes all current search suffixes', +'eventdnsbase::countNameservers' => 'Gets the number of configured nameservers', +'eventdnsbase::loadHosts' => 'Loads a hosts file (in the same format as /etc/hosts) from hosts file', +'eventdnsbase::parseResolvConf' => 'Scans the resolv.conf-formatted file', +'eventdnsbase::setOption' => 'Set the value of a configuration option', +'eventdnsbase::setSearchNdots' => 'Set the \'ndots\' parameter for searches', +'eventhttp::__construct' => 'Constructs EventHttp object(the HTTP server)', +'eventhttp::accept' => 'Makes an HTTP server accept connections on the specified socket stream or resource', +'eventhttp::addServerAlias' => 'Adds a server alias to the HTTP server object', +'eventhttp::bind' => 'Binds an HTTP server on the specified address and port', +'eventhttp::removeServerAlias' => 'Removes server alias', +'eventhttp::setAllowedMethods' => 'Sets the what HTTP methods are supported in requests accepted by this server, and passed to user callbacks', +'eventhttp::setCallback' => 'Sets a callback for specified URI', +'eventhttp::setDefaultCallback' => 'Sets default callback to handle requests that are not caught by specific callbacks', +'eventhttp::setMaxBodySize' => 'Sets maximum request body size', +'eventhttp::setMaxHeadersSize' => 'Sets maximum HTTP header size', +'eventhttp::setTimeout' => 'Sets the timeout for an HTTP request', +'eventhttpconnection::__construct' => 'Constructs EventHttpConnection object', +'eventhttpconnection::getBase' => 'Returns event base associated with the connection', +'eventhttpconnection::getPeer' => 'Gets the remote address and port associated with the connection', +'eventhttpconnection::makeRequest' => 'Makes an HTTP request over the specified connection', +'eventhttpconnection::setCloseCallback' => 'Set callback for connection close', +'eventhttpconnection::setLocalAddress' => 'Sets the IP address from which HTTP connections are made', +'eventhttpconnection::setLocalPort' => 'Sets the local port from which connections are made', +'eventhttpconnection::setMaxBodySize' => 'Sets maximum body size for the connection', +'eventhttpconnection::setMaxHeadersSize' => 'Sets maximum header size', +'eventhttpconnection::setRetries' => 'Sets the retry limit for the connection', +'eventhttpconnection::setTimeout' => 'Sets the timeout for the connection', +'eventhttprequest::__construct' => 'Constructs EventHttpRequest object', +'eventhttprequest::addHeader' => 'Adds an HTTP header to the headers of the request', +'eventhttprequest::cancel' => 'Cancels a pending HTTP request', +'eventhttprequest::clearHeaders' => 'Removes all output headers from the header list of the request', +'eventhttprequest::closeConnection' => 'Closes associated HTTP connection', +'eventhttprequest::findHeader' => 'Finds the value belonging a header', +'eventhttprequest::free' => 'Frees the object and removes associated events', +'eventhttprequest::getBufferEvent' => 'Returns EventBufferEvent object', +'eventhttprequest::getCommand' => 'Returns the request command(method)', +'eventhttprequest::getConnection' => 'Returns EventHttpConnection object', +'eventhttprequest::getHost' => 'Returns the request host', +'eventhttprequest::getInputBuffer' => 'Returns the input buffer', +'eventhttprequest::getInputHeaders' => 'Returns associative array of the input headers', +'eventhttprequest::getOutputBuffer' => 'Returns the output buffer of the request', +'eventhttprequest::getOutputHeaders' => 'Returns associative array of the output headers', +'eventhttprequest::getResponseCode' => 'Returns the response code', +'eventhttprequest::getUri' => 'Returns the request URI', +'eventhttprequest::removeHeader' => 'Removes an HTTP header from the headers of the request', +'eventhttprequest::sendError' => 'Send an HTML error message to the client', +'eventhttprequest::sendReply' => 'Send an HTML reply to the client', +'eventhttprequest::sendReplyChunk' => 'Send another data chunk as part of an ongoing chunked reply', +'eventhttprequest::sendReplyEnd' => 'Complete a chunked reply, freeing the request as appropriate', +'eventhttprequest::sendReplyStart' => 'Initiate a chunked reply', +'eventlistener::__construct' => 'Creates new connection listener associated with an event base', +'eventlistener::disable' => 'Disables an event connect listener object', +'eventlistener::enable' => 'Enables an event connect listener object', +'eventlistener::getBase' => 'Returns event base associated with the event listener', +'eventlistener::getSocketName' => 'Retreives the current address to which the listener\'s socket is bound', +'eventlistener::setCallback' => 'The setCallback purpose', +'eventlistener::setErrorCallback' => 'Set event listener\'s error callback', +'eventsslcontext::__construct' => 'Constructs an OpenSSL context for use with Event classes', +'eventutil::__construct' => 'The abstract constructor', +'eventutil::getLastSocketErrno' => 'Returns the most recent socket error number', +'eventutil::getLastSocketError' => 'Returns the most recent socket error', +'eventutil::getSocketFd' => 'Returns numeric file descriptor of a socket, or stream', +'eventutil::getSocketName' => 'Retreives the current address to which the socket is bound', +'eventutil::setSocketOption' => 'Sets socket options', +'eventutil::sslRandPoll' => 'Generates entropy by means of OpenSSL\'s RAND_poll()', +'evfork::__construct' => 'Constructs the EvFork watcher object', +'EvFork::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evfork::createStopped' => 'Creates a stopped instance of EvFork watcher class', +'EvFork::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvFork::getLoop' => 'Returns the loop responsible for the watcher.', +'EvFork::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvFork::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'EvFork::setCallback' => 'Sets new callback for the watcher.', +'EvFork::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvFork::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evidle::__construct' => 'Constructs the EvIdle watcher object', +'EvIdle::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evidle::createStopped' => 'Creates instance of a stopped EvIdle watcher object', +'EvIdle::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvIdle::getLoop' => 'Returns the loop responsible for the watcher.', +'EvIdle::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvIdle::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'EvIdle::setCallback' => 'Sets new callback for the watcher.', +'EvIdle::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvIdle::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evio::__construct' => 'Constructs EvIo watcher object', +'EvIo::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evio::createStopped' => 'Create stopped EvIo watcher object', +'EvIo::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvIo::getLoop' => 'Returns the loop responsible for the watcher.', +'EvIo::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvIo::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'evio::set' => 'Configures the watcher', +'EvIo::setCallback' => 'Sets new callback for the watcher.', +'EvIo::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvIo::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evloop::__construct' => 'Constructs the event loop object', +'evloop::backend' => 'Returns an integer describing the backend used by libev', +'evloop::check' => 'Creates EvCheck object associated with the current event loop instance', +'evloop::child' => 'Creates EvChild object associated with the current event loop', +'evloop::defaultLoop' => 'Returns or creates the default event loop', +'evloop::embed' => 'Creates an instance of EvEmbed watcher associated with the current EvLoop object', +'evloop::fork' => 'Creates EvFork watcher object associated with the current event loop instance', +'evloop::idle' => 'Creates EvIdle watcher object associated with the current event loop instance', +'evloop::invokePending' => 'Invoke all pending watchers while resetting their pending state', +'evloop::io' => 'Create EvIo watcher object associated with the current event loop instance', +'evloop::loopFork' => 'Must be called after a fork', +'evloop::now' => 'Returns the current "event loop time"', +'evloop::nowUpdate' => 'Establishes the current time by querying the kernel, updating the time returned by EvLoop::now in the progress', +'evloop::periodic' => 'Creates EvPeriodic watcher object associated with the current event loop instance', +'evloop::prepare' => 'Creates EvPrepare watcher object associated with the current event loop instance', +'evloop::resume' => 'Resume previously suspended default event loop', +'evloop::run' => 'Begin checking for events and calling callbacks for the loop', +'evloop::signal' => 'Creates EvSignal watcher object associated with the current event loop instance', +'evloop::stat' => 'Creates EvStat watcher object associated with the current event loop instance', +'evloop::stop' => 'Stops the event loop', +'evloop::suspend' => 'Suspend the loop', +'evloop::timer' => 'Creates EvTimer watcher object associated with the current event loop instance', +'evloop::verify' => 'Performs internal consistency checks(for debugging)', +'evperiodic::__construct' => 'Constructs EvPeriodic watcher object', +'evperiodic::again' => 'Simply stops and restarts the periodic watcher again', +'evperiodic::at' => 'Returns the absolute time that this watcher is supposed to trigger next', +'EvPeriodic::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evperiodic::createStopped' => 'Create a stopped EvPeriodic watcher', +'EvPeriodic::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvPeriodic::getLoop' => 'Returns the loop responsible for the watcher.', +'EvPeriodic::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvPeriodic::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'evperiodic::set' => 'Configures the watcher', +'EvPeriodic::setCallback' => 'Sets new callback for the watcher.', +'EvPeriodic::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvPeriodic::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evprepare::__construct' => 'Constructs EvPrepare watcher object', +'EvPrepare::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evprepare::createStopped' => 'Creates a stopped instance of EvPrepare watcher', +'EvPrepare::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvPrepare::getLoop' => 'Returns the loop responsible for the watcher.', +'EvPrepare::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvPrepare::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'EvPrepare::setCallback' => 'Sets new callback for the watcher.', +'EvPrepare::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvPrepare::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evsignal::__construct' => 'Constructs EvSignal watcher object', +'EvSignal::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evsignal::createStopped' => 'Create stopped EvSignal watcher object', +'EvSignal::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvSignal::getLoop' => 'Returns the loop responsible for the watcher.', +'EvSignal::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvSignal::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'evsignal::set' => 'Configures the watcher', +'EvSignal::setCallback' => 'Sets new callback for the watcher.', +'EvSignal::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvSignal::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evstat::__construct' => 'Constructs EvStat watcher object', +'evstat::attr' => 'Returns the values most recently detected by Ev', +'EvStat::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evstat::createStopped' => 'Create a stopped EvStat watcher object', +'EvStat::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvStat::getLoop' => 'Returns the loop responsible for the watcher.', +'EvStat::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvStat::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'evstat::prev' => 'Returns the previous set of values returned by EvStat::attr', +'evstat::set' => 'Configures the watcher', +'EvStat::setCallback' => 'Sets new callback for the watcher.', +'EvStat::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'evstat::stat' => 'Initiates the stat call', +'EvStat::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evtimer::__construct' => 'Constructs an EvTimer watcher object', +'evtimer::again' => 'Restarts the timer watcher', +'EvTimer::clear' => 'Clear watcher pending status. + +If the watcher is pending, this method clears its pending status and returns its revents bitset (as if its +callback was invoked). If the watcher isn\'t pending it does nothing and returns 0. + +Sometimes it can be useful to "poll" a watcher instead of waiting for its callback to be invoked, which can be +accomplished with this function.', +'evtimer::createStopped' => 'Creates EvTimer stopped watcher object', +'EvTimer::feed' => 'Feeds the given revents set into the event loop. + +Feeds the given revents set into the event loop, as if the specified event had happened for the watcher.', +'EvTimer::getLoop' => 'Returns the loop responsible for the watcher.', +'EvTimer::invoke' => 'Invokes the watcher callback with the given received events bit mask.', +'EvTimer::keepAlive' => 'Configures whether to keep the loop from returning. + +Configures whether to keep the loop from returning. With keepalive value set to FALSE the watcher won\'t keep +Ev::run() / EvLoop::run() from returning even though the watcher is active. + +Watchers have keepalive value TRUE by default. + +Clearing keepalive status is useful when returning from Ev::run() / EvLoop::run() just because of the watcher +is undesirable. It could be a long running UDP socket watcher or so.', +'evtimer::set' => 'Configures the watcher', +'EvTimer::setCallback' => 'Sets new callback for the watcher.', +'EvTimer::start' => 'Starts the watcher. + +Marks the watcher as active. Note that only active watchers will receive events.', +'EvTimer::stop' => 'Stops the watcher. + +Marks the watcher as inactive. Note that only active watchers will receive events.', +'evwatcher::__construct' => 'Abstract constructor of a watcher object', +'evwatcher::clear' => 'Clear watcher pending status', +'evwatcher::feed' => 'Feeds the given revents set into the event loop', +'evwatcher::getLoop' => 'Returns the loop responsible for the watcher', +'evwatcher::invoke' => 'Invokes the watcher callback with the given received events bit mask', +'evwatcher::keepalive' => 'Configures whether to keep the loop from returning', +'evwatcher::setCallback' => 'Sets new callback for the watcher', +'evwatcher::start' => 'Starts the watcher', +'evwatcher::stop' => 'Stops the watcher', +'Exception::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'Exception::__construct' => 'Construct the exception. Note: The message is NOT binary safe.', +'Exception::__toString' => 'String representation of the exception', +'Exception::getCode' => 'Gets the Exception code', +'Exception::getFile' => 'Gets the file in which the exception occurred', +'Exception::getLine' => 'Gets the line in which the exception occurred', +'Exception::getMessage' => 'Gets the Exception message', +'Exception::getPrevious' => 'Returns previous Exception', +'Exception::getTrace' => 'Gets the stack trace', +'Exception::getTraceAsString' => 'Gets the stack trace as a string', +'exec' => 'Execute an external program', +'exif_imagetype' => 'Determine the type of an image', +'exif_read_data' => 'Reads the EXIF headers from an image file', +'exif_tagname' => 'Get the header name for an index', +'exif_thumbnail' => 'Retrieve the embedded thumbnail of an image', +'exit' => 'Output a message and terminate the current script', +'exp' => 'Calculates the exponent of e', +'expect_expectl' => 'Waits until the output from a process matches one of the patterns, a specified time period has passed, or an EOF is seen', +'expect_popen' => 'Execute command via Bourne shell, and open the PTY stream to the process', +'explode' => 'Split a string by a string', +'expm1' => 'Returns exp(number) - 1, computed in a way that is accurate even when the value of number is close to zero', +'expression' => 'Bind prepared statement variables as parameters', +'extension_loaded' => 'Find out whether an extension is loaded', +'extract' => 'Import variables into the current symbol table from an array', +'ezmlm_hash' => 'Calculate the hash value needed by EZMLM', +'fam_cancel_monitor' => 'Terminate monitoring', +'fam_close' => 'Close FAM connection', +'fam_monitor_collection' => 'Monitor a collection of files in a directory for changes', +'fam_monitor_directory' => 'Monitor a directory for changes', +'fam_monitor_file' => 'Monitor a regular file for changes', +'fam_next_event' => 'Get next pending FAM event', +'fam_open' => 'Open connection to FAM daemon', +'fam_pending' => 'Check for pending FAM events', +'fam_resume_monitor' => 'Resume suspended monitoring', +'fam_suspend_monitor' => 'Temporarily suspend monitoring', +'fann_cascadetrain_on_data' => 'Trains on an entire dataset, for a period of time using the Cascade2 training algorithm', +'fann_cascadetrain_on_file' => 'Trains on an entire dataset read from file, for a period of time using the Cascade2 training algorithm', +'fann_clear_scaling_params' => 'Clears scaling parameters', +'fann_copy' => 'Creates a copy of a fann structure', +'fann_create_from_file' => 'Constructs a backpropagation neural network from a configuration file', +'fann_create_shortcut' => 'Creates a standard backpropagation neural network which is not fully connectected and has shortcut connections', +'fann_create_shortcut_array' => 'Creates a standard backpropagation neural network which is not fully connectected and has shortcut connections', +'fann_create_sparse' => 'Creates a standard backpropagation neural network, which is not fully connected', +'fann_create_sparse_array' => 'Creates a standard backpropagation neural network, which is not fully connected using an array of layer sizes', +'fann_create_standard' => 'Creates a standard fully connected backpropagation neural network', +'fann_create_standard_array' => 'Creates a standard fully connected backpropagation neural network using an array of layer sizes', +'fann_create_train' => 'Creates an empty training data struct', +'fann_create_train_from_callback' => 'Creates the training data struct from a user supplied function', +'fann_descale_input' => 'Scale data in input vector after get it from ann based on previously calculated parameters', +'fann_descale_output' => 'Scale data in output vector after get it from ann based on previously calculated parameters', +'fann_descale_train' => 'Descale input and output data based on previously calculated parameters', +'fann_destroy' => 'Destroys the entire network and properly freeing all the associated memory', +'fann_destroy_train' => 'Destructs the training data', +'fann_duplicate_train_data' => 'Returns an exact copy of a fann train data', +'fann_get_activation_function' => 'Returns the activation function', +'fann_get_activation_steepness' => 'Returns the activation steepness for supplied neuron and layer number', +'fann_get_bias_array' => 'Get the number of bias in each layer in the network', +'fann_get_bit_fail' => 'The number of fail bits', +'fann_get_bit_fail_limit' => 'Returns the bit fail limit used during training', +'fann_get_cascade_activation_functions' => 'Returns the cascade activation functions', +'fann_get_cascade_activation_functions_count' => 'Returns the number of cascade activation functions', +'fann_get_cascade_activation_steepnesses' => 'Returns the cascade activation steepnesses', +'fann_get_cascade_activation_steepnesses_count' => 'The number of activation steepnesses', +'fann_get_cascade_candidate_change_fraction' => 'Returns the cascade candidate change fraction', +'fann_get_cascade_candidate_limit' => 'Return the candidate limit', +'fann_get_cascade_candidate_stagnation_epochs' => 'Returns the number of cascade candidate stagnation epochs', +'fann_get_cascade_max_cand_epochs' => 'Returns the maximum candidate epochs', +'fann_get_cascade_max_out_epochs' => 'Returns the maximum out epochs', +'fann_get_cascade_min_cand_epochs' => 'Returns the minimum candidate epochs', +'fann_get_cascade_min_out_epochs' => 'Returns the minimum out epochs', +'fann_get_cascade_num_candidate_groups' => 'Returns the number of candidate groups', +'fann_get_cascade_num_candidates' => 'Returns the number of candidates used during training', +'fann_get_cascade_output_change_fraction' => 'Returns the cascade output change fraction', +'fann_get_cascade_output_stagnation_epochs' => 'Returns the number of cascade output stagnation epochs', +'fann_get_cascade_weight_multiplier' => 'Returns the weight multiplier', +'fann_get_connection_array' => 'Get connections in the network', +'fann_get_connection_rate' => 'Get the connection rate used when the network was created', +'fann_get_errno' => 'Returns the last error number', +'fann_get_errstr' => 'Returns the last errstr', +'fann_get_layer_array' => 'Get the number of neurons in each layer in the network', +'fann_get_learning_momentum' => 'Returns the learning momentum', +'fann_get_learning_rate' => 'Returns the learning rate', +'fann_get_mse' => 'Reads the mean square error from the network', +'fann_get_network_type' => 'Get the type of neural network it was created as', +'fann_get_num_input' => 'Get the number of input neurons', +'fann_get_num_layers' => 'Get the number of layers in the neural network', +'fann_get_num_output' => 'Get the number of output neurons', +'fann_get_quickprop_decay' => 'Returns the decay which is a factor that weights should decrease in each iteration during quickprop training', +'fann_get_quickprop_mu' => 'Returns the mu factor', +'fann_get_rprop_decrease_factor' => 'Returns the increase factor used during RPROP training', +'fann_get_rprop_delta_max' => 'Returns the maximum step-size', +'fann_get_rprop_delta_min' => 'Returns the minimum step-size', +'fann_get_rprop_delta_zero' => 'Returns the initial step-size', +'fann_get_rprop_increase_factor' => 'Returns the increase factor used during RPROP training', +'fann_get_sarprop_step_error_shift' => 'Returns the sarprop step error shift', +'fann_get_sarprop_step_error_threshold_factor' => 'Returns the sarprop step error threshold factor', +'fann_get_sarprop_temperature' => 'Returns the sarprop temperature', +'fann_get_sarprop_weight_decay_shift' => 'Returns the sarprop weight decay shift', +'fann_get_total_connections' => 'Get the total number of connections in the entire network', +'fann_get_total_neurons' => 'Get the total number of neurons in the entire network', +'fann_get_train_error_function' => 'Returns the error function used during training', +'fann_get_train_stop_function' => 'Returns the stop function used during training', +'fann_get_training_algorithm' => 'Returns the training algorithm', +'fann_init_weights' => 'Initialize the weights using Widrow + Nguyen’s algorithm', +'fann_length_train_data' => 'Returns the number of training patterns in the train data', +'fann_merge_train_data' => 'Merges the train data', +'fann_num_input_train_data' => 'Returns the number of inputs in each of the training patterns in the train data', +'fann_num_output_train_data' => 'Returns the number of outputs in each of the training patterns in the train data', +'fann_print_error' => 'Prints the error string', +'fann_randomize_weights' => 'Give each connection a random weight between min_weight and max_weight', +'fann_read_train_from_file' => 'Reads a file that stores training data', +'fann_reset_errno' => 'Resets the last error number', +'fann_reset_errstr' => 'Resets the last error string', +'fann_reset_mse' => 'Resets the mean square error from the network', +'fann_run' => 'Will run input through the neural network', +'fann_save' => 'Saves the entire network to a configuration file', +'fann_save_train' => 'Save the training structure to a file', +'fann_scale_input' => 'Scale data in input vector before feed it to ann based on previously calculated parameters', +'fann_scale_input_train_data' => 'Scales the inputs in the training data to the specified range', +'fann_scale_output' => 'Scale data in output vector before feed it to ann based on previously calculated parameters', +'fann_scale_output_train_data' => 'Scales the outputs in the training data to the specified range', +'fann_scale_train' => 'Scale input and output data based on previously calculated parameters', +'fann_scale_train_data' => 'Scales the inputs and outputs in the training data to the specified range', +'fann_set_activation_function' => 'Sets the activation function for supplied neuron and layer', +'fann_set_activation_function_hidden' => 'Sets the activation function for all of the hidden layers', +'fann_set_activation_function_layer' => 'Sets the activation function for all the neurons in the supplied layer', +'fann_set_activation_function_output' => 'Sets the activation function for the output layer', +'fann_set_activation_steepness' => 'Sets the activation steepness for supplied neuron and layer number', +'fann_set_activation_steepness_hidden' => 'Sets the steepness of the activation steepness for all neurons in the all hidden layers', +'fann_set_activation_steepness_layer' => 'Sets the activation steepness for all of the neurons in the supplied layer number', +'fann_set_activation_steepness_output' => 'Sets the steepness of the activation steepness in the output layer', +'fann_set_bit_fail_limit' => 'Set the bit fail limit used during training', +'fann_set_callback' => 'Sets the callback function for use during training', +'fann_set_cascade_activation_functions' => 'Sets the array of cascade candidate activation functions', +'fann_set_cascade_activation_steepnesses' => 'Sets the array of cascade candidate activation steepnesses', +'fann_set_cascade_candidate_change_fraction' => 'Sets the cascade candidate change fraction', +'fann_set_cascade_candidate_limit' => 'Sets the candidate limit', +'fann_set_cascade_candidate_stagnation_epochs' => 'Sets the number of cascade candidate stagnation epochs', +'fann_set_cascade_max_cand_epochs' => 'Sets the max candidate epochs', +'fann_set_cascade_max_out_epochs' => 'Sets the maximum out epochs', +'fann_set_cascade_min_cand_epochs' => 'Sets the min candidate epochs', +'fann_set_cascade_min_out_epochs' => 'Sets the minimum out epochs', +'fann_set_cascade_num_candidate_groups' => 'Sets the number of candidate groups', +'fann_set_cascade_output_change_fraction' => 'Sets the cascade output change fraction', +'fann_set_cascade_output_stagnation_epochs' => 'Sets the number of cascade output stagnation epochs', +'fann_set_cascade_weight_multiplier' => 'Sets the weight multiplier', +'fann_set_error_log' => 'Sets where the errors are logged to', +'fann_set_input_scaling_params' => 'Calculate input scaling parameters for future use based on training data', +'fann_set_learning_momentum' => 'Sets the learning momentum', +'fann_set_learning_rate' => 'Sets the learning rate', +'fann_set_output_scaling_params' => 'Calculate output scaling parameters for future use based on training data', +'fann_set_quickprop_decay' => 'Sets the quickprop decay factor', +'fann_set_quickprop_mu' => 'Sets the quickprop mu factor', +'fann_set_rprop_decrease_factor' => 'Sets the decrease factor used during RPROP training', +'fann_set_rprop_delta_max' => 'Sets the maximum step-size', +'fann_set_rprop_delta_min' => 'Sets the minimum step-size', +'fann_set_rprop_delta_zero' => 'Sets the initial step-size', +'fann_set_rprop_increase_factor' => 'Sets the increase factor used during RPROP training', +'fann_set_sarprop_step_error_shift' => 'Sets the sarprop step error shift', +'fann_set_sarprop_step_error_threshold_factor' => 'Sets the sarprop step error threshold factor', +'fann_set_sarprop_temperature' => 'Sets the sarprop temperature', +'fann_set_sarprop_weight_decay_shift' => 'Sets the sarprop weight decay shift', +'fann_set_scaling_params' => 'Calculate input and output scaling parameters for future use based on training data', +'fann_set_train_error_function' => 'Sets the error function used during training', +'fann_set_train_stop_function' => 'Sets the stop function used during training', +'fann_set_training_algorithm' => 'Sets the training algorithm', +'fann_set_weight' => 'Set a connection in the network', +'fann_set_weight_array' => 'Set connections in the network', +'fann_shuffle_train_data' => 'Shuffles training data, randomizing the order', +'fann_subset_train_data' => 'Returns an copy of a subset of the train data', +'fann_test' => 'Test with a set of inputs, and a set of desired outputs', +'fann_test_data' => 'Test a set of training data and calculates the MSE for the training data', +'fann_train' => 'Train one iteration with a set of inputs, and a set of desired outputs', +'fann_train_epoch' => 'Train one epoch with a set of training data', +'fann_train_on_data' => 'Trains on an entire dataset for a period of time', +'fann_train_on_file' => 'Trains on an entire dataset, which is read from file, for a period of time', +'fannconnection::__construct' => 'The connection constructor', +'fannconnection::getFromNeuron' => 'Returns the positions of starting neuron', +'fannconnection::getToNeuron' => 'Returns the positions of terminating neuron', +'fannconnection::getWeight' => 'Returns the connection weight', +'fannconnection::setWeight' => 'Sets the connections weight', +'fastcgi_finish_request' => 'Flushes all response data to the client', +'fbird_add_user' => 'Alias of ibase_add_user', +'fbird_affected_rows' => 'Alias of ibase_affected_rows', +'fbird_backup' => 'Alias of ibase_backup', +'fbird_blob_add' => 'Alias of ibase_blob_add', +'fbird_blob_cancel' => 'Cancel creating blob', +'fbird_blob_close' => 'Alias of ibase_blob_close', +'fbird_blob_create' => 'Alias of ibase_blob_create', +'fbird_blob_echo' => 'Alias of ibase_blob_echo', +'fbird_blob_get' => 'Alias of ibase_blob_get', +'fbird_blob_import' => 'Alias of ibase_blob_import', +'fbird_blob_info' => 'Alias of ibase_blob_info', +'fbird_blob_open' => 'Alias of ibase_blob_open', +'fbird_close' => 'Alias of ibase_close', +'fbird_commit' => 'Alias of ibase_commit', +'fbird_commit_ret' => 'Alias of ibase_commit_ret', +'fbird_connect' => 'Alias of ibase_connect', +'fbird_db_info' => 'Alias of ibase_db_info', +'fbird_delete_user' => 'Alias of ibase_delete_user', +'fbird_drop_db' => 'Alias of ibase_drop_db', +'fbird_errcode' => 'Alias of ibase_errcode', +'fbird_errmsg' => 'Alias of ibase_errmsg', +'fbird_execute' => 'Alias of ibase_execute', +'fbird_fetch_assoc' => 'Alias of ibase_fetch_assoc', +'fbird_fetch_object' => 'Alias of ibase_fetch_object', +'fbird_fetch_row' => 'Alias of ibase_fetch_row', +'fbird_field_info' => 'Alias of ibase_field_info', +'fbird_free_event_handler' => 'Alias of ibase_free_event_handler', +'fbird_free_query' => 'Alias of ibase_free_query', +'fbird_free_result' => 'Alias of ibase_free_result', +'fbird_gen_id' => 'Alias of ibase_gen_id', +'fbird_maintain_db' => 'Alias of ibase_maintain_db', +'fbird_modify_user' => 'Alias of ibase_modify_user', +'fbird_name_result' => 'Alias of ibase_name_result', +'fbird_num_fields' => 'Alias of ibase_num_fields', +'fbird_num_params' => 'Alias of ibase_num_params', +'fbird_param_info' => 'Alias of ibase_param_info', +'fbird_pconnect' => 'Alias of ibase_pconnect', +'fbird_prepare' => 'Alias of ibase_prepare', +'fbird_query' => 'Alias of ibase_query', +'fbird_restore' => 'Alias of ibase_restore', +'fbird_rollback' => 'Alias of ibase_rollback', +'fbird_rollback_ret' => 'Alias of ibase_rollback_ret', +'fbird_server_info' => 'Alias of ibase_server_info', +'fbird_service_attach' => 'Alias of ibase_service_attach', +'fbird_service_detach' => 'Alias of ibase_service_detach', +'fbird_set_event_handler' => 'Alias of ibase_set_event_handler', +'fbird_trans' => 'Alias of ibase_trans', +'fbird_wait_event' => 'Alias of ibase_wait_event', +'fbsql_affected_rows' => 'Get number of affected rows in previous FrontBase operation', +'fbsql_autocommit' => 'Enable or disable autocommit', +'fbsql_blob_size' => 'Get the size of a BLOB', +'fbsql_change_user' => 'Change logged in user of the active connection', +'fbsql_clob_size' => 'Get the size of a CLOB', +'fbsql_close' => 'Close FrontBase connection', +'fbsql_commit' => 'Commits a transaction to the database', +'fbsql_connect' => 'Open a connection to a FrontBase Server', +'fbsql_create_blob' => 'Create a BLOB', +'fbsql_create_clob' => 'Create a CLOB', +'fbsql_create_db' => 'Create a FrontBase database', +'fbsql_data_seek' => 'Move internal result pointer', +'fbsql_database' => 'Get or set the database name used with a connection', +'fbsql_database_password' => 'Sets or retrieves the password for a FrontBase database', +'fbsql_db_query' => 'Send a FrontBase query', +'fbsql_db_status' => 'Get the status for a given database', +'fbsql_drop_db' => 'Drop (delete) a FrontBase database', +'fbsql_errno' => 'Returns the error number from previous operation', +'fbsql_error' => 'Returns the error message from previous operation', +'fbsql_fetch_array' => 'Fetch a result row as an associative array, a numeric array, or both', +'fbsql_fetch_assoc' => 'Fetch a result row as an associative array', +'fbsql_fetch_field' => 'Get column information from a result and return as an object', +'fbsql_fetch_lengths' => 'Get the length of each output in a result', +'fbsql_fetch_object' => 'Fetch a result row as an object', +'fbsql_fetch_row' => 'Get a result row as an enumerated array', +'fbsql_field_flags' => 'Get the flags associated with the specified field in a result', +'fbsql_field_len' => 'Returns the length of the specified field', +'fbsql_field_name' => 'Get the name of the specified field in a result', +'fbsql_field_seek' => 'Set result pointer to a specified field offset', +'fbsql_field_table' => 'Get name of the table the specified field is in', +'fbsql_field_type' => 'Get the type of the specified field in a result', +'fbsql_free_result' => 'Free result memory', +'fbsql_hostname' => 'Get or set the host name used with a connection', +'fbsql_insert_id' => 'Get the id generated from the previous INSERT operation', +'fbsql_list_dbs' => 'List databases available on a FrontBase server', +'fbsql_list_fields' => 'List FrontBase result fields', +'fbsql_list_tables' => 'List tables in a FrontBase database', +'fbsql_next_result' => 'Move the internal result pointer to the next result', +'fbsql_num_fields' => 'Get number of fields in result', +'fbsql_num_rows' => 'Get number of rows in result', +'fbsql_password' => 'Get or set the user password used with a connection', +'fbsql_pconnect' => 'Open a persistent connection to a FrontBase Server', +'fbsql_query' => 'Send a FrontBase query', +'fbsql_read_blob' => 'Read a BLOB from the database', +'fbsql_read_clob' => 'Read a CLOB from the database', +'fbsql_result' => 'Get result data', +'fbsql_rollback' => 'Rollback a transaction to the database', +'fbsql_rows_fetched' => 'Get the number of rows affected by the last statement', +'fbsql_select_db' => 'Select a FrontBase database', +'fbsql_set_characterset' => 'Change input/output character set', +'fbsql_set_lob_mode' => 'Set the LOB retrieve mode for a FrontBase result set', +'fbsql_set_password' => 'Change the password for a given user', +'fbsql_set_transaction' => 'Set the transaction locking and isolation', +'fbsql_start_db' => 'Start a database on local or remote server', +'fbsql_stop_db' => 'Stop a database on local or remote server', +'fbsql_table_name' => 'Get table name of field', +'fbsql_tablename' => 'Alias of fbsql_table_name', +'fbsql_username' => 'Get or set the username for the connection', +'fbsql_warnings' => 'Enable or disable FrontBase warnings', +'fclose' => 'Closes an open file pointer', +'fdf_add_doc_javascript' => 'Adds javascript code to the FDF document', +'fdf_add_template' => 'Adds a template into the FDF document', +'fdf_close' => 'Close an FDF document', +'fdf_create' => 'Create a new FDF document', +'fdf_enum_values' => 'Call a user defined function for each document value', +'fdf_errno' => 'Return error code for last fdf operation', +'fdf_error' => 'Return error description for FDF error code', +'fdf_get_ap' => 'Get the appearance of a field', +'fdf_get_attachment' => 'Extracts uploaded file embedded in the FDF', +'fdf_get_encoding' => 'Get the value of the /Encoding key', +'fdf_get_file' => 'Get the value of the /F key', +'fdf_get_flags' => 'Gets the flags of a field', +'fdf_get_opt' => 'Gets a value from the opt array of a field', +'fdf_get_status' => 'Get the value of the /STATUS key', +'fdf_get_value' => 'Get the value of a field', +'fdf_get_version' => 'Gets version number for FDF API or file', +'fdf_header' => 'Sets FDF-specific output headers', +'fdf_next_field_name' => 'Get the next field name', +'fdf_open' => 'Open a FDF document', +'fdf_open_string' => 'Read a FDF document from a string', +'fdf_remove_item' => 'Sets target frame for form', +'fdf_save' => 'Save a FDF document', +'fdf_save_string' => 'Returns the FDF document as a string', +'fdf_set_ap' => 'Set the appearance of a field', +'fdf_set_encoding' => 'Sets FDF character encoding', +'fdf_set_file' => 'Set PDF document to display FDF data in', +'fdf_set_flags' => 'Sets a flag of a field', +'fdf_set_javascript_action' => 'Sets an javascript action of a field', +'fdf_set_on_import_javascript' => 'Adds javascript code to be executed when Acrobat opens the FDF', +'fdf_set_opt' => 'Sets an option of a field', +'fdf_set_status' => 'Set the value of the /STATUS key', +'fdf_set_submit_form_action' => 'Sets a submit form action of a field', +'fdf_set_target_frame' => 'Set target frame for form display', +'fdf_set_value' => 'Set the value of a field', +'fdf_set_version' => 'Sets version number for a FDF file', +'feof' => 'Tests for end-of-file on a file pointer', +'FFI::addr' => 'Returns C pointer to the given C data structure. The pointer is +not "owned" and won\'t be free. Anyway, this is a potentially +unsafe operation, because the life-time of the returned pointer +may be longer than life-time of the source object, and this may +cause dangling pointer dereference (like in regular C).', +'FFI::alignof' => 'Returns size of C data type of the given FFI\CData or FFI\CType.', +'FFI::arrayType' => 'Constructs a new C array type with elements of $type and +dimensions specified by $dimensions.', +'FFI::cast' => 'Casts given $pointer to another C type, specified by C declaration +string or FFI\CType object. + +This function may be called statically and use only predefined +types, or as a method of previously created FFI object. In last +case the first argument may reuse all type and tag names +defined in FFI::cdef().', +'FFI::cdef' => 'The method creates a binding on the existing C function. + +All variables and functions defined by first arguments are bound +to corresponding native symbols in DSO library and then may be +accessed as FFI object methods and properties. C types of argument, +return value and variables are automatically converted to/from PHP +types (if possible). Otherwise, they are wrapped in a special CData +proxy object and may be accessed by elements.', +'FFI::free' => 'Manually removes previously created "not-owned" data structure.', +'ffi::isNull' => 'Checks whether a FFI\CData is a null pointer', +'FFI::load' => '

Instead of embedding of a long C definition into PHP string, +and creating FFI through FFI::cdef(), it\'s possible to separate +it into a C header file. Note, that C preprocessor directives +(e.g. #define or #ifdef) are not supported. And only a couple of +special macros may be used especially for FFI.

+ + + #define FFI_LIB "libc.so.6" + + int printf(const char *format, ...); + + +Here, FFI_LIB specifies, that the given library should be loaded. + + + $ffi = FFI::load(__DIR__ . "/printf.h"); + $ffi->printf("Hello world!\n"); +', +'FFI::memcmp' => 'Compares $size bytes from memory area $a and $b.', +'FFI::memcpy' => 'Copies $size bytes from memory area $source to memory area $target. +$source may be any native data structure (FFI\CData) or PHP string.', +'FFI::memset' => 'Fills the $size bytes of the memory area pointed to by $target with +the constant byte $byte.', +'FFI::new' => 'Method that creates an arbitrary C structure.', +'FFI::scope' => 'FFI definition parsing and shared library loading may take +significant time. It\'s not useful to do it on each HTTP request in +WEB environment. However, it\'s possible to pre-load FFI definitions +and libraries at php startup, and instantiate FFI objects when +necessary. Header files may be extended with FFI_SCOPE define +(default pre-loading scope is "C"). This name is going to be +used as FFI::scope() argument. It\'s possible to pre-load few +files into a single scope. + + + #define FFI_LIB "libc.so.6" + #define FFI_SCOPE "libc" + + int printf(const char *format, ...); + + +These files are loaded through the same FFI::load() load function, +executed from file loaded by opcache.preload php.ini directive. + + + ffi.preload=/etc/php/ffi/printf.h + + +Finally, FFI::scope() instantiate an FFI object, that implements +all C definition from the given scope. + + + $ffi = FFI::scope("libc"); + $ffi->printf("Hello world!\n"); +', +'FFI::sizeof' => 'Returns size of C data type of the given FFI\CData or FFI\CType.', +'FFI::string' => 'Creates a PHP string from $size bytes of memory area pointed by +$source. If size is omitted, $source must be zero terminated +array of C chars.', +'FFI::type' => 'This function creates and returns a FFI\CType object, representng +type of the given C type declaration string. + +FFI::type() may be called statically and use only predefined types, +or as a method of previously created FFI object. In last case the +first argument may reuse all type and tag names defined in +FFI::cdef().', +'FFI::typeof' => 'This function returns a FFI\CType object, representing the type of +the given FFI\CData object.', +'fflush' => 'Flushes the output to a file', +'ffmpeg_animated_gif::addFrame' => 'Add a frame to the end of the animated gif.', +'ffmpeg_frame::__construct' => 'NOTE: This function will not be available if GD is not enabled.', +'ffmpeg_frame::crop' => 'Crop the frame.', +'ffmpeg_frame::getHeight' => 'Return the height of the frame.', +'ffmpeg_frame::getPresentationTimestamp' => 'Return the presentation time stamp of the frame.', +'ffmpeg_frame::getPTS' => 'Return the presentation time stamp of the frame.', +'ffmpeg_frame::getWidth' => 'Return the width of the frame.', +'ffmpeg_frame::resize' => 'Resize and optionally crop the frame. (Cropping is built into ffmpeg resizing so I\'m providing it here for completeness.)', +'ffmpeg_frame::toGDImage' => 'Returns a truecolor GD image of the frame. +NOTE: This function will not be available if GD is not enabled.', +'ffmpeg_movie::__construct' => 'Open a video or audio file and return it as an object.', +'ffmpeg_movie::getArtist' => 'Return the author field from the movie or the artist ID3 field from an mp3 file.', +'ffmpeg_movie::getAudioBitRate' => 'Return the audio bit rate of the media file in bits per second.', +'ffmpeg_movie::getAudioChannels' => 'Return the number of audio channels in this movie as an integer.', +'ffmpeg_movie::getAudioCodec' => 'Return the name of the audio codec used to encode this movie as a string.', +'ffmpeg_movie::getAudioSampleRate' => 'Return the audio sample rate of the media file in bits per second.', +'ffmpeg_movie::getAuthor' => 'Return the author field from the movie or the artist ID3 field from an mp3 file.', +'ffmpeg_movie::getBitRate' => 'Return the bit rate of the movie or audio file in bits per second.', +'ffmpeg_movie::getComment' => 'Return the comment field from the movie or audio file.', +'ffmpeg_movie::getCopyright' => 'Return the copyright field from the movie or audio file.', +'ffmpeg_movie::getDuration' => 'Return the duration of a movie or audio file in seconds.', +'ffmpeg_movie::getFilename' => 'Return the path and name of the movie file or audio file.', +'ffmpeg_movie::getFrame' => 'Returns a frame from the movie as an ffmpeg_frame object. Returns false if the frame was not found.', +'ffmpeg_movie::getFrameCount' => 'Return the number of frames in a movie or audio file.', +'ffmpeg_movie::getFrameHeight' => 'Return the height of the movie in pixels.', +'ffmpeg_movie::getFrameNumber' => 'Return the current frame index.', +'ffmpeg_movie::getFrameRate' => 'Return the frame rate of a movie in fps.', +'ffmpeg_movie::getFrameWidth' => 'Return the width of the movie in pixels.', +'ffmpeg_movie::getGenre' => 'Return the genre ID3 field from an mp3 file.', +'ffmpeg_movie::getNextKeyFrame' => 'Returns the next key frame from the movie as an ffmpeg_frame object. Returns false if the frame was not found.', +'ffmpeg_movie::getPixelFormat' => 'Return the pixel format of the movie.', +'ffmpeg_movie::getTitle' => 'Return the title field from the movie or audio file.', +'ffmpeg_movie::getTrackNumber' => 'Return the track ID3 field from an mp3 file.', +'ffmpeg_movie::getVideoBitRate' => 'Return the bit rate of the video in bits per second. +NOTE: This only works for files with constant bit rate.', +'ffmpeg_movie::getVideoCodec' => 'Return the name of the video codec used to encode this movie as a string.', +'ffmpeg_movie::getYear' => 'Return the year ID3 field from an mp3 file.', +'ffmpeg_movie::hasAudio' => 'Return boolean value indicating whether the movie has an audio stream.', +'ffmpeg_movie::hasVideo' => 'Return boolean value indicating whether the movie has a video stream.', +'fgetc' => 'Gets character from file pointer', +'fgetcsv' => 'Gets line from file pointer and parse for CSV fields', +'fgets' => 'Gets line from file pointer', +'fgetss' => 'Gets line from file pointer and strip HTML tags', +'file' => 'Reads entire file into an array', +'file_exists' => 'Checks whether a file or directory exists', +'file_get_contents' => 'Reads entire file into a string', +'file_put_contents' => 'Write data to a file', +'fileatime' => 'Gets last access time of file', +'filectime' => 'Gets inode change time of file', +'filegroup' => 'Gets file group', +'fileinode' => 'Gets file inode', +'filemtime' => 'Gets file modification time', +'fileowner' => 'Gets file owner', +'fileperms' => 'Gets file permissions', +'filepro' => 'Read and verify the map file', +'filepro_fieldcount' => 'Find out how many fields are in a filePro database', +'filepro_fieldname' => 'Gets the name of a field', +'filepro_fieldtype' => 'Gets the type of a field', +'filepro_fieldwidth' => 'Gets the width of a field', +'filepro_retrieve' => 'Retrieves data from a filePro database', +'filepro_rowcount' => 'Find out how many rows are in a filePro database', +'filesize' => 'Gets file size', +'FilesystemIterator::__construct' => 'Constructs a new filesystem iterator', +'FilesystemIterator::__toString' => 'Get file name as a string', +'FilesystemIterator::current' => 'The current file', +'FilesystemIterator::getATime' => 'Get last access time of the current DirectoryIterator item', +'FilesystemIterator::getBasename' => 'Get base name of current DirectoryIterator item', +'FilesystemIterator::getCTime' => 'Get inode change time of the current DirectoryIterator item', +'FilesystemIterator::getExtension' => 'Gets the file extension', +'FilesystemIterator::getFileInfo' => 'Gets an SplFileInfo object for the file', +'FilesystemIterator::getFilename' => 'Return file name of current DirectoryIterator item', +'FilesystemIterator::getFlags' => 'Get the handling flags', +'FilesystemIterator::getGroup' => 'Get group for the current DirectoryIterator item', +'FilesystemIterator::getInode' => 'Get inode for the current DirectoryIterator item', +'FilesystemIterator::getLinkTarget' => 'Gets the target of a link', +'FilesystemIterator::getMTime' => 'Get last modification time of current DirectoryIterator item', +'FilesystemIterator::getOwner' => 'Get owner of current DirectoryIterator item', +'FilesystemIterator::getPath' => 'Get path of current Iterator item without filename', +'FilesystemIterator::getPathInfo' => 'Gets an SplFileInfo object for the path', +'FilesystemIterator::getPathname' => 'Return path and file name of current DirectoryIterator item', +'FilesystemIterator::getPerms' => 'Get the permissions of current DirectoryIterator item', +'FilesystemIterator::getRealPath' => 'Gets absolute path to file', +'FilesystemIterator::getSize' => 'Get size of current DirectoryIterator item', +'FilesystemIterator::getType' => 'Determine the type of the current DirectoryIterator item', +'FilesystemIterator::isDir' => 'Determine if current DirectoryIterator item is a directory', +'FilesystemIterator::isDot' => 'Determine if current DirectoryIterator item is \'.\' or \'..\'', +'FilesystemIterator::isExecutable' => 'Determine if current DirectoryIterator item is executable', +'FilesystemIterator::isFile' => 'Determine if current DirectoryIterator item is a regular file', +'FilesystemIterator::isLink' => 'Determine if current DirectoryIterator item is a symbolic link', +'FilesystemIterator::isReadable' => 'Determine if current DirectoryIterator item can be read', +'FilesystemIterator::isWritable' => 'Determine if current DirectoryIterator item can be written to', +'FilesystemIterator::key' => 'Retrieve the key for the current file', +'FilesystemIterator::next' => 'Move to the next file', +'FilesystemIterator::openFile' => 'Gets an SplFileObject object for the file', +'FilesystemIterator::rewind' => 'Rewinds back to the beginning', +'FilesystemIterator::seek' => 'Seek to a DirectoryIterator item', +'FilesystemIterator::setFileClass' => 'Sets the class used with SplFileInfo::openFile', +'FilesystemIterator::setFlags' => 'Sets handling flags', +'FilesystemIterator::setInfoClass' => 'Sets the class used with SplFileInfo::getFileInfo and SplFileInfo::getPathInfo', +'FilesystemIterator::valid' => 'Check whether current DirectoryIterator position is a valid file', +'filetype' => 'Gets file type', +'filter_has_var' => 'Checks if variable of specified type exists', +'filter_id' => 'Returns the filter ID belonging to a named filter', +'filter_input' => 'Gets a specific external variable by name and optionally filters it', +'filter_input_array' => 'Gets external variables and optionally filters them', +'filter_list' => 'Returns a list of all supported filters', +'filter_var' => 'Filters a variable with a specified filter', +'filter_var_array' => 'Gets multiple variables and optionally filters them', +'filteriterator::__construct' => 'Construct a filterIterator', +'filteriterator::accept' => 'Check whether the current element of the iterator is acceptable', +'filteriterator::current' => 'Get the current element value', +'filteriterator::getInnerIterator' => 'Get the inner iterator', +'filteriterator::key' => 'Get the current key', +'filteriterator::next' => 'Move the iterator forward', +'filteriterator::rewind' => 'Rewind the iterator', +'filteriterator::valid' => 'Check whether the current element is valid', +'finfo::__construct' => 'Alias of finfo_open', +'finfo::buffer' => 'Alias of finfo_buffer()', +'finfo::file' => 'Alias of finfo_file()', +'finfo::set_flags' => 'Alias of finfo_set_flags()', +'finfo_close' => 'Close fileinfo resource', +'floatval' => 'Get float value of a variable', +'flock' => 'Portable advisory file locking', +'floor' => 'Round fractions down', +'flush' => 'Flush system output buffer', +'fmod' => 'Returns the floating point remainder (modulo) of the division of the arguments', +'fnmatch' => 'Match filename against a pattern', +'fopen' => 'Opens file or URL', +'forward_static_call' => 'Call a static method', +'forward_static_call_array' => 'Call a static method and pass the arguments as array', +'fpassthru' => 'Output all remaining data on a file pointer', +'fprintf' => 'Write a formatted string to a stream', +'fputcsv' => 'Format line as CSV and write to file pointer', +'fputs' => 'Alias of fwrite', +'fread' => 'Binary-safe file read', +'frenchtojd' => 'Converts a date from the French Republican Calendar to a Julian Day Count', +'fribidi_log2vis' => 'Convert a logical string to a visual one', +'fscanf' => 'Parses input from a file according to a format', +'fseek' => 'Seeks on a file pointer', +'fsockopen' => 'Open Internet or Unix domain socket connection', +'fstat' => 'Gets information about a file using an open file pointer', +'ftell' => 'Returns the current position of the file read/write pointer', +'ftok' => 'Convert a pathname and a project identifier to a System V IPC key', +'ftp_alloc' => 'Allocates space for a file to be uploaded', +'ftp_append' => 'Append content of a file a another file on the FTP server', +'ftp_cdup' => 'Changes to the parent directory', +'ftp_chdir' => 'Changes the current directory on a FTP server', +'ftp_chmod' => 'Set permissions on a file via FTP', +'ftp_close' => 'Closes an FTP connection', +'ftp_connect' => 'Opens an FTP connection', +'ftp_delete' => 'Deletes a file on the FTP server', +'ftp_exec' => 'Requests execution of a command on the FTP server', +'ftp_fget' => 'Downloads a file from the FTP server and saves to an open file', +'ftp_fput' => 'Uploads from an open file to the FTP server', +'ftp_get' => 'Downloads a file from the FTP server', +'ftp_get_option' => 'Retrieves various runtime behaviours of the current FTP stream', +'ftp_login' => 'Logs in to an FTP connection', +'ftp_mdtm' => 'Returns the last modified time of the given file', +'ftp_mkdir' => 'Creates a directory', +'ftp_mlsd' => 'Returns a list of files in the given directory', +'ftp_nb_continue' => 'Continues retrieving/sending a file (non-blocking)', +'ftp_nb_fget' => 'Retrieves a file from the FTP server and writes it to an open file (non-blocking)', +'ftp_nb_fput' => 'Stores a file from an open file to the FTP server (non-blocking)', +'ftp_nb_get' => 'Retrieves a file from the FTP server and writes it to a local file (non-blocking)', +'ftp_nb_put' => 'Stores a file on the FTP server (non-blocking)', +'ftp_nlist' => 'Returns a list of files in the given directory', +'ftp_pasv' => 'Turns passive mode on or off', +'ftp_put' => 'Uploads a file to the FTP server', +'ftp_pwd' => 'Returns the current directory name', +'ftp_quit' => 'Alias of ftp_close', +'ftp_raw' => 'Sends an arbitrary command to an FTP server', +'ftp_rawlist' => 'Returns a detailed list of files in the given directory', +'ftp_rename' => 'Renames a file or a directory on the FTP server', +'ftp_rmdir' => 'Removes a directory', +'ftp_set_option' => 'Set miscellaneous runtime FTP options', +'ftp_site' => 'Sends a SITE command to the server', +'ftp_size' => 'Returns the size of the given file', +'ftp_ssl_connect' => 'Opens a Secure SSL-FTP connection', +'ftp_systype' => 'Returns the system type identifier of the remote FTP server', +'ftruncate' => 'Truncates a file to a given length', +'func_get_arg' => 'Return an item from the argument list', +'func_get_args' => 'Returns an array comprising a function\'s argument list', +'func_num_args' => 'Returns the number of arguments passed to the function', +'function_exists' => 'Return `true` if the given function has been defined', +'fwrite' => 'Binary-safe file write', +'gc_collect_cycles' => 'Forces collection of any existing garbage cycles', +'gc_disable' => 'Deactivates the circular reference collector', +'gc_enable' => 'Activates the circular reference collector', +'gc_enabled' => 'Returns status of the circular reference collector', +'gc_mem_caches' => 'Reclaims memory used by the Zend Engine memory manager', +'gc_status' => 'Gets information about the garbage collector', +'gd_info' => 'Retrieve information about the currently installed GD library', +'gearmanclient::__construct' => 'Create a GearmanClient instance', +'gearmanclient::addOptions' => 'Add client options', +'gearmanclient::addServer' => 'Add a job server to the client', +'gearmanclient::addServers' => 'Add a list of job servers to the client', +'gearmanclient::addTask' => 'Add a task to be run in parallel', +'gearmanclient::addTaskBackground' => 'Add a background task to be run in parallel', +'gearmanclient::addTaskHigh' => 'Add a high priority task to run in parallel', +'gearmanclient::addTaskHighBackground' => 'Add a high priority background task to be run in parallel', +'gearmanclient::addTaskLow' => 'Add a low priority task to run in parallel', +'gearmanclient::addTaskLowBackground' => 'Add a low priority background task to be run in parallel', +'gearmanclient::addTaskStatus' => 'Add a task to get status', +'gearmanclient::clearCallbacks' => 'Clear all task callback functions', +'gearmanclient::clone' => 'Create a copy of a GearmanClient object', +'gearmanclient::context' => 'Get the application context', +'gearmanclient::data' => 'Get the application data (deprecated)', +'gearmanclient::do' => 'Run a single task and return a result [deprecated]', +'gearmanclient::doBackground' => 'Run a task in the background', +'gearmanclient::doHigh' => 'Run a single high priority task', +'gearmanclient::doHighBackground' => 'Run a high priority task in the background', +'gearmanclient::doJobHandle' => 'Get the job handle for the running task', +'gearmanclient::doLow' => 'Run a single low priority task', +'gearmanclient::doLowBackground' => 'Run a low priority task in the background', +'gearmanclient::doNormal' => 'Run a single task and return a result', +'gearmanclient::doStatus' => 'Get the status for the running task', +'gearmanclient::echo' => 'Send data to all job servers to see if they echo it back [deprecated]', +'gearmanclient::error' => 'Returns an error string for the last error encountered', +'gearmanclient::getErrno' => 'Get an errno value', +'gearmanclient::jobStatus' => 'Get the status of a background job', +'gearmanclient::ping' => 'Send data to all job servers to see if they echo it back', +'gearmanclient::removeOptions' => 'Remove client options', +'gearmanclient::returnCode' => 'Get the last Gearman return code', +'gearmanclient::runTasks' => 'Run a list of tasks in parallel', +'gearmanclient::setClientCallback' => 'Callback function when there is a data packet for a task (deprecated)', +'gearmanclient::setCompleteCallback' => 'Set a function to be called on task completion', +'gearmanclient::setContext' => 'Set application context', +'gearmanclient::setCreatedCallback' => 'Set a callback for when a task is queued', +'gearmanclient::setData' => 'Set application data (deprecated)', +'gearmanclient::setDataCallback' => 'Callback function when there is a data packet for a task', +'gearmanclient::setExceptionCallback' => 'Set a callback for worker exceptions', +'gearmanclient::setFailCallback' => 'Set callback for job failure', +'gearmanclient::setOptions' => 'Set client options', +'gearmanclient::setStatusCallback' => 'Set a callback for collecting task status', +'gearmanclient::setTimeout' => 'Set socket I/O activity timeout', +'gearmanclient::setWarningCallback' => 'Set a callback for worker warnings', +'gearmanclient::setWorkloadCallback' => 'Set a callback for accepting incremental data updates', +'gearmanclient::timeout' => 'Get current socket I/O activity timeout value', +'gearmanjob::__construct' => 'Create a GearmanJob instance', +'gearmanjob::complete' => 'Send the result and complete status (deprecated)', +'gearmanjob::data' => 'Send data for a running job (deprecated)', +'gearmanjob::exception' => 'Send exception for running job (deprecated)', +'gearmanjob::fail' => 'Send fail status (deprecated)', +'gearmanjob::functionName' => 'Get function name', +'gearmanjob::handle' => 'Get the job handle', +'gearmanjob::returnCode' => 'Get last return code', +'gearmanjob::sendComplete' => 'Send the result and complete status', +'gearmanjob::sendData' => 'Send data for a running job', +'gearmanjob::sendException' => 'Send exception for running job (exception)', +'gearmanjob::sendFail' => 'Send fail status', +'gearmanjob::sendStatus' => 'Send status', +'gearmanjob::sendWarning' => 'Send a warning', +'gearmanjob::setReturn' => 'Set a return value', +'gearmanjob::status' => 'Send status (deprecated)', +'gearmanjob::unique' => 'Get the unique identifier', +'gearmanjob::warning' => 'Send a warning (deprecated)', +'gearmanjob::workload' => 'Get workload', +'gearmanjob::workloadSize' => 'Get size of work load', +'gearmantask::__construct' => 'Create a GearmanTask instance', +'gearmantask::create' => 'Create a task (deprecated)', +'gearmantask::data' => 'Get data returned for a task', +'gearmantask::dataSize' => 'Get the size of returned data', +'gearmantask::function' => 'Get associated function name (deprecated)', +'gearmantask::functionName' => 'Get associated function name', +'gearmantask::isKnown' => 'Determine if task is known', +'gearmantask::isRunning' => 'Test whether the task is currently running', +'gearmantask::jobHandle' => 'Get the job handle', +'gearmantask::recvData' => 'Read work or result data into a buffer for a task', +'gearmantask::returnCode' => 'Get the last return code', +'gearmantask::sendData' => 'Send data for a task (deprecated)', +'gearmantask::sendWorkload' => 'Send data for a task', +'gearmantask::taskDenominator' => 'Get completion percentage denominator', +'gearmantask::taskNumerator' => 'Get completion percentage numerator', +'gearmantask::unique' => 'Get the unique identifier for a task', +'gearmantask::uuid' => 'Get the unique identifier for a task (deprecated)', +'gearmanworker::__construct' => 'Create a GearmanWorker instance', +'gearmanworker::addFunction' => 'Register and add callback function', +'gearmanworker::addOptions' => 'Add worker options', +'gearmanworker::addServer' => 'Add a job server', +'gearmanworker::addServers' => 'Add job servers', +'gearmanworker::clone' => 'Create a copy of the worker', +'gearmanworker::echo' => 'Test job server response', +'gearmanworker::error' => 'Get the last error encountered', +'gearmanworker::getErrno' => 'Get errno', +'gearmanworker::options' => 'Get worker options', +'gearmanworker::register' => 'Register a function with the job server', +'gearmanworker::removeOptions' => 'Remove worker options', +'gearmanworker::returnCode' => 'Get last Gearman return code', +'gearmanworker::setId' => 'Give the worker an identifier so it can be tracked when asking gearmand for the list of available workers', +'gearmanworker::setOptions' => 'Set worker options', +'gearmanworker::setTimeout' => 'Set socket I/O activity timeout', +'gearmanworker::timeout' => 'Get socket I/O activity timeout', +'gearmanworker::unregister' => 'Unregister a function name with the job servers', +'gearmanworker::unregisterAll' => 'Unregister all function names with the job servers', +'gearmanworker::wait' => 'Wait for activity from one of the job servers', +'gearmanworker::work' => 'Wait for and perform jobs', +'gender\gender::__construct' => 'Construct the Gender object', +'gender\gender::connect' => 'Connect to an external name dictionary', +'gender\gender::country' => 'Get textual country representation', +'gender\gender::get' => 'Get gender of a name', +'gender\gender::isNick' => 'Check if the name0 is an alias of the name1', +'gender\gender::similarNames' => 'Get similar names', +'Generator::__wakeup' => 'Serialize callback +Throws an exception as generators can\'t be serialized.', +'Generator::current' => 'Returns whatever was passed to yield or null if nothing was passed or the generator is already closed.', +'Generator::getReturn' => 'Returns whatever was passed to return or null if nothing. +Throws an exception if the generator is still valid.', +'Generator::key' => 'Returns the yielded key or, if none was specified, an auto-incrementing key or null if the generator is already closed.', +'Generator::next' => 'Resumes the generator (unless the generator is already closed).', +'Generator::rewind' => 'Throws an exception if the generator is currently after the first yield.', +'Generator::send' => 'Sets the return value of the yield expression and resumes the generator (unless the generator is already closed).', +'Generator::valid' => 'Returns false if the generator has been closed, true otherwise.', +'geoip_asnum_by_name' => 'Get the Autonomous System Numbers (ASN)', +'geoip_continent_code_by_name' => 'Get the two letter continent code', +'geoip_country_code3_by_name' => 'Get the three letter country code', +'geoip_country_code_by_name' => 'Get the two letter country code', +'geoip_country_name_by_name' => 'Get the full country name', +'geoip_database_info' => 'Get GeoIP Database information', +'geoip_db_avail' => 'Determine if GeoIP Database is available', +'geoip_db_filename' => 'Returns the filename of the corresponding GeoIP Database', +'geoip_db_get_all_info' => 'Returns detailed information about all GeoIP database types', +'geoip_domain_by_name' => 'Get the second level domain name', +'geoip_id_by_name' => 'Get the Internet connection type', +'geoip_isp_by_name' => 'Get the Internet Service Provider (ISP) name', +'geoip_netspeedcell_by_name' => 'Get the Internet connection speed', +'geoip_org_by_name' => 'Get the organization name', +'geoip_record_by_name' => 'Returns the detailed City information found in the GeoIP Database', +'geoip_region_by_name' => 'Get the country code and region', +'geoip_region_name_by_code' => 'Returns the region name for some country and region code combo', +'geoip_setup_custom_directory' => 'Set a custom directory for the GeoIP database', +'geoip_time_zone_by_country_and_region' => 'Returns the time zone for some country and region code combo', +'GEOSGeometry::__construct' => 'GEOSGeometry constructor.', +'GEOSPolygonize' => '- \'rings\' + Type: array of GEOSGeometry + Rings that can be formed by the costituent + linework of geometry. +- \'cut_edges\' (optional) + Type: array of GEOSGeometry + Edges which are connected at both ends but + which do not form part of polygon. +- \'dangles\' + Type: array of GEOSGeometry + Edges which have one or both ends which are + not incident on another edge endpoint +- \'invalid_rings\' + Type: array of GEOSGeometry + Edges which form rings which are invalid + (e.g. the component lines contain a self-intersection)', +'GEOSWKBReader::__construct' => 'GEOSWKBReader constructor.', +'GEOSWKBWriter::__construct' => 'GEOSWKBWriter constructor.', +'GEOSWKTReader::__construct' => 'GEOSWKTReader constructor.', +'GEOSWKTWriter::__construct' => 'GEOSWKTWriter constructor.', +'get_browser' => 'Tells what the user\'s browser is capable of', +'get_called_class' => 'The "Late Static Binding" class name', +'get_cfg_var' => 'Gets the value of a PHP configuration option', +'get_class' => 'Returns the name of the class of an object', +'get_class_methods' => 'Gets the class methods\' names', +'get_class_vars' => 'Get the default properties of the class', +'get_current_user' => 'Gets the name of the owner of the current PHP script', +'get_declared_classes' => 'Returns an array with the name of the defined classes', +'get_declared_interfaces' => 'Returns an array of all declared interfaces', +'get_declared_traits' => 'Returns an array of all declared traits', +'get_defined_constants' => 'Returns an associative array with the names of all the constants and their values', +'get_defined_functions' => 'Returns an array of all defined functions', +'get_defined_vars' => 'Returns an array of all defined variables', +'get_extension_funcs' => 'Returns an array with the names of the functions of a module', +'get_headers' => 'Fetches all the headers sent by the server in response to an HTTP request', +'get_html_translation_table' => 'Returns the translation table used by htmlspecialchars and htmlentities', +'get_include_path' => 'Gets the current include_path configuration option', +'get_included_files' => 'Returns an array with the names of included or required files', +'get_loaded_extensions' => 'Returns an array with the names of all modules compiled and loaded', +'get_magic_quotes_gpc' => 'Gets the current configuration setting of magic_quotes_gpc', +'get_magic_quotes_runtime' => 'Gets the current active configuration setting of magic_quotes_runtime', +'get_meta_tags' => 'Extracts all meta tag content attributes from a file and returns an array', +'get_object_vars' => 'Gets the properties of the given object', +'get_parent_class' => 'Retrieves the parent class name for object or class', +'get_required_files' => 'Alias of get_included_files', +'get_resource_type' => 'Returns the resource type', +'get_resources' => 'Returns active resources', +'getallheaders' => 'Fetch all HTTP request headers', +'getcwd' => 'Gets the current working directory', +'getdate' => 'Get date/time information', +'getenv' => 'Gets the value of an environment variable', +'gethostbyaddr' => 'Get the Internet host name corresponding to a given IP address', +'gethostbyname' => 'Get the IPv4 address corresponding to a given Internet host name', +'gethostbynamel' => 'Get a list of IPv4 addresses corresponding to a given Internet host name', +'gethostname' => 'Gets the host name', +'getimagesize' => 'Get the size of an image', +'getimagesizefromstring' => 'Get the size of an image from a string', +'getlastmod' => 'Gets time of last page modification', +'getmxrr' => 'Get MX records corresponding to a given Internet host name', +'getmygid' => 'Get PHP script owner\'s GID', +'getmyinode' => 'Gets the inode of the current script', +'getmypid' => 'Gets PHP\'s process ID', +'getmyuid' => 'Gets PHP script owner\'s UID', +'getopt' => 'Gets options from the command line argument list', +'getprotobyname' => 'Get protocol number associated with protocol name', +'getprotobynumber' => 'Get protocol name associated with protocol number', +'getrandmax' => 'Show largest possible random value', +'getrusage' => 'Gets the current resource usages', +'getservbyname' => 'Get port number associated with an Internet service and protocol', +'getservbyport' => 'Get Internet service which corresponds to port and protocol', +'getsession' => 'Connect to a MySQL server', +'gettext' => 'Lookup a message in the current domain', +'gettimeofday' => 'Get current time', +'gettype' => 'Get the type of a variable', +'glob' => 'Find pathnames matching a pattern', +'globiterator::__construct' => 'Construct a directory using glob', +'GlobIterator::__toString' => 'Get file name as a string', +'globiterator::count' => 'Get the number of directories and files', +'GlobIterator::current' => 'The current file', +'GlobIterator::getATime' => 'Get last access time of the current DirectoryIterator item', +'GlobIterator::getBasename' => 'Get base name of current DirectoryIterator item', +'GlobIterator::getCTime' => 'Get inode change time of the current DirectoryIterator item', +'GlobIterator::getExtension' => 'Gets the file extension', +'GlobIterator::getFileInfo' => 'Gets an SplFileInfo object for the file', +'GlobIterator::getFilename' => 'Return file name of current DirectoryIterator item', +'GlobIterator::getFlags' => 'Get the handling flags', +'GlobIterator::getGroup' => 'Get group for the current DirectoryIterator item', +'GlobIterator::getInode' => 'Get inode for the current DirectoryIterator item', +'GlobIterator::getLinkTarget' => 'Gets the target of a link', +'GlobIterator::getMTime' => 'Get last modification time of current DirectoryIterator item', +'GlobIterator::getOwner' => 'Get owner of current DirectoryIterator item', +'GlobIterator::getPath' => 'Get path of current Iterator item without filename', +'GlobIterator::getPathInfo' => 'Gets an SplFileInfo object for the path', +'GlobIterator::getPathname' => 'Return path and file name of current DirectoryIterator item', +'GlobIterator::getPerms' => 'Get the permissions of current DirectoryIterator item', +'GlobIterator::getRealPath' => 'Gets absolute path to file', +'GlobIterator::getSize' => 'Get size of current DirectoryIterator item', +'GlobIterator::getType' => 'Determine the type of the current DirectoryIterator item', +'GlobIterator::isDir' => 'Determine if current DirectoryIterator item is a directory', +'GlobIterator::isDot' => 'Determine if current DirectoryIterator item is \'.\' or \'..\'', +'GlobIterator::isExecutable' => 'Determine if current DirectoryIterator item is executable', +'GlobIterator::isFile' => 'Determine if current DirectoryIterator item is a regular file', +'GlobIterator::isLink' => 'Determine if current DirectoryIterator item is a symbolic link', +'GlobIterator::isReadable' => 'Determine if current DirectoryIterator item can be read', +'GlobIterator::isWritable' => 'Determine if current DirectoryIterator item can be written to', +'GlobIterator::key' => 'Retrieve the key for the current file', +'GlobIterator::next' => 'Move to the next file', +'GlobIterator::openFile' => 'Gets an SplFileObject object for the file', +'GlobIterator::rewind' => 'Rewinds back to the beginning', +'GlobIterator::seek' => 'Seek to a DirectoryIterator item', +'GlobIterator::setFileClass' => 'Sets the class used with SplFileInfo::openFile', +'GlobIterator::setFlags' => 'Sets handling flags', +'GlobIterator::setInfoClass' => 'Sets the class used with SplFileInfo::getFileInfo and SplFileInfo::getPathInfo', +'GlobIterator::valid' => 'Check whether current DirectoryIterator position is a valid file', +'gmagick::__construct' => 'The Gmagick constructor', +'gmagick::addimage' => 'Adds new image to Gmagick object image list', +'gmagick::addnoiseimage' => 'Adds random noise to the image', +'gmagick::annotateimage' => 'Annotates an image with text', +'gmagick::blurimage' => 'Adds blur filter to image', +'gmagick::borderimage' => 'Surrounds the image with a border', +'gmagick::charcoalimage' => 'Simulates a charcoal drawing', +'gmagick::chopimage' => 'Removes a region of an image and trims', +'gmagick::clear' => 'Clears all resources associated to Gmagick object', +'gmagick::commentimage' => 'Adds a comment to your image', +'gmagick::compositeimage' => 'Composite one image onto another', +'gmagick::cropimage' => 'Extracts a region of the image', +'gmagick::cropthumbnailimage' => 'Creates a crop thumbnail', +'gmagick::current' => 'The current purpose', +'gmagick::cyclecolormapimage' => 'Displaces an image\'s colormap', +'gmagick::deconstructimages' => 'Returns certain pixel differences between images', +'gmagick::despeckleimage' => 'The despeckleimage purpose', +'gmagick::destroy' => 'The destroy purpose', +'gmagick::drawimage' => 'Renders the GmagickDraw object on the current image', +'gmagick::edgeimage' => 'Enhance edges within the image', +'gmagick::embossimage' => 'Returns a grayscale image with a three-dimensional effect', +'gmagick::enhanceimage' => 'Improves the quality of a noisy image', +'gmagick::equalizeimage' => 'Equalizes the image histogram', +'gmagick::flipimage' => 'Creates a vertical mirror image', +'gmagick::flopimage' => 'The flopimage purpose', +'gmagick::frameimage' => 'Adds a simulated three-dimensional border', +'gmagick::gammaimage' => 'Gamma-corrects an image', +'gmagick::getcopyright' => 'Returns the GraphicsMagick API copyright as a string', +'gmagick::getfilename' => 'The filename associated with an image sequence', +'gmagick::getimagebackgroundcolor' => 'Returns the image background color', +'gmagick::getimageblueprimary' => 'Returns the chromaticy blue primary point', +'gmagick::getimagebordercolor' => 'Returns the image border color', +'gmagick::getimagechanneldepth' => 'Gets the depth for a particular image channel', +'gmagick::getimagecolors' => 'Returns the color of the specified colormap index', +'gmagick::getimagecolorspace' => 'Gets the image colorspace', +'gmagick::getimagecompose' => 'Returns the composite operator associated with the image', +'gmagick::getimagedelay' => 'Gets the image delay', +'gmagick::getimagedepth' => 'Gets the depth of the image', +'gmagick::getimagedispose' => 'Gets the image disposal method', +'gmagick::getimageextrema' => 'Gets the extrema for the image', +'gmagick::getimagefilename' => 'Returns the filename of a particular image in a sequence', +'gmagick::getimageformat' => 'Returns the format of a particular image in a sequence', +'gmagick::getimagegamma' => 'Gets the image gamma', +'gmagick::getimagegreenprimary' => 'Returns the chromaticy green primary point', +'gmagick::getimageheight' => 'Returns the image height', +'gmagick::getimagehistogram' => 'Gets the image histogram', +'gmagick::getimageindex' => 'Gets the index of the current active image', +'gmagick::getimageinterlacescheme' => 'Gets the image interlace scheme', +'gmagick::getimageiterations' => 'Gets the image iterations', +'gmagick::getimagematte' => 'Check if the image has a matte channel', +'gmagick::getimagemattecolor' => 'Returns the image matte color', +'gmagick::getimageprofile' => 'Returns the named image profile', +'gmagick::getimageredprimary' => 'Returns the chromaticity red primary point', +'gmagick::getimagerenderingintent' => 'Gets the image rendering intent', +'gmagick::getimageresolution' => 'Gets the image X and Y resolution', +'gmagick::getimagescene' => 'Gets the image scene', +'gmagick::getimagesignature' => 'Generates an SHA-256 message digest', +'gmagick::getimagetype' => 'Gets the potential image type', +'gmagick::getimageunits' => 'Gets the image units of resolution', +'gmagick::getimagewhitepoint' => 'Returns the chromaticity white point', +'gmagick::getimagewidth' => 'Returns the width of the image', +'gmagick::getpackagename' => 'Returns the GraphicsMagick package name', +'gmagick::getquantumdepth' => 'Returns the Gmagick quantum depth as a string', +'gmagick::getreleasedate' => 'Returns the GraphicsMagick release date as a string', +'gmagick::getsamplingfactors' => 'Gets the horizontal and vertical sampling factor', +'gmagick::getsize' => 'Returns the size associated with the Gmagick object', +'gmagick::getversion' => 'Returns the GraphicsMagick API version', +'gmagick::hasnextimage' => 'Checks if the object has more images', +'gmagick::haspreviousimage' => 'Checks if the object has a previous image', +'gmagick::implodeimage' => 'Creates a new image as a copy', +'gmagick::labelimage' => 'Adds a label to an image', +'gmagick::levelimage' => 'Adjusts the levels of an image', +'gmagick::magnifyimage' => 'Scales an image proportionally 2x', +'gmagick::mapimage' => 'Replaces the colors of an image with the closest color from a reference image', +'gmagick::medianfilterimage' => 'Applies a digital filter', +'gmagick::minifyimage' => 'Scales an image proportionally to half its size', +'gmagick::modulateimage' => 'Control the brightness, saturation, and hue', +'gmagick::motionblurimage' => 'Simulates motion blur', +'gmagick::newimage' => 'Creates a new image', +'gmagick::nextimage' => 'Moves to the next image', +'gmagick::normalizeimage' => 'Enhances the contrast of a color image', +'gmagick::oilpaintimage' => 'Simulates an oil painting', +'gmagick::previousimage' => 'Move to the previous image in the object', +'gmagick::profileimage' => 'Adds or removes a profile from an image', +'gmagick::quantizeimage' => 'Analyzes the colors within a reference image', +'gmagick::quantizeimages' => 'The quantizeimages purpose', +'gmagick::queryfontmetrics' => 'Returns an array representing the font metrics', +'gmagick::queryfonts' => 'Returns the configured fonts', +'gmagick::queryformats' => 'Returns formats supported by Gmagick', +'gmagick::radialblurimage' => 'Radial blurs an image', +'gmagick::raiseimage' => 'Creates a simulated 3d button-like effect', +'gmagick::read' => 'Reads image from filename', +'gmagick::readimage' => 'Reads image from filename', +'gmagick::readimageblob' => 'Reads image from a binary string', +'gmagick::readimagefile' => 'The readimagefile purpose', +'gmagick::reducenoiseimage' => 'Smooths the contours of an image', +'gmagick::removeimage' => 'Removes an image from the image list', +'gmagick::removeimageprofile' => 'Removes the named image profile and returns it', +'gmagick::resampleimage' => 'Resample image to desired resolution', +'gmagick::resizeimage' => 'Scales an image', +'gmagick::rollimage' => 'Offsets an image', +'gmagick::rotateimage' => 'Rotates an image', +'gmagick::scaleimage' => 'Scales the size of an image', +'gmagick::separateimagechannel' => 'Separates a channel from the image', +'gmagick::setCompressionQuality' => 'Sets the object\'s default compression quality', +'gmagick::setfilename' => 'Sets the filename before you read or write the image', +'gmagick::setimagebackgroundcolor' => 'Sets the image background color', +'gmagick::setimageblueprimary' => 'Sets the image chromaticity blue primary point', +'gmagick::setimagebordercolor' => 'Sets the image border color', +'gmagick::setimagechanneldepth' => 'Sets the depth of a particular image channel', +'gmagick::setimagecolorspace' => 'Sets the image colorspace', +'gmagick::setimagecompose' => 'Sets the image composite operator', +'gmagick::setimagedelay' => 'Sets the image delay', +'gmagick::setimagedepth' => 'Sets the image depth', +'gmagick::setimagedispose' => 'Sets the image disposal method', +'gmagick::setimagefilename' => 'Sets the filename of a particular image in a sequence', +'gmagick::setimageformat' => 'Sets the format of a particular image', +'gmagick::setimagegamma' => 'Sets the image gamma', +'gmagick::setimagegreenprimary' => 'Sets the image chromaticity green primary point', +'gmagick::setimageindex' => 'Set the iterator to the position in the image list specified with the index parameter', +'gmagick::setimageinterlacescheme' => 'Sets the interlace scheme of the image', +'gmagick::setimageiterations' => 'Sets the image iterations', +'gmagick::setimageprofile' => 'Adds a named profile to the Gmagick object', +'gmagick::setimageredprimary' => 'Sets the image chromaticity red primary point', +'gmagick::setimagerenderingintent' => 'Sets the image rendering intent', +'gmagick::setimageresolution' => 'Sets the image resolution', +'gmagick::setimagescene' => 'Sets the image scene', +'gmagick::setimagetype' => 'Sets the image type', +'gmagick::setimageunits' => 'Sets the image units of resolution', +'gmagick::setimagewhitepoint' => 'Sets the image chromaticity white point', +'gmagick::setsamplingfactors' => 'Sets the image sampling factors', +'gmagick::setsize' => 'Sets the size of the Gmagick object', +'gmagick::shearimage' => 'Creating a parallelogram', +'gmagick::solarizeimage' => 'Applies a solarizing effect to the image', +'gmagick::spreadimage' => 'Randomly displaces each pixel in a block', +'gmagick::stripimage' => 'Strips an image of all profiles and comments', +'gmagick::swirlimage' => 'Swirls the pixels about the center of the image', +'gmagick::thumbnailimage' => 'Changes the size of an image', +'gmagick::trimimage' => 'Remove edges from the image', +'gmagick::write' => 'Alias of Gmagick::writeimage', +'gmagick::writeimage' => 'Writes an image to the specified filename', +'gmagickdraw::annotate' => 'Draws text on the image', +'gmagickdraw::arc' => 'Draws an arc', +'gmagickdraw::bezier' => 'Draws a bezier curve', +'gmagickdraw::ellipse' => 'Draws an ellipse on the image', +'gmagickdraw::getfillcolor' => 'Returns the fill color', +'gmagickdraw::getfillopacity' => 'Returns the opacity used when drawing', +'gmagickdraw::getfont' => 'Returns the font', +'gmagickdraw::getfontsize' => 'Returns the font pointsize', +'gmagickdraw::getfontstyle' => 'Returns the font style', +'gmagickdraw::getfontweight' => 'Returns the font weight', +'gmagickdraw::getstrokecolor' => 'Returns the color used for stroking object outlines', +'gmagickdraw::getstrokeopacity' => 'Returns the opacity of stroked object outlines', +'gmagickdraw::getstrokewidth' => 'Returns the width of the stroke used to draw object outlines', +'gmagickdraw::gettextdecoration' => 'Returns the text decoration', +'gmagickdraw::gettextencoding' => 'Returns the code set used for text annotations', +'gmagickdraw::line' => 'The line purpose', +'gmagickdraw::point' => 'Draws a point', +'gmagickdraw::polygon' => 'Draws a polygon', +'gmagickdraw::polyline' => 'Draws a polyline', +'gmagickdraw::rectangle' => 'Draws a rectangle', +'gmagickdraw::rotate' => 'Applies the specified rotation to the current coordinate space', +'gmagickdraw::roundrectangle' => 'Draws a rounded rectangle', +'gmagickdraw::scale' => 'Adjusts the scaling factor', +'gmagickdraw::setfillcolor' => 'Sets the fill color to be used for drawing filled objects', +'gmagickdraw::setfillopacity' => 'The setfillopacity purpose', +'gmagickdraw::setfont' => 'Sets the fully-specified font to use when annotating with text', +'gmagickdraw::setfontsize' => 'Sets the font pointsize to use when annotating with text', +'gmagickdraw::setfontstyle' => 'Sets the font style to use when annotating with text', +'gmagickdraw::setfontweight' => 'Sets the font weight', +'gmagickdraw::setstrokecolor' => 'Sets the color used for stroking object outlines', +'gmagickdraw::setstrokeopacity' => 'Specifies the opacity of stroked object outlines', +'gmagickdraw::setstrokewidth' => 'Sets the width of the stroke used to draw object outlines', +'gmagickdraw::settextdecoration' => 'Specifies a decoration', +'gmagickdraw::settextencoding' => 'Specifies the text code set', +'gmagickpixel::__construct' => 'The GmagickPixel constructor', +'gmagickpixel::getcolor' => 'Returns the color', +'gmagickpixel::getcolorcount' => 'Returns the color count associated with this color', +'gmagickpixel::getcolorvalue' => 'Gets the normalized value of the provided color channel', +'gmagickpixel::setcolor' => 'Sets the color', +'gmagickpixel::setcolorvalue' => 'Sets the normalized value of one of the channels', +'gmdate' => 'Format a GMT/UTC date/time', +'gmmktime' => 'Get Unix timestamp for a GMT date', +'GMP::serialize' => 'String representation of object', +'GMP::unserialize' => 'Constructs the object', +'gmp_abs' => 'Absolute value', +'gmp_add' => 'Add numbers', +'gmp_and' => 'Bitwise AND', +'gmp_binomial' => 'Calculates binomial coefficient', +'gmp_clrbit' => 'Clear bit', +'gmp_cmp' => 'Compare numbers', +'gmp_com' => 'Calculates one\'s complement', +'gmp_div' => 'Alias of gmp_div_q', +'gmp_div_q' => 'Divide numbers', +'gmp_div_qr' => 'Divide numbers and get quotient and remainder', +'gmp_div_r' => 'Remainder of the division of numbers', +'gmp_divexact' => 'Exact division of numbers', +'gmp_export' => 'Export to a binary string', +'gmp_fact' => 'Factorial', +'gmp_gcd' => 'Calculate GCD', +'gmp_gcdext' => 'Calculate GCD and multipliers', +'gmp_hamdist' => 'Hamming distance', +'gmp_import' => 'Import from a binary string', +'gmp_init' => 'Create GMP number', +'gmp_intval' => 'Convert GMP number to integer', +'gmp_invert' => 'Inverse by modulo', +'gmp_jacobi' => 'Jacobi symbol', +'gmp_kronecker' => 'Kronecker symbol', +'gmp_lcm' => 'Calculate GCD', +'gmp_legendre' => 'Legendre symbol', +'gmp_mod' => 'Modulo operation', +'gmp_mul' => 'Multiply numbers', +'gmp_neg' => 'Negate number', +'gmp_nextprime' => 'Find next prime number', +'gmp_or' => 'Bitwise OR', +'gmp_perfect_power' => 'Perfect power check', +'gmp_perfect_square' => 'Perfect square check', +'gmp_popcount' => 'Population count', +'gmp_pow' => 'Raise number into power', +'gmp_powm' => 'Raise number into power with modulo', +'gmp_prob_prime' => 'Check if number is "probably prime"', +'gmp_random' => 'Random number', +'gmp_random_bits' => 'Random number', +'gmp_random_range' => 'Random number', +'gmp_random_seed' => 'Sets the RNG seed', +'gmp_root' => 'Take the integer part of nth root', +'gmp_rootrem' => 'Take the integer part and remainder of nth root', +'gmp_scan0' => 'Scan for 0', +'gmp_scan1' => 'Scan for 1', +'gmp_setbit' => 'Set bit', +'gmp_sign' => 'Sign of number', +'gmp_sqrt' => 'Calculate square root', +'gmp_sqrtrem' => 'Square root with remainder', +'gmp_strval' => 'Convert GMP number to string', +'gmp_sub' => 'Subtract numbers', +'gmp_testbit' => 'Tests if a bit is set', +'gmp_xor' => 'Bitwise XOR', +'gmstrftime' => 'Format a GMT/UTC time/date according to locale settings', +'gnupg::adddecryptkey' => 'Add a key for decryption', +'gnupg::addencryptkey' => 'Add a key for encryption', +'gnupg::addsignkey' => 'Add a key for signing', +'gnupg::cleardecryptkeys' => 'Removes all keys which were set for decryption before', +'gnupg::clearencryptkeys' => 'Removes all keys which were set for encryption before', +'gnupg::clearsignkeys' => 'Removes all keys which were set for signing before', +'gnupg::decrypt' => 'Decrypts a given text', +'gnupg::decryptverify' => 'Decrypts and verifies a given text', +'gnupg::encrypt' => 'Encrypts a given text', +'gnupg::encryptsign' => 'Encrypts and signs a given text', +'gnupg::export' => 'Exports a key', +'gnupg::geterror' => 'Returns the errortext, if a function fails', +'gnupg::getprotocol' => 'Returns the currently active protocol for all operations', +'gnupg::import' => 'Imports a key', +'gnupg::init' => 'Initialize a connection', +'gnupg::keyinfo' => 'Returns an array with information about all keys that matches the given pattern', +'gnupg::setarmor' => 'Toggle armored output', +'gnupg::seterrormode' => 'Sets the mode for error_reporting', +'gnupg::setsignmode' => 'Sets the mode for signing', +'gnupg::sign' => 'Signs a given text', +'gnupg::verify' => 'Verifies a signed text', +'gnupg_adddecryptkey' => 'Add a key for decryption', +'gnupg_addencryptkey' => 'Add a key for encryption', +'gnupg_addsignkey' => 'Add a key for signing', +'gnupg_cleardecryptkeys' => 'Removes all keys which were set for decryption before', +'gnupg_clearencryptkeys' => 'Removes all keys which were set for encryption before', +'gnupg_clearsignkeys' => 'Removes all keys which were set for signing before', +'gnupg_decrypt' => 'Decrypts a given text', +'gnupg_decryptverify' => 'Decrypts and verifies a given text', +'gnupg_encrypt' => 'Encrypts a given text', +'gnupg_encryptsign' => 'Encrypts and signs a given text', +'gnupg_export' => 'Exports a key', +'gnupg_geterror' => 'Returns the errortext, if a function fails', +'gnupg_getprotocol' => 'Returns the currently active protocol for all operations', +'gnupg_import' => 'Imports a key', +'gnupg_init' => 'Initialize a connection', +'gnupg_keyinfo' => 'Returns an array with information about all keys that matches the given pattern', +'gnupg_setarmor' => 'Toggle armored output', +'gnupg_seterrormode' => 'Sets the mode for error_reporting', +'gnupg_setsignmode' => 'Sets the mode for signing', +'gnupg_sign' => 'Signs a given text', +'gnupg_verify' => 'Verifies a signed text', +'gopher_parsedir' => 'Translate a gopher formatted directory entry into an associative array', +'gregoriantojd' => 'Converts a Gregorian date to Julian Day Count', +'gridObj::set' => 'Set object property to a new value.', +'Grpc\Call::__construct' => 'Constructs a new instance of the Call class.', +'Grpc\Call::cancel' => 'Cancel the call. This will cause the call to end with STATUS_CANCELLED if it +has not already ended with another status.', +'Grpc\Call::getPeer' => 'Get the endpoint this call/stream is connected to', +'Grpc\Call::setCredentials' => 'Set the CallCredentials for this call.', +'Grpc\Call::startBatch' => 'Start a batch of RPC actions.', +'Grpc\CallCredentials::createComposite' => 'Create composite credentials from two existing credentials.', +'Grpc\CallCredentials::createFromPlugin' => 'Create a call credentials object from the plugin API', +'Grpc\Channel::__construct' => 'Construct an instance of the Channel class. If the $args array contains a +"credentials" key mapping to a ChannelCredentials object, a secure channel +will be created with those credentials.', +'Grpc\Channel::close' => 'Close the channel', +'Grpc\Channel::getConnectivityState' => 'Get the connectivity state of the channel', +'Grpc\Channel::getTarget' => 'Get the endpoint this call/stream is connected to', +'Grpc\Channel::watchConnectivityState' => 'Watch the connectivity state of the channel until it changed', +'Grpc\ChannelCredentials::createComposite' => 'Create composite credentials from two existing credentials.', +'Grpc\ChannelCredentials::createDefault' => 'Create a default channel credentials object.', +'Grpc\ChannelCredentials::createInsecure' => 'Create insecure channel credentials', +'Grpc\ChannelCredentials::createSsl' => 'Create SSL credentials.', +'Grpc\ChannelCredentials::setDefaultRootsPem' => 'Set default roots pem.', +'Grpc\Server::__construct' => 'Constructs a new instance of the Server class', +'Grpc\Server::addHttp2Port' => 'Add a http2 over tcp listener.', +'Grpc\Server::addSecureHttp2Port' => 'Add a secure http2 over tcp listener.', +'Grpc\Server::requestCall' => 'Request a call on a server. Creates a single GRPC_SERVER_RPC_NEW event.', +'Grpc\Server::start' => 'Start a server - tells all listeners to start listening', +'Grpc\ServerCredentials::createSsl' => 'Create SSL credentials.', +'Grpc\Timeval::__construct' => 'Constructs a new instance of the Timeval class', +'Grpc\Timeval::add' => 'Adds another Timeval to this one and returns the sum. Calculations saturate +at infinities.', +'Grpc\Timeval::compare' => 'Return negative, 0, or positive according to whether a < b, a == b, or a > b +respectively.', +'Grpc\Timeval::infFuture' => 'Returns the infinite future time value as a timeval object', +'Grpc\Timeval::infPast' => 'Returns the infinite past time value as a timeval object', +'Grpc\Timeval::now' => 'Returns the current time as a timeval object', +'Grpc\Timeval::similar' => 'Checks whether the two times are within $threshold of each other', +'Grpc\Timeval::sleepUntil' => 'Sleep until this time, interpreted as an absolute timeout', +'Grpc\Timeval::subtract' => 'Subtracts another Timeval from this one and returns the difference. +Calculations saturate at infinities.', +'Grpc\Timeval::zero' => 'Returns the zero time interval as a timeval object', +'gupnp_context_get_host_ip' => 'Get the IP address', +'gupnp_context_get_port' => 'Get the port', +'gupnp_context_get_subscription_timeout' => 'Get the event subscription timeout', +'gupnp_context_host_path' => 'Start hosting', +'gupnp_context_new' => 'Create a new context', +'gupnp_context_set_subscription_timeout' => 'Sets the event subscription timeout', +'gupnp_context_timeout_add' => 'Sets a function to be called at regular intervals', +'gupnp_context_unhost_path' => 'Stop hosting', +'gupnp_control_point_browse_start' => 'Start browsing', +'gupnp_control_point_browse_stop' => 'Stop browsing', +'gupnp_control_point_callback_set' => 'Set control point callback', +'gupnp_control_point_new' => 'Create a new control point', +'gupnp_device_action_callback_set' => 'Set device callback function', +'gupnp_device_info_get' => 'Get info of root device', +'gupnp_device_info_get_service' => 'Get the service with type', +'gupnp_root_device_get_available' => 'Check whether root device is available', +'gupnp_root_device_get_relative_location' => 'Get the relative location of root device', +'gupnp_root_device_new' => 'Create a new root device', +'gupnp_root_device_set_available' => 'Set whether or not root_device is available', +'gupnp_root_device_start' => 'Start main loop', +'gupnp_root_device_stop' => 'Stop main loop', +'gupnp_service_action_get' => 'Retrieves the specified action arguments', +'gupnp_service_action_return' => 'Return successfully', +'gupnp_service_action_return_error' => 'Return error code', +'gupnp_service_action_set' => 'Sets the specified action return values', +'gupnp_service_freeze_notify' => 'Freeze new notifications', +'gupnp_service_info_get' => 'Get full info of service', +'gupnp_service_info_get_introspection' => 'Get resource introspection of service', +'gupnp_service_introspection_get_state_variable' => 'Returns the state variable data', +'gupnp_service_notify' => 'Notifies listening clients', +'gupnp_service_proxy_action_get' => 'Send action to the service and get value', +'gupnp_service_proxy_action_set' => 'Send action to the service and set value', +'gupnp_service_proxy_add_notify' => 'Sets up callback for variable change notification', +'gupnp_service_proxy_callback_set' => 'Set service proxy callback for signal', +'gupnp_service_proxy_get_subscribed' => 'Check whether subscription is valid to the service', +'gupnp_service_proxy_remove_notify' => 'Cancels the variable change notification', +'gupnp_service_proxy_send_action' => 'Send action with multiple parameters synchronously', +'gupnp_service_proxy_set_subscribed' => '(Un)subscribes to the service', +'gupnp_service_thaw_notify' => 'Sends out any pending notifications and stops queuing of new ones', +'gzclose' => 'Close an open gz-file pointer', +'gzcompress' => 'Compress a string', +'gzdecode' => 'Decodes a gzip compressed string', +'gzdeflate' => 'Deflate a string', +'gzencode' => 'Create a gzip compressed string', +'gzeof' => 'Test for EOF on a gz-file pointer', +'gzfile' => 'Read entire gz-file into an array', +'gzgetc' => 'Get character from gz-file pointer', +'gzgets' => 'Get line from file pointer', +'gzgetss' => 'Get line from gz-file pointer and strip HTML tags', +'gzinflate' => 'Inflate a deflated string', +'gzopen' => 'Open gz-file', +'gzpassthru' => 'Output all remaining data on a gz-file pointer', +'gzputs' => 'Alias of gzwrite', +'gzread' => 'Binary-safe gz-file read', +'gzrewind' => 'Rewind the position of a gz-file pointer', +'gzseek' => 'Seek on a gz-file pointer', +'gztell' => 'Tell gz-file pointer read/write position', +'gzuncompress' => 'Uncompress a compressed string', +'gzwrite' => 'Binary-safe gz-file write', +'haruannotation::setBorderStyle' => 'Set the border style of the annotation', +'haruannotation::setHighlightMode' => 'Set the highlighting mode of the annotation', +'haruannotation::setIcon' => 'Set the icon style of the annotation', +'haruannotation::setOpened' => 'Set the initial state of the annotation', +'harudestination::setFit' => 'Set the appearance of the page to fit the window', +'harudestination::setFitB' => 'Set the appearance of the page to fit the bounding box of the page within the window', +'harudestination::setFitBH' => 'Set the appearance of the page to fit the width of the bounding box', +'harudestination::setFitBV' => 'Set the appearance of the page to fit the height of the boudning box', +'harudestination::setFitH' => 'Set the appearance of the page to fit the window width', +'harudestination::setFitR' => 'Set the appearance of the page to fit the specified rectangle', +'harudestination::setFitV' => 'Set the appearance of the page to fit the window height', +'harudestination::setXYZ' => 'Set the appearance of the page', +'harudoc::__construct' => 'Construct new HaruDoc instance', +'harudoc::addPage' => 'Add new page to the document', +'harudoc::addPageLabel' => 'Set the numbering style for the specified range of pages', +'harudoc::createOutline' => 'Create a HaruOutline instance', +'harudoc::getCurrentEncoder' => 'Get HaruEncoder currently used in the document', +'harudoc::getCurrentPage' => 'Return current page of the document', +'harudoc::getEncoder' => 'Get HaruEncoder instance for the specified encoding', +'harudoc::getFont' => 'Get HaruFont instance', +'harudoc::getInfoAttr' => 'Get current value of the specified document attribute', +'harudoc::getPageLayout' => 'Get current page layout', +'harudoc::getPageMode' => 'Get current page mode', +'harudoc::getStreamSize' => 'Get the size of the temporary stream', +'harudoc::insertPage' => 'Insert new page just before the specified page', +'harudoc::loadJPEG' => 'Load a JPEG image', +'harudoc::loadPNG' => 'Load PNG image and return HaruImage instance', +'harudoc::loadRaw' => 'Load a RAW image', +'harudoc::loadTTC' => 'Load the font with the specified index from TTC file', +'harudoc::loadTTF' => 'Load TTF font file', +'harudoc::loadType1' => 'Load Type1 font', +'harudoc::output' => 'Write the document data to the output buffer', +'harudoc::readFromStream' => 'Read data from the temporary stream', +'harudoc::resetError' => 'Reset error state of the document handle', +'harudoc::resetStream' => 'Rewind the temporary stream', +'harudoc::save' => 'Save the document into the specified file', +'harudoc::saveToStream' => 'Save the document into a temporary stream', +'harudoc::setCompressionMode' => 'Set compression mode for the document', +'harudoc::setCurrentEncoder' => 'Set the current encoder for the document', +'harudoc::setEncryptionMode' => 'Set encryption mode for the document', +'harudoc::setInfoAttr' => 'Set the info attribute of the document', +'harudoc::setInfoDateAttr' => 'Set the datetime info attributes of the document', +'harudoc::setOpenAction' => 'Define which page is shown when the document is opened', +'harudoc::setPageLayout' => 'Set how pages should be displayed', +'harudoc::setPageMode' => 'Set how the document should be displayed', +'harudoc::setPagesConfiguration' => 'Set the number of pages per set of pages', +'harudoc::setPassword' => 'Set owner and user passwords for the document', +'harudoc::setPermission' => 'Set permissions for the document', +'harudoc::useCNSEncodings' => 'Enable Chinese simplified encodings', +'harudoc::useCNSFonts' => 'Enable builtin Chinese simplified fonts', +'harudoc::useCNTEncodings' => 'Enable Chinese traditional encodings', +'harudoc::useCNTFonts' => 'Enable builtin Chinese traditional fonts', +'harudoc::useJPEncodings' => 'Enable Japanese encodings', +'harudoc::useJPFonts' => 'Enable builtin Japanese fonts', +'harudoc::useKREncodings' => 'Enable Korean encodings', +'harudoc::useKRFonts' => 'Enable builtin Korean fonts', +'haruencoder::getByteType' => 'Get the type of the byte in the text', +'haruencoder::getType' => 'Get the type of the encoder', +'haruencoder::getUnicode' => 'Convert the specified character to unicode', +'haruencoder::getWritingMode' => 'Get the writing mode of the encoder', +'harufont::getAscent' => 'Get the vertical ascent of the font', +'harufont::getCapHeight' => 'Get the distance from the baseline of uppercase letters', +'harufont::getDescent' => 'Get the vertical descent of the font', +'harufont::getEncodingName' => 'Get the name of the encoding', +'harufont::getFontName' => 'Get the name of the font', +'harufont::getTextWidth' => 'Get the total width of the text, number of characters, number of words and number of spaces', +'harufont::getUnicodeWidth' => 'Get the width of the character in the font', +'harufont::getXHeight' => 'Get the distance from the baseline of lowercase letters', +'harufont::measureText' => 'Calculate the number of characters which can be included within the specified width', +'haruimage::getBitsPerComponent' => 'Get the number of bits used to describe each color component of the image', +'haruimage::getColorSpace' => 'Get the name of the color space', +'haruimage::getHeight' => 'Get the height of the image', +'haruimage::getSize' => 'Get size of the image', +'haruimage::getWidth' => 'Get the width of the image', +'haruimage::setColorMask' => 'Set the color mask of the image', +'haruimage::setMaskImage' => 'Set the image mask', +'haruoutline::setDestination' => 'Set the destination for the outline', +'haruoutline::setOpened' => 'Set the initial state of the outline', +'harupage::arc' => 'Append an arc to the current path', +'harupage::beginText' => 'Begin a text object and set the current text position to (0,0)', +'harupage::circle' => 'Append a circle to the current path', +'harupage::closePath' => 'Append a straight line from the current point to the start point of the path', +'harupage::concat' => 'Concatenate current transformation matrix of the page and the specified matrix', +'harupage::createDestination' => 'Create new HaruDestination instance', +'harupage::createLinkAnnotation' => 'Create new HaruAnnotation instance', +'harupage::createTextAnnotation' => 'Create new HaruAnnotation instance', +'harupage::createURLAnnotation' => 'Create and return new HaruAnnotation instance', +'harupage::curveTo' => 'Append a Bezier curve to the current path', +'harupage::curveTo2' => 'Append a Bezier curve to the current path', +'harupage::curveTo3' => 'Append a Bezier curve to the current path', +'harupage::drawImage' => 'Show image at the page', +'harupage::ellipse' => 'Append an ellipse to the current path', +'harupage::endPath' => 'End current path object without filling and painting operations', +'harupage::endText' => 'End current text object', +'harupage::eofill' => 'Fill current path using even-odd rule', +'harupage::eoFillStroke' => 'Fill current path using even-odd rule, then paint the path', +'harupage::fill' => 'Fill current path using nonzero winding number rule', +'harupage::fillStroke' => 'Fill current path using nonzero winding number rule, then paint the path', +'harupage::getCharSpace' => 'Get the current value of character spacing', +'harupage::getCMYKFill' => 'Get the current filling color', +'harupage::getCMYKStroke' => 'Get the current stroking color', +'harupage::getCurrentFont' => 'Get the currently used font', +'harupage::getCurrentFontSize' => 'Get the current font size', +'harupage::getCurrentPos' => 'Get the current position for path painting', +'harupage::getCurrentTextPos' => 'Get the current position for text printing', +'harupage::getDash' => 'Get the current dash pattern', +'harupage::getFillingColorSpace' => 'Get the current filling color space', +'harupage::getFlatness' => 'Get the flatness of the page', +'harupage::getGMode' => 'Get the current graphics mode', +'harupage::getGrayFill' => 'Get the current filling color', +'harupage::getGrayStroke' => 'Get the current stroking color', +'harupage::getHeight' => 'Get the height of the page', +'harupage::getHorizontalScaling' => 'Get the current value of horizontal scaling', +'harupage::getLineCap' => 'Get the current line cap style', +'harupage::getLineJoin' => 'Get the current line join style', +'harupage::getLineWidth' => 'Get the current line width', +'harupage::getMiterLimit' => 'Get the value of miter limit', +'harupage::getRGBFill' => 'Get the current filling color', +'harupage::getRGBStroke' => 'Get the current stroking color', +'harupage::getStrokingColorSpace' => 'Get the current stroking color space', +'harupage::getTextLeading' => 'Get the current value of line spacing', +'harupage::getTextMatrix' => 'Get the current text transformation matrix of the page', +'harupage::getTextRenderingMode' => 'Get the current text rendering mode', +'harupage::getTextRise' => 'Get the current value of text rising', +'harupage::getTextWidth' => 'Get the width of the text using current fontsize, character spacing and word spacing', +'harupage::getTransMatrix' => 'Get the current transformation matrix of the page', +'harupage::getWidth' => 'Get the width of the page', +'harupage::getWordSpace' => 'Get the current value of word spacing', +'harupage::lineTo' => 'Draw a line from the current point to the specified point', +'harupage::measureText' => 'Calculate the byte length of characters which can be included on one line of the specified width', +'harupage::moveTextPos' => 'Move text position to the specified offset', +'harupage::moveTo' => 'Set starting point for new drawing path', +'harupage::moveToNextLine' => 'Move text position to the start of the next line', +'harupage::rectangle' => 'Append a rectangle to the current path', +'harupage::setCharSpace' => 'Set character spacing for the page', +'harupage::setCMYKFill' => 'Set filling color for the page', +'harupage::setCMYKStroke' => 'Set stroking color for the page', +'harupage::setDash' => 'Set the dash pattern for the page', +'harupage::setFlatness' => 'Set flatness for the page', +'harupage::setFontAndSize' => 'Set font and fontsize for the page', +'harupage::setGrayFill' => 'Set filling color for the page', +'harupage::setGrayStroke' => 'Sets stroking color for the page', +'harupage::setHeight' => 'Set height of the page', +'harupage::setHorizontalScaling' => 'Set horizontal scaling for the page', +'harupage::setLineCap' => 'Set the shape to be used at the ends of lines', +'harupage::setLineJoin' => 'Set line join style for the page', +'harupage::setLineWidth' => 'Set line width for the page', +'harupage::setMiterLimit' => 'Set the current value of the miter limit of the page', +'harupage::setRGBFill' => 'Set filling color for the page', +'harupage::setRGBStroke' => 'Set stroking color for the page', +'harupage::setRotate' => 'Set rotation angle of the page', +'harupage::setSize' => 'Set size and direction of the page', +'harupage::setSlideShow' => 'Set transition style for the page', +'harupage::setTextLeading' => 'Set text leading (line spacing) for the page', +'harupage::setTextMatrix' => 'Set the current text transformation matrix of the page', +'harupage::setTextRenderingMode' => 'Set text rendering mode for the page', +'harupage::setTextRise' => 'Set the current value of text rising', +'harupage::setWidth' => 'Set width of the page', +'harupage::setWordSpace' => 'Set word spacing for the page', +'harupage::showText' => 'Print text at the current position of the page', +'harupage::showTextNextLine' => 'Move the current position to the start of the next line and print the text', +'harupage::stroke' => 'Paint current path', +'harupage::textOut' => 'Print the text on the specified position', +'harupage::textRect' => 'Print the text inside the specified region', +'hash' => 'Generate a hash value (message digest)', +'hash_algos' => 'Return a list of registered hashing algorithms', +'hash_copy' => 'Copy hashing context', +'hash_equals' => 'Timing attack safe string comparison', +'hash_file' => 'Generate a hash value using the contents of a given file', +'hash_final' => 'Finalize an incremental hash and return resulting digest', +'hash_hkdf' => 'Generate a HKDF key derivation of a supplied key input', +'hash_hmac' => 'Generate a keyed hash value using the HMAC method', +'hash_hmac_algos' => 'Return a list of registered hashing algorithms suitable for hash_hmac', +'hash_hmac_file' => 'Generate a keyed hash value using the HMAC method and the contents of a given file', +'hash_init' => 'Initialize an incremental hashing context', +'hash_pbkdf2' => 'Generate a PBKDF2 key derivation of a supplied password', +'hash_update' => 'Pump data into an active hashing context', +'hash_update_file' => 'Pump data into an active hashing context from a file', +'hash_update_stream' => 'Pump data into an active hashing context from an open stream', +'hashcontext::__construct' => 'Private constructor to disallow direct instantiation', +'hashTableObj::clear' => 'Clear all items in the hashTable (To NULL).', +'hashTableObj::get' => 'Fetch class metadata entry by name. Returns "" if no entry +matches the name. Note that the search is case sensitive.', +'hashTableObj::nextkey' => 'Return the next key or first key if previousKey = NULL. +Return NULL if no item is in the hashTable or end of hashTable is +reached', +'hashTableObj::remove' => 'Remove a metadata entry in the hashTable. Returns MS_SUCCESS/MS_FAILURE.', +'hashTableObj::set' => 'Set a metadata entry in the hashTable. Returns MS_SUCCESS/MS_FAILURE.', +'header' => 'Send a raw HTTP header', +'header_register_callback' => 'Call a header function', +'header_remove' => 'Remove previously set headers', +'headers_list' => 'Returns a list of response headers sent (or ready to send)', +'headers_sent' => 'Checks if or where headers have been sent', +'hebrev' => 'Convert logical Hebrew text to visual text', +'hebrevc' => 'Convert logical Hebrew text to visual text with newline conversion', +'hex2bin' => 'Decodes a hexadecimally encoded binary string', +'hexdec' => 'Hexadecimal to decimal', +'highlight_file' => 'Syntax highlighting of a file', +'highlight_string' => 'Syntax highlighting of a string', +'hrtime' => 'Get the system\'s high resolution time', +'hrtime_performancecounter::getFrequency' => 'Timer frequency in ticks per second', +'hrtime_performancecounter::getTicks' => 'Current ticks from the system', +'hrtime_performancecounter::getTicksSince' => 'Ticks elapsed since the given value', +'hrtime_stopwatch::getElapsedTicks' => 'Get elapsed ticks for all intervals', +'hrtime_stopwatch::getElapsedTime' => 'Get elapsed time for all intervals', +'hrtime_stopwatch::getLastElapsedTicks' => 'Get elapsed ticks for the last interval', +'hrtime_stopwatch::getLastElapsedTime' => 'Get elapsed time for the last interval', +'hrtime_stopwatch::isRunning' => 'Whether the measurement is running', +'hrtime_stopwatch::start' => 'Start time measurement', +'hrtime_stopwatch::stop' => 'Stop time measurement', +'html_entity_decode' => 'Convert HTML entities to their corresponding characters', +'htmlentities' => 'Convert all applicable characters to HTML entities', +'htmlspecialchars' => 'Convert special characters to HTML entities', +'htmlspecialchars_decode' => 'Convert special HTML entities back to characters', +'http\Client::__construct' => 'Create a new HTTP client.', +'http\Client::addCookies' => 'Add custom cookies. +See http\Client::setCookies().', +'http\Client::addSslOptions' => 'Add specific SSL options. +See http\Client::setSslOptions(), http\Client::setOptions() and http\Client\Curl\$ssl options.', +'http\Client::attach' => 'Implements SplSubject. Attach another observer. +Attached observers will be notified with progress of each transfer.', +'http\Client::configure' => 'Configure the client\'s low level options. + +> ***NOTE:*** +> This method has been added in v2.3.0.', +'http\Client::count' => 'Implements Countable. Retrieve the number of enqueued requests. + +> ***NOTE:*** +> The enqueued requests are counted without regard whether they are finished or not.', +'http\Client::dequeue' => 'Dequeue the http\Client\Request $request. + +See http\Client::requeue(), if you want to requeue the request, instead of calling http\Client::dequeue() and then http\Client::enqueue().', +'http\Client::detach' => 'Implements SplSubject. Detach $observer, which has been previously attached.', +'http\Client::enableEvents' => 'Enable usage of an event library like libevent, which might improve performance with big socket sets. + +> ***NOTE:*** +> This method has been deprecated in 2.3.0, please use http\Client::configure() instead.', +'http\Client::enablePipelining' => 'Enable sending pipelined requests to the same host if the driver supports it. + +> ***NOTE:*** +> This method has been deprecated in 2.3.0, please use http\Client::configure() instead.', +'http\Client::enqueue' => 'Add another http\Client\Request to the request queue. +If the optional callback $cb returns true, the request will be automatically dequeued. + +> ***Note:*** +> The http\Client\Response object resulting from the request is always stored +> internally to be retrieved at a later time, __even__ when $cb is used. +> +> If you are about to send a lot of requests and do __not__ need the response +> after executing the callback, you can use http\Client::getResponse() within +> the callback to keep the memory usage level as low as possible. + +See http\Client::dequeue() and http\Client::send().', +'http\Client::getAvailableConfiguration' => 'Get a list of available configuration options and their default values. + +See f.e. the [configuration options for the Curl driver](http/Client/Curl#Configuration:).', +'http\Client::getAvailableDrivers' => 'List available drivers.', +'http\Client::getAvailableOptions' => 'Retrieve a list of available request options and their default values. + +See f.e. the [request options for the Curl driver](http/Client/Curl#Options:).', +'http\Client::getCookies' => 'Get priorly set custom cookies. +See http\Client::setCookies().', +'http\Client::getHistory' => 'Simply returns the http\Message chain representing the request/response history. + +> ***NOTE:*** +> The history is only recorded while http\Client::$recordHistory is true.', +'http\Client::getObservers' => 'Returns the SplObjectStorage holding attached observers.', +'http\Client::getOptions' => 'Get priorly set options. +See http\Client::setOptions().', +'http\Client::getProgressInfo' => 'Retrieve the progress information for $request.', +'http\Client::getResponse' => 'Retrieve the corresponding response of an already finished request, or the last received response if $request is not set. + +> ***NOTE:*** +> If $request is NULL, then the response is removed from the internal storage (stack-like operation).', +'http\Client::getSslOptions' => 'Retrieve priorly set SSL options. +See http\Client::getOptions() and http\Client::setSslOptions().', +'http\Client::getTransferInfo' => 'Get transfer related information for a running or finished request.', +'http\Client::notify' => 'Implements SplSubject. Notify attached observers about progress with $request.', +'http\Client::once' => 'Perform outstanding transfer actions. +See http\Client::wait() for the completing interface.', +'http\Client::requeue' => 'Requeue an http\Client\Request. + +The difference simply is, that this method, in contrast to http\Client::enqueue(), does not throw an http\Exception when the request to queue is already enqueued and dequeues it automatically prior enqueueing it again.', +'http\Client::reset' => 'Reset the client to the initial state.', +'http\Client::send' => 'Send all enqueued requests. +See http\Client::once() and http\Client::wait() for a more fine grained interface.', +'http\Client::setCookies' => 'Set custom cookies. +See http\Client::addCookies() and http\Client::getCookies().', +'http\Client::setDebug' => 'Set client debugging callback. + +> ***NOTE:*** +> This method has been added in v2.6.0, resp. v3.1.0.', +'http\Client::setOptions' => 'Set client options. +See http\Client\Curl. + +> ***NOTE:*** +> Only options specified prior enqueueing a request are applied to the request.', +'http\Client::setSslOptions' => 'Specifically set SSL options. +See http\Client::setOptions() and http\Client\Curl\$ssl options.', +'http\Client::wait' => 'Wait for $timeout seconds for transfers to provide data. +This is the completion call to http\Client::once().', +'http\Client\Curl\User::init' => 'Initialize the event loop.', +'http\Client\Curl\User::once' => 'Run the loop as long as it does not block. + +> ***NOTE:*** +> This method is called by http\Client::once(), so it does not need to have an actual implementation if http\Client::once() is never called.', +'http\Client\Curl\User::send' => 'Run the loop. + +> ***NOTE:*** +> This method is called by http\Client::send(), so it does not need to have an actual implementation if http\Client::send() is never called.', +'http\Client\Curl\User::socket' => 'Register (or deregister) a socket watcher.', +'http\Client\Curl\User::timer' => 'Register a timeout watcher.', +'http\Client\Curl\User::wait' => 'Wait/poll/select (block the loop) until events fire. + +> ***NOTE:*** +> This method is called by http\Client::wait(), so it does not need to have an actual implementation if http\Client::wait() is never called.', +'http\Client\Request::__construct' => 'Create a new client request message to be enqueued and sent by http\Client.', +'http\Client\Request::__toString' => 'Retrieve the message serialized to a string. +Alias of http\Message::toString().', +'http\Client\Request::addBody' => 'Append the data of $body to the message\'s body. +See http\Message::setBody() and http\Message\Body::append().', +'http\Client\Request::addHeader' => 'Add an header, appending to already existing headers. +See http\Message::addHeaders() and http\Message::setHeader().', +'http\Client\Request::addHeaders' => 'Add headers, optionally appending values, if header keys already exist. +See http\Message::addHeader() and http\Message::setHeaders().', +'http\Client\Request::addQuery' => 'Add querystring data. +See http\Client\Request::setQuery() and http\Message::setRequestUrl().', +'http\Client\Request::addSslOptions' => 'Add specific SSL options. +See http\Client\Request::setSslOptions(), http\Client\Request::setOptions() and http\Client\Curl\$ssl options.', +'http\Client\Request::count' => 'Implements Countable.', +'http\Client\Request::current' => 'Implements iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Client\Request::detach' => 'Detach a clone of this message from any message chain.', +'http\Client\Request::getBody' => 'Retrieve the message\'s body. +See http\Message::setBody().', +'http\Client\Request::getContentType' => 'Extract the currently set "Content-Type" header. +See http\Client\Request::setContentType().', +'http\Client\Request::getHeader' => 'Retrieve a single header, optionally hydrated into a http\Header extending class.', +'http\Client\Request::getHeaders' => 'Retrieve all message headers. +See http\Message::setHeaders() and http\Message::getHeader().', +'http\Client\Request::getHttpVersion' => 'Retrieve the HTTP protocol version of the message. +See http\Message::setHttpVersion().', +'http\Client\Request::getInfo' => 'Retrieve the first line of a request or response message. +See http\Message::setInfo and also: + +* http\Message::getType() +* http\Message::getHttpVersion() +* http\Message::getResponseCode() +* http\Message::getResponseStatus() +* http\Message::getRequestMethod() +* http\Message::getRequestUrl()', +'http\Client\Request::getOptions' => 'Get priorly set options. +See http\Client\Request::setOptions().', +'http\Client\Request::getParentMessage' => 'Retrieve any parent message. +See http\Message::reverse().', +'http\Client\Request::getQuery' => 'Retrieve the currently set querystring.', +'http\Client\Request::getRequestMethod' => 'Retrieve the request method of the message. +See http\Message::setRequestMethod() and http\Message::getRequestUrl().', +'http\Client\Request::getRequestUrl' => 'Retrieve the request URL of the message. +See http\Message::setRequestUrl().', +'http\Client\Request::getResponseCode' => 'Retrieve the response code of the message. +See http\Message::setResponseCode() and http\Message::getResponseStatus().', +'http\Client\Request::getResponseStatus' => 'Retrieve the response status of the message. +See http\Message::setResponseStatus() and http\Message::getResponseCode().', +'http\Client\Request::getSslOptions' => 'Retrieve priorly set SSL options. +See http\Client\Request::getOptions() and http\Client\Request::setSslOptions().', +'http\Client\Request::getType' => 'Retrieve the type of the message. +See http\Message::setType() and http\Message::getInfo().', +'http\Client\Request::isMultipart' => 'Check whether this message is a multipart message based on it\'s content type. +If the message is a multipart message and a reference $boundary is given, the boundary string of the multipart message will be stored in $boundary. + +See http\Message::splitMultipartBody().', +'http\Client\Request::key' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Client\Request::next' => 'Implements Iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Client\Request::prepend' => 'Prepend message(s) $message to this message, or the top most message of this message chain. + +> ***NOTE:*** +> The message chains must not overlap.', +'http\Client\Request::reverse' => 'Reverse the message chain and return the former top-most message. + +> ***NOTE:*** +> Message chains are ordered in reverse-parsed order by default, i.e. the last parsed message is the message you\'ll receive from any call parsing HTTP messages. +> +> This call re-orders the messages of the chain and returns the message that was parsed first with any later parsed messages re-parentized.', +'http\Client\Request::rewind' => 'Implements Iterator.', +'http\Client\Request::serialize' => 'Implements Serializable.', +'http\Client\Request::setBody' => 'Set the message\'s body. +See http\Message::getBody() and http\Message::addBody().', +'http\Client\Request::setContentType' => 'Set the MIME content type of the request message.', +'http\Client\Request::setHeader' => 'Set a single header. +See http\Message::getHeader() and http\Message::addHeader(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Client\Request::setHeaders' => 'Set the message headers. +See http\Message::getHeaders() and http\Message::addHeaders(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Client\Request::setHttpVersion' => 'Set the HTTP protocol version of the message. +See http\Message::getHttpVersion().', +'http\Client\Request::setInfo' => 'Set the complete message info, i.e. type and response resp. request information, at once. +See http\Message::getInfo().', +'http\Client\Request::setOptions' => 'Set client options. +See http\Client::setOptions() and http\Client\Curl. + +Request specific options override general options which were set in the client. + +> ***NOTE:*** +> Only options specified prior enqueueing a request are applied to the request.', +'http\Client\Request::setQuery' => '(Re)set the querystring. +See http\Client\Request::addQuery() and http\Message::setRequestUrl().', +'http\Client\Request::setRequestMethod' => 'Set the request method of the message. +See http\Message::getRequestMethod() and http\Message::setRequestUrl().', +'http\Client\Request::setRequestUrl' => 'Set the request URL of the message. +See http\Message::getRequestUrl() and http\Message::setRequestMethod().', +'http\Client\Request::setResponseCode' => 'Set the response status code. +See http\Message::getResponseCode() and http\Message::setResponseStatus(). + +> ***NOTE:*** +> This method also resets the response status phrase to the default for that code.', +'http\Client\Request::setResponseStatus' => 'Set the response status phrase. +See http\Message::getResponseStatus() and http\Message::setResponseCode().', +'http\Client\Request::setSslOptions' => 'Specifically set SSL options. +See http\Client\Request::setOptions() and http\Client\Curl\$ssl options.', +'http\Client\Request::setType' => 'Set the message type and reset the message info. +See http\Message::getType() and http\Message::setInfo().', +'http\Client\Request::splitMultipartBody' => 'Splits the body of a multipart message. +See http\Message::isMultipart() and http\Message\Body::addPart().', +'http\Client\Request::toCallback' => 'Stream the message through a callback.', +'http\Client\Request::toStream' => 'Stream the message into stream $stream, starting from $offset, streaming $maxlen at most.', +'http\Client\Request::toString' => 'Retrieve the message serialized to a string.', +'http\Client\Request::unserialize' => 'Implements Serializable.', +'http\Client\Request::valid' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Client\Response::__construct' => 'Create a new HTTP message.', +'http\Client\Response::__toString' => 'Retrieve the message serialized to a string. +Alias of http\Message::toString().', +'http\Client\Response::addBody' => 'Append the data of $body to the message\'s body. +See http\Message::setBody() and http\Message\Body::append().', +'http\Client\Response::addHeader' => 'Add an header, appending to already existing headers. +See http\Message::addHeaders() and http\Message::setHeader().', +'http\Client\Response::addHeaders' => 'Add headers, optionally appending values, if header keys already exist. +See http\Message::addHeader() and http\Message::setHeaders().', +'http\Client\Response::count' => 'Implements Countable.', +'http\Client\Response::current' => 'Implements iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Client\Response::detach' => 'Detach a clone of this message from any message chain.', +'http\Client\Response::getBody' => 'Retrieve the message\'s body. +See http\Message::setBody().', +'http\Client\Response::getCookies' => 'Extract response cookies. +Parses any "Set-Cookie" response headers into an http\Cookie list. See http\Cookie::__construct().', +'http\Client\Response::getHeader' => 'Retrieve a single header, optionally hydrated into a http\Header extending class.', +'http\Client\Response::getHeaders' => 'Retrieve all message headers. +See http\Message::setHeaders() and http\Message::getHeader().', +'http\Client\Response::getHttpVersion' => 'Retrieve the HTTP protocol version of the message. +See http\Message::setHttpVersion().', +'http\Client\Response::getInfo' => 'Retrieve the first line of a request or response message. +See http\Message::setInfo and also: + +* http\Message::getType() +* http\Message::getHttpVersion() +* http\Message::getResponseCode() +* http\Message::getResponseStatus() +* http\Message::getRequestMethod() +* http\Message::getRequestUrl()', +'http\Client\Response::getParentMessage' => 'Retrieve any parent message. +See http\Message::reverse().', +'http\Client\Response::getRequestMethod' => 'Retrieve the request method of the message. +See http\Message::setRequestMethod() and http\Message::getRequestUrl().', +'http\Client\Response::getRequestUrl' => 'Retrieve the request URL of the message. +See http\Message::setRequestUrl().', +'http\Client\Response::getResponseCode' => 'Retrieve the response code of the message. +See http\Message::setResponseCode() and http\Message::getResponseStatus().', +'http\Client\Response::getResponseStatus' => 'Retrieve the response status of the message. +See http\Message::setResponseStatus() and http\Message::getResponseCode().', +'http\Client\Response::getTransferInfo' => 'Retrieve transfer related information after the request has completed. +See http\Client::getTransferInfo().', +'http\Client\Response::getType' => 'Retrieve the type of the message. +See http\Message::setType() and http\Message::getInfo().', +'http\Client\Response::isMultipart' => 'Check whether this message is a multipart message based on it\'s content type. +If the message is a multipart message and a reference $boundary is given, the boundary string of the multipart message will be stored in $boundary. + +See http\Message::splitMultipartBody().', +'http\Client\Response::key' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Client\Response::next' => 'Implements Iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Client\Response::prepend' => 'Prepend message(s) $message to this message, or the top most message of this message chain. + +> ***NOTE:*** +> The message chains must not overlap.', +'http\Client\Response::reverse' => 'Reverse the message chain and return the former top-most message. + +> ***NOTE:*** +> Message chains are ordered in reverse-parsed order by default, i.e. the last parsed message is the message you\'ll receive from any call parsing HTTP messages. +> +> This call re-orders the messages of the chain and returns the message that was parsed first with any later parsed messages re-parentized.', +'http\Client\Response::rewind' => 'Implements Iterator.', +'http\Client\Response::serialize' => 'Implements Serializable.', +'http\Client\Response::setBody' => 'Set the message\'s body. +See http\Message::getBody() and http\Message::addBody().', +'http\Client\Response::setHeader' => 'Set a single header. +See http\Message::getHeader() and http\Message::addHeader(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Client\Response::setHeaders' => 'Set the message headers. +See http\Message::getHeaders() and http\Message::addHeaders(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Client\Response::setHttpVersion' => 'Set the HTTP protocol version of the message. +See http\Message::getHttpVersion().', +'http\Client\Response::setInfo' => 'Set the complete message info, i.e. type and response resp. request information, at once. +See http\Message::getInfo().', +'http\Client\Response::setRequestMethod' => 'Set the request method of the message. +See http\Message::getRequestMethod() and http\Message::setRequestUrl().', +'http\Client\Response::setRequestUrl' => 'Set the request URL of the message. +See http\Message::getRequestUrl() and http\Message::setRequestMethod().', +'http\Client\Response::setResponseCode' => 'Set the response status code. +See http\Message::getResponseCode() and http\Message::setResponseStatus(). + +> ***NOTE:*** +> This method also resets the response status phrase to the default for that code.', +'http\Client\Response::setResponseStatus' => 'Set the response status phrase. +See http\Message::getResponseStatus() and http\Message::setResponseCode().', +'http\Client\Response::setType' => 'Set the message type and reset the message info. +See http\Message::getType() and http\Message::setInfo().', +'http\Client\Response::splitMultipartBody' => 'Splits the body of a multipart message. +See http\Message::isMultipart() and http\Message\Body::addPart().', +'http\Client\Response::toCallback' => 'Stream the message through a callback.', +'http\Client\Response::toStream' => 'Stream the message into stream $stream, starting from $offset, streaming $maxlen at most.', +'http\Client\Response::toString' => 'Retrieve the message serialized to a string.', +'http\Client\Response::unserialize' => 'Implements Serializable.', +'http\Client\Response::valid' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Cookie::__construct' => 'Create a new cookie list.', +'http\Cookie::__toString' => 'String cast handler. Alias of http\Cookie::toString().', +'http\Cookie::addCookie' => 'Add a cookie. +See http\Cookie::setCookie() and http\Cookie::addCookies().', +'http\Cookie::addCookies' => '(Re)set the cookies. +See http\Cookie::setCookies().', +'http\Cookie::addExtra' => 'Add an extra attribute to the cookie list. +See http\Cookie::setExtra().', +'http\Cookie::addExtras' => 'Add several extra attributes. +See http\Cookie::addExtra().', +'http\Cookie::getCookie' => 'Retrieve a specific cookie value. +See http\Cookie::setCookie().', +'http\Cookie::getCookies' => 'Get the list of cookies. +See http\Cookie::setCookies().', +'http\Cookie::getDomain' => 'Retrieve the effective domain of the cookie list. +See http\Cookie::setDomain().', +'http\Cookie::getExpires' => 'Get the currently set expires attribute. +See http\Cookie::setExpires(). + +> ***NOTE:*** +> A return value of -1 means that the attribute is not set.', +'http\Cookie::getExtra' => 'Retrieve an extra attribute. +See http\Cookie::setExtra().', +'http\Cookie::getExtras' => 'Retrieve the list of extra attributes. +See http\Cookie::setExtras().', +'http\Cookie::getFlags' => 'Get the currently set flags. +See http\Cookie::SECURE and http\Cookie::HTTPONLY constants.', +'http\Cookie::getMaxAge' => 'Get the currently set max-age attribute of the cookie list. +See http\Cookie::setMaxAge(). + +> ***NOTE:*** +> A return value of -1 means that the attribute is not set.', +'http\Cookie::getPath' => 'Retrieve the path the cookie(s) of this cookie list are effective at. +See http\Cookie::setPath().', +'http\Cookie::setCookie' => '(Re)set a cookie. +See http\Cookie::addCookie() and http\Cookie::setCookies(). + +> ***NOTE:*** +> The cookie will be deleted from the list if $cookie_value is NULL.', +'http\Cookie::setCookies' => '(Re)set the cookies. +See http\Cookie::addCookies().', +'http\Cookie::setDomain' => 'Set the effective domain of the cookie list. +See http\Cookie::setPath().', +'http\Cookie::setExpires' => 'Set the traditional expires timestamp. +See http\Cookie::setMaxAge() for a safer alternative.', +'http\Cookie::setExtra' => '(Re)set an extra attribute. +See http\Cookie::addExtra(). + +> ***NOTE:*** +> The attribute will be removed from the extras list if $extra_value is NULL.', +'http\Cookie::setExtras' => '(Re)set the extra attributes. +See http\Cookie::addExtras().', +'http\Cookie::setFlags' => 'Set the flags to specified $value. +See http\Cookie::SECURE and http\Cookie::HTTPONLY constants.', +'http\Cookie::setMaxAge' => 'Set the maximum age the cookie may have on the client side. +This is a client clock departure safe alternative to the "expires" attribute. +See http\Cookie::setExpires().', +'http\Cookie::setPath' => 'Set the path the cookie(s) of this cookie list should be effective at. +See http\Cookie::setDomain().', +'http\Cookie::toArray' => 'Get the cookie list as array.', +'http\Cookie::toString' => 'Retrieve the string representation of the cookie list. +See http\Cookie::toArray().', +'http\Encoding\Stream::__construct' => 'Base constructor for encoding stream implementations.', +'http\Encoding\Stream::done' => 'Check whether the encoding stream is already done.', +'http\Encoding\Stream::finish' => 'Finish and reset the encoding stream. +Returns any pending data.', +'http\Encoding\Stream::flush' => 'Flush the encoding stream. +Returns any pending data.', +'http\Encoding\Stream::update' => 'Update the encoding stream with more input.', +'http\Encoding\Stream\Debrotli::__construct' => 'Base constructor for encoding stream implementations.', +'http\Encoding\Stream\Debrotli::decode' => 'Decode brotli encoded data.', +'http\Encoding\Stream\Debrotli::done' => 'Check whether the encoding stream is already done.', +'http\Encoding\Stream\Debrotli::finish' => 'Finish and reset the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Debrotli::flush' => 'Flush the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Debrotli::update' => 'Update the encoding stream with more input.', +'http\Encoding\Stream\Dechunk::__construct' => 'Base constructor for encoding stream implementations.', +'http\Encoding\Stream\Dechunk::decode' => 'Decode chunked encoded data.', +'http\Encoding\Stream\Dechunk::done' => 'Check whether the encoding stream is already done.', +'http\Encoding\Stream\Dechunk::finish' => 'Finish and reset the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Dechunk::flush' => 'Flush the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Dechunk::update' => 'Update the encoding stream with more input.', +'http\Encoding\Stream\Deflate::__construct' => 'Base constructor for encoding stream implementations.', +'http\Encoding\Stream\Deflate::done' => 'Check whether the encoding stream is already done.', +'http\Encoding\Stream\Deflate::encode' => 'Encode data with deflate/zlib/gzip encoding.', +'http\Encoding\Stream\Deflate::finish' => 'Finish and reset the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Deflate::flush' => 'Flush the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Deflate::update' => 'Update the encoding stream with more input.', +'http\Encoding\Stream\Enbrotli::__construct' => 'Base constructor for encoding stream implementations.', +'http\Encoding\Stream\Enbrotli::done' => 'Check whether the encoding stream is already done.', +'http\Encoding\Stream\Enbrotli::encode' => 'Encode data with brotli encoding.', +'http\Encoding\Stream\Enbrotli::finish' => 'Finish and reset the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Enbrotli::flush' => 'Flush the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Enbrotli::update' => 'Update the encoding stream with more input.', +'http\Encoding\Stream\Inflate::__construct' => 'Base constructor for encoding stream implementations.', +'http\Encoding\Stream\Inflate::decode' => 'Decode deflate/zlib/gzip encoded data.', +'http\Encoding\Stream\Inflate::done' => 'Check whether the encoding stream is already done.', +'http\Encoding\Stream\Inflate::finish' => 'Finish and reset the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Inflate::flush' => 'Flush the encoding stream. +Returns any pending data.', +'http\Encoding\Stream\Inflate::update' => 'Update the encoding stream with more input.', +'http\Env::getRequestBody' => 'Retrieve the current HTTP request\'s body.', +'http\Env::getRequestHeader' => 'Retrieve one or all headers of the current HTTP request.', +'http\Env::getResponseCode' => 'Get the HTTP response code to send.', +'http\Env::getResponseHeader' => 'Get one or all HTTP response headers to be sent.', +'http\Env::getResponseStatusForAllCodes' => 'Retrieve a list of all known HTTP response status.', +'http\Env::getResponseStatusForCode' => 'Retrieve the string representation of specified HTTP response code.', +'http\Env::negotiate' => 'Generic negotiator. For specific client negotiation see http\Env::negotiateContentType() and related methods. + +> ***NOTE:*** +> The first element of $supported serves as a default if no operand matches.', +'http\Env::negotiateCharset' => 'Negotiate the client\'s preferred character set. + +> ***NOTE:*** +> The first element of $supported character sets serves as a default if no character set matches.', +'http\Env::negotiateContentType' => 'Negotiate the client\'s preferred MIME content type. + +> ***NOTE:*** +> The first element of $supported content types serves as a default if no content-type matches.', +'http\Env::negotiateEncoding' => 'Negotiate the client\'s preferred encoding. + +> ***NOTE:*** +> The first element of $supported encodings serves as a default if no encoding matches.', +'http\Env::negotiateLanguage' => 'Negotiate the client\'s preferred language. + +> ***NOTE:*** +> The first element of $supported languages serves as a default if no language matches.', +'http\Env::setResponseCode' => 'Set the HTTP response code to send.', +'http\Env::setResponseHeader' => 'Set a response header, either replacing a prior set header, or appending the new header value, depending on $replace. + +If no $header_value is specified, or $header_value is NULL, then a previously set header with the same key will be deleted from the list. + +If $response_code is not 0, the response status code is updated accordingly.', +'http\Env\Request::__construct' => 'Create an instance of the server\'s current HTTP request. + +Upon construction, the http\Env\Request acquires http\QueryString instances of query parameters ($\_GET) and form parameters ($\_POST). + +It also compiles an array of uploaded files ($\_FILES) more comprehensive than the original $\_FILES array, see http\Env\Request::getFiles() for that matter.', +'http\Env\Request::__toString' => 'Retrieve the message serialized to a string. +Alias of http\Message::toString().', +'http\Env\Request::addBody' => 'Append the data of $body to the message\'s body. +See http\Message::setBody() and http\Message\Body::append().', +'http\Env\Request::addHeader' => 'Add an header, appending to already existing headers. +See http\Message::addHeaders() and http\Message::setHeader().', +'http\Env\Request::addHeaders' => 'Add headers, optionally appending values, if header keys already exist. +See http\Message::addHeader() and http\Message::setHeaders().', +'http\Env\Request::count' => 'Implements Countable.', +'http\Env\Request::current' => 'Implements iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Env\Request::detach' => 'Detach a clone of this message from any message chain.', +'http\Env\Request::getBody' => 'Retrieve the message\'s body. +See http\Message::setBody().', +'http\Env\Request::getCookie' => 'Retrieve an URL query value ($_GET)', +'http\Env\Request::getFiles' => 'Retrieve the uploaded files list ($_FILES)', +'http\Env\Request::getForm' => 'Retrieve a form value ($_POST)', +'http\Env\Request::getHeader' => 'Retrieve a single header, optionally hydrated into a http\Header extending class.', +'http\Env\Request::getHeaders' => 'Retrieve all message headers. +See http\Message::setHeaders() and http\Message::getHeader().', +'http\Env\Request::getHttpVersion' => 'Retrieve the HTTP protocol version of the message. +See http\Message::setHttpVersion().', +'http\Env\Request::getInfo' => 'Retrieve the first line of a request or response message. +See http\Message::setInfo and also: + +* http\Message::getType() +* http\Message::getHttpVersion() +* http\Message::getResponseCode() +* http\Message::getResponseStatus() +* http\Message::getRequestMethod() +* http\Message::getRequestUrl()', +'http\Env\Request::getParentMessage' => 'Retrieve any parent message. +See http\Message::reverse().', +'http\Env\Request::getQuery' => 'Retrieve an URL query value ($_GET)', +'http\Env\Request::getRequestMethod' => 'Retrieve the request method of the message. +See http\Message::setRequestMethod() and http\Message::getRequestUrl().', +'http\Env\Request::getRequestUrl' => 'Retrieve the request URL of the message. +See http\Message::setRequestUrl().', +'http\Env\Request::getResponseCode' => 'Retrieve the response code of the message. +See http\Message::setResponseCode() and http\Message::getResponseStatus().', +'http\Env\Request::getResponseStatus' => 'Retrieve the response status of the message. +See http\Message::setResponseStatus() and http\Message::getResponseCode().', +'http\Env\Request::getType' => 'Retrieve the type of the message. +See http\Message::setType() and http\Message::getInfo().', +'http\Env\Request::isMultipart' => 'Check whether this message is a multipart message based on it\'s content type. +If the message is a multipart message and a reference $boundary is given, the boundary string of the multipart message will be stored in $boundary. + +See http\Message::splitMultipartBody().', +'http\Env\Request::key' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Env\Request::next' => 'Implements Iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Env\Request::prepend' => 'Prepend message(s) $message to this message, or the top most message of this message chain. + +> ***NOTE:*** +> The message chains must not overlap.', +'http\Env\Request::reverse' => 'Reverse the message chain and return the former top-most message. + +> ***NOTE:*** +> Message chains are ordered in reverse-parsed order by default, i.e. the last parsed message is the message you\'ll receive from any call parsing HTTP messages. +> +> This call re-orders the messages of the chain and returns the message that was parsed first with any later parsed messages re-parentized.', +'http\Env\Request::rewind' => 'Implements Iterator.', +'http\Env\Request::serialize' => 'Implements Serializable.', +'http\Env\Request::setBody' => 'Set the message\'s body. +See http\Message::getBody() and http\Message::addBody().', +'http\Env\Request::setHeader' => 'Set a single header. +See http\Message::getHeader() and http\Message::addHeader(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Env\Request::setHeaders' => 'Set the message headers. +See http\Message::getHeaders() and http\Message::addHeaders(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Env\Request::setHttpVersion' => 'Set the HTTP protocol version of the message. +See http\Message::getHttpVersion().', +'http\Env\Request::setInfo' => 'Set the complete message info, i.e. type and response resp. request information, at once. +See http\Message::getInfo().', +'http\Env\Request::setRequestMethod' => 'Set the request method of the message. +See http\Message::getRequestMethod() and http\Message::setRequestUrl().', +'http\Env\Request::setRequestUrl' => 'Set the request URL of the message. +See http\Message::getRequestUrl() and http\Message::setRequestMethod().', +'http\Env\Request::setResponseCode' => 'Set the response status code. +See http\Message::getResponseCode() and http\Message::setResponseStatus(). + +> ***NOTE:*** +> This method also resets the response status phrase to the default for that code.', +'http\Env\Request::setResponseStatus' => 'Set the response status phrase. +See http\Message::getResponseStatus() and http\Message::setResponseCode().', +'http\Env\Request::setType' => 'Set the message type and reset the message info. +See http\Message::getType() and http\Message::setInfo().', +'http\Env\Request::splitMultipartBody' => 'Splits the body of a multipart message. +See http\Message::isMultipart() and http\Message\Body::addPart().', +'http\Env\Request::toCallback' => 'Stream the message through a callback.', +'http\Env\Request::toStream' => 'Stream the message into stream $stream, starting from $offset, streaming $maxlen at most.', +'http\Env\Request::toString' => 'Retrieve the message serialized to a string.', +'http\Env\Request::unserialize' => 'Implements Serializable.', +'http\Env\Request::valid' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Env\Response::__construct' => 'Create a new env response message instance.', +'http\Env\Response::__invoke' => 'Output buffer handler', +'http\Env\Response::__toString' => 'Retrieve the message serialized to a string. +Alias of http\Message::toString().', +'http\Env\Response::addBody' => 'Append the data of $body to the message\'s body. +See http\Message::setBody() and http\Message\Body::append().', +'http\Env\Response::addHeader' => 'Add an header, appending to already existing headers. +See http\Message::addHeaders() and http\Message::setHeader().', +'http\Env\Response::addHeaders' => 'Add headers, optionally appending values, if header keys already exist. +See http\Message::addHeader() and http\Message::setHeaders().', +'http\Env\Response::count' => 'Implements Countable.', +'http\Env\Response::current' => 'Implements iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Env\Response::detach' => 'Detach a clone of this message from any message chain.', +'http\Env\Response::getBody' => 'Retrieve the message\'s body. +See http\Message::setBody().', +'http\Env\Response::getHeader' => 'Retrieve a single header, optionally hydrated into a http\Header extending class.', +'http\Env\Response::getHeaders' => 'Retrieve all message headers. +See http\Message::setHeaders() and http\Message::getHeader().', +'http\Env\Response::getHttpVersion' => 'Retrieve the HTTP protocol version of the message. +See http\Message::setHttpVersion().', +'http\Env\Response::getInfo' => 'Retrieve the first line of a request or response message. +See http\Message::setInfo and also: + +* http\Message::getType() +* http\Message::getHttpVersion() +* http\Message::getResponseCode() +* http\Message::getResponseStatus() +* http\Message::getRequestMethod() +* http\Message::getRequestUrl()', +'http\Env\Response::getParentMessage' => 'Retrieve any parent message. +See http\Message::reverse().', +'http\Env\Response::getRequestMethod' => 'Retrieve the request method of the message. +See http\Message::setRequestMethod() and http\Message::getRequestUrl().', +'http\Env\Response::getRequestUrl' => 'Retrieve the request URL of the message. +See http\Message::setRequestUrl().', +'http\Env\Response::getResponseCode' => 'Retrieve the response code of the message. +See http\Message::setResponseCode() and http\Message::getResponseStatus().', +'http\Env\Response::getResponseStatus' => 'Retrieve the response status of the message. +See http\Message::setResponseStatus() and http\Message::getResponseCode().', +'http\Env\Response::getType' => 'Retrieve the type of the message. +See http\Message::setType() and http\Message::getInfo().', +'http\Env\Response::isCachedByEtag' => 'Manually test the header $header_name of the environment\'s request for a cache hit. +http\Env\Response::send() checks that itself, though.', +'http\Env\Response::isCachedByLastModified' => 'Manually test the header $header_name of the environment\'s request for a cache hit. +http\Env\Response::send() checks that itself, though.', +'http\Env\Response::isMultipart' => 'Check whether this message is a multipart message based on it\'s content type. +If the message is a multipart message and a reference $boundary is given, the boundary string of the multipart message will be stored in $boundary. + +See http\Message::splitMultipartBody().', +'http\Env\Response::key' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Env\Response::next' => 'Implements Iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Env\Response::prepend' => 'Prepend message(s) $message to this message, or the top most message of this message chain. + +> ***NOTE:*** +> The message chains must not overlap.', +'http\Env\Response::reverse' => 'Reverse the message chain and return the former top-most message. + +> ***NOTE:*** +> Message chains are ordered in reverse-parsed order by default, i.e. the last parsed message is the message you\'ll receive from any call parsing HTTP messages. +> +> This call re-orders the messages of the chain and returns the message that was parsed first with any later parsed messages re-parentized.', +'http\Env\Response::rewind' => 'Implements Iterator.', +'http\Env\Response::send' => 'Send the response through the SAPI or $stream', +'http\Env\Response::serialize' => 'Implements Serializable.', +'http\Env\Response::setBody' => 'Set the message\'s body. +See http\Message::getBody() and http\Message::addBody().', +'http\Env\Response::setCacheControl' => 'Make suggestions to the client how it should cache the response', +'http\Env\Response::setContentDisposition' => 'Set the response’s content disposition parameters', +'http\Env\Response::setContentEncoding' => 'Enable support for “Accept-Encoding” requests with deflate or gzip', +'http\Env\Response::setContentType' => 'Set the MIME content type of the response', +'http\Env\Response::setCookie' => 'Add cookies to the response to send', +'http\Env\Response::setEnvRequest' => 'Override the environment’s request', +'http\Env\Response::setEtag' => 'Override the environment’s request', +'http\Env\Response::setHeader' => 'Set a single header. +See http\Message::getHeader() and http\Message::addHeader(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Env\Response::setHeaders' => 'Set the message headers. +See http\Message::getHeaders() and http\Message::addHeaders(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Env\Response::setHttpVersion' => 'Set the HTTP protocol version of the message. +See http\Message::getHttpVersion().', +'http\Env\Response::setInfo' => 'Set the complete message info, i.e. type and response resp. request information, at once. +See http\Message::getInfo().', +'http\Env\Response::setLastModified' => 'Override the environment’s request', +'http\Env\Response::setRequestMethod' => 'Set the request method of the message. +See http\Message::getRequestMethod() and http\Message::setRequestUrl().', +'http\Env\Response::setRequestUrl' => 'Set the request URL of the message. +See http\Message::getRequestUrl() and http\Message::setRequestMethod().', +'http\Env\Response::setResponseCode' => 'Set the response status code. +See http\Message::getResponseCode() and http\Message::setResponseStatus(). + +> ***NOTE:*** +> This method also resets the response status phrase to the default for that code.', +'http\Env\Response::setResponseStatus' => 'Set the response status phrase. +See http\Message::getResponseStatus() and http\Message::setResponseCode().', +'http\Env\Response::setThrottleRate' => 'Override the environment’s request', +'http\Env\Response::setType' => 'Set the message type and reset the message info. +See http\Message::getType() and http\Message::setInfo().', +'http\Env\Response::splitMultipartBody' => 'Splits the body of a multipart message. +See http\Message::isMultipart() and http\Message\Body::addPart().', +'http\Env\Response::toCallback' => 'Stream the message through a callback.', +'http\Env\Response::toStream' => 'Stream the message into stream $stream, starting from $offset, streaming $maxlen at most.', +'http\Env\Response::toString' => 'Retrieve the message serialized to a string.', +'http\Env\Response::unserialize' => 'Implements Serializable.', +'http\Env\Response::valid' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Header::__construct' => 'Create an http\Header instance for use of simple matching or negotiation. If the value of the header is an array it may be compounded to a single comma separated string.', +'http\Header::__toString' => 'String cast handler. Alias of http\Header::serialize().', +'http\Header::getParams' => 'Create a parameter list out of the HTTP header value.', +'http\Header::match' => 'Match the HTTP header\'s value against provided $value according to $flags.', +'http\Header::negotiate' => 'Negotiate the header\'s value against a list of supported values in $supported. +Negotiation operation is adopted according to the header name, i.e. if the +header being negotiated is Accept, then a slash is used as primary type +separator, and if the header is Accept-Language respectively, a hyphen is +used instead. + +> ***NOTE:*** +> The first element of $supported serves as a default if no operand matches.', +'http\Header::parse' => 'Parse HTTP headers. +See also http\Header\Parser.', +'http\Header::serialize' => 'Implements Serializable.', +'http\Header::toString' => 'Convenience method. Alias of http\Header::serialize().', +'http\Header::unserialize' => 'Implements Serializable.', +'http\Header\Parser::getState' => 'Retrieve the current state of the parser. +See http\Header\Parser::STATE_* constants.', +'http\Header\Parser::parse' => 'Parse a string.', +'http\Header\Parser::stream' => 'Parse a stream.', +'http\Message::__construct' => 'Create a new HTTP message.', +'http\Message::__toString' => 'Retrieve the message serialized to a string. +Alias of http\Message::toString().', +'http\Message::addBody' => 'Append the data of $body to the message\'s body. +See http\Message::setBody() and http\Message\Body::append().', +'http\Message::addHeader' => 'Add an header, appending to already existing headers. +See http\Message::addHeaders() and http\Message::setHeader().', +'http\Message::addHeaders' => 'Add headers, optionally appending values, if header keys already exist. +See http\Message::addHeader() and http\Message::setHeaders().', +'http\Message::count' => 'Implements Countable.', +'http\Message::current' => 'Implements iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Message::detach' => 'Detach a clone of this message from any message chain.', +'http\Message::getBody' => 'Retrieve the message\'s body. +See http\Message::setBody().', +'http\Message::getHeader' => 'Retrieve a single header, optionally hydrated into a http\Header extending class.', +'http\Message::getHeaders' => 'Retrieve all message headers. +See http\Message::setHeaders() and http\Message::getHeader().', +'http\Message::getHttpVersion' => 'Retrieve the HTTP protocol version of the message. +See http\Message::setHttpVersion().', +'http\Message::getInfo' => 'Retrieve the first line of a request or response message. +See http\Message::setInfo and also: + +* http\Message::getType() +* http\Message::getHttpVersion() +* http\Message::getResponseCode() +* http\Message::getResponseStatus() +* http\Message::getRequestMethod() +* http\Message::getRequestUrl()', +'http\Message::getParentMessage' => 'Retrieve any parent message. +See http\Message::reverse().', +'http\Message::getRequestMethod' => 'Retrieve the request method of the message. +See http\Message::setRequestMethod() and http\Message::getRequestUrl().', +'http\Message::getRequestUrl' => 'Retrieve the request URL of the message. +See http\Message::setRequestUrl().', +'http\Message::getResponseCode' => 'Retrieve the response code of the message. +See http\Message::setResponseCode() and http\Message::getResponseStatus().', +'http\Message::getResponseStatus' => 'Retrieve the response status of the message. +See http\Message::setResponseStatus() and http\Message::getResponseCode().', +'http\Message::getType' => 'Retrieve the type of the message. +See http\Message::setType() and http\Message::getInfo().', +'http\Message::isMultipart' => 'Check whether this message is a multipart message based on it\'s content type. +If the message is a multipart message and a reference $boundary is given, the boundary string of the multipart message will be stored in $boundary. + +See http\Message::splitMultipartBody().', +'http\Message::key' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Message::next' => 'Implements Iterator. +See http\Message::valid() and http\Message::rewind().', +'http\Message::prepend' => 'Prepend message(s) $message to this message, or the top most message of this message chain. + +> ***NOTE:*** +> The message chains must not overlap.', +'http\Message::reverse' => 'Reverse the message chain and return the former top-most message. + +> ***NOTE:*** +> Message chains are ordered in reverse-parsed order by default, i.e. the last parsed message is the message you\'ll receive from any call parsing HTTP messages. +> +> This call re-orders the messages of the chain and returns the message that was parsed first with any later parsed messages re-parentized.', +'http\Message::rewind' => 'Implements Iterator.', +'http\Message::serialize' => 'Implements Serializable.', +'http\Message::setBody' => 'Set the message\'s body. +See http\Message::getBody() and http\Message::addBody().', +'http\Message::setHeader' => 'Set a single header. +See http\Message::getHeader() and http\Message::addHeader(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Message::setHeaders' => 'Set the message headers. +See http\Message::getHeaders() and http\Message::addHeaders(). + +> ***NOTE:*** +> Prior to v2.5.6/v3.1.0 headers with the same name were merged into a single +> header with values concatenated by comma.', +'http\Message::setHttpVersion' => 'Set the HTTP protocol version of the message. +See http\Message::getHttpVersion().', +'http\Message::setInfo' => 'Set the complete message info, i.e. type and response resp. request information, at once. +See http\Message::getInfo().', +'http\Message::setRequestMethod' => 'Set the request method of the message. +See http\Message::getRequestMethod() and http\Message::setRequestUrl().', +'http\Message::setRequestUrl' => 'Set the request URL of the message. +See http\Message::getRequestUrl() and http\Message::setRequestMethod().', +'http\Message::setResponseCode' => 'Set the response status code. +See http\Message::getResponseCode() and http\Message::setResponseStatus(). + +> ***NOTE:*** +> This method also resets the response status phrase to the default for that code.', +'http\Message::setResponseStatus' => 'Set the response status phrase. +See http\Message::getResponseStatus() and http\Message::setResponseCode().', +'http\Message::setType' => 'Set the message type and reset the message info. +See http\Message::getType() and http\Message::setInfo().', +'http\Message::splitMultipartBody' => 'Splits the body of a multipart message. +See http\Message::isMultipart() and http\Message\Body::addPart().', +'http\Message::toCallback' => 'Stream the message through a callback.', +'http\Message::toStream' => 'Stream the message into stream $stream, starting from $offset, streaming $maxlen at most.', +'http\Message::toString' => 'Retrieve the message serialized to a string.', +'http\Message::unserialize' => 'Implements Serializable.', +'http\Message::valid' => 'Implements Iterator. +See http\Message::current() and http\Message::rewind().', +'http\Message\Body::__construct' => 'Create a new message body, optionally referencing $stream.', +'http\Message\Body::__toString' => 'String cast handler.', +'http\Message\Body::addForm' => 'Add form fields and files to the message body. + +> ***NOTE:*** +> Currently, http\Message\Body::addForm() creates "multipart/form-data" bodies.', +'http\Message\Body::addPart' => 'Add a part to a multipart body.', +'http\Message\Body::append' => 'Append plain bytes to the message body.', +'http\Message\Body::etag' => 'Retrieve the ETag of the body.', +'http\Message\Body::getBoundary' => 'Retrieve any boundary of the message body. +See http\Message::splitMultipartBody().', +'http\Message\Body::getResource' => 'Retrieve the underlying stream resource.', +'http\Message\Body::serialize' => 'Implements Serializable. +Alias of http\Message\Body::__toString().', +'http\Message\Body::stat' => 'Stat size, atime, mtime and/or ctime.', +'http\Message\Body::toCallback' => 'Stream the message body through a callback.', +'http\Message\Body::toStream' => 'Stream the message body into another stream $stream, starting from $offset, streaming $maxlen at most.', +'http\Message\Body::toString' => 'Retrieve the message body serialized to a string. +Alias of http\Message\Body::__toString().', +'http\Message\Body::unserialize' => 'Implements Serializable.', +'http\Message\Parser::getState' => 'Retrieve the current state of the parser. +See http\Message\Parser::STATE_* constants.', +'http\Message\Parser::parse' => 'Parse a string.', +'http\Message\Parser::stream' => 'Parse a stream.', +'http\Params::__construct' => 'Instantiate a new HTTP (header) parameter set.', +'http\Params::__toString' => 'String cast handler. Alias of http\Params::toString(). +Returns a stringified version of the parameters.', +'http\Params::offsetExists' => 'Implements ArrayAccess.', +'http\Params::offsetGet' => 'Implements ArrayAccess.', +'http\Params::offsetSet' => 'Implements ArrayAccess.', +'http\Params::offsetUnset' => 'Implements ArrayAccess.', +'http\Params::toArray' => 'Convenience method that simply returns http\Params::$params.', +'http\Params::toString' => 'Returns a stringified version of the parameters.', +'http\QueryString::__construct' => 'QueryString constructor.', +'http\QueryString::__toString' => 'Get the string representation of the querystring (x-www-form-urlencoded).', +'http\QueryString::get' => 'Retrieve an querystring value', +'http\QueryString::getArray' => 'Retrieve an array value at offset $name', +'http\QueryString::getBool' => 'Retrieve an array value at offset $name', +'http\QueryString::getFloat' => 'Retrieve an array value at offset $name', +'http\QueryString::getGlobalInstance' => 'Retrieve the global querystring instance referencing $_GET', +'http\QueryString::getInt' => 'Retrieve an array value at offset $name', +'http\QueryString::getIterator' => 'Implements IteratorAggregate.', +'http\QueryString::getObject' => 'Retrieve an array value at offset $name', +'http\QueryString::getString' => 'Retrieve an array value at offset $name', +'http\QueryString::mod' => 'Set additional $params to a clone of this instance', +'http\QueryString::offsetExists' => 'Implements ArrayAccess.', +'http\QueryString::offsetGet' => 'Implements ArrayAccess.', +'http\QueryString::offsetSet' => 'Implements ArrayAccess.', +'http\QueryString::offsetUnset' => 'Implements ArrayAccess.', +'http\QueryString::serialize' => 'Implements Serializable. +See http\QueryString::toString().', +'http\QueryString::set' => 'Set additional querystring entries', +'http\QueryString::toArray' => 'Returns http\QueryString::$queryArray', +'http\QueryString::toString' => 'Get the string representation of the querystring (x-www-form-urlencoded)', +'http\QueryString::unserialize' => 'Implements Serializable.', +'http\QueryString::xlate' => 'Translate character encodings of the querystring with ext/iconv', +'http\Url::__construct' => 'Url constructor.', +'http\Url::__toString' => 'Alias of Url::toString()', +'http\Url::mod' => 'Clone this URL and apply $parts to the cloned URL', +'http\Url::toArray' => 'Retrieve the URL parts as array', +'http\Url::toString' => 'Get the string prepresentation of the URL', +'http_build_query' => 'Generate URL-encoded query string', +'http_response_code' => 'Get or Set the HTTP response code', +'HttpDeflateStream::__construct' => 'HttpDeflateStream class constructor', +'HttpDeflateStream::factory' => 'HttpDeflateStream class factory', +'HttpDeflateStream::finish' => 'Finalize deflate stream', +'HttpDeflateStream::flush' => 'Flush deflate stream', +'HttpDeflateStream::update' => 'Update deflate stream', +'HttpInflateStream::__construct' => 'HttpInflateStream class constructor', +'HttpInflateStream::factory' => 'HttpInflateStream class factory', +'HttpInflateStream::finish' => 'Finalize inflate stream', +'HttpInflateStream::flush' => 'Flush inflate stream', +'HttpInflateStream::update' => 'Update inflate stream', +'HttpMessage::__construct' => 'HttpMessage constructor', +'HttpMessage::addHeaders' => 'Add headers', +'HttpMessage::detach' => 'Detach HttpMessage', +'HttpMessage::factory' => 'Create HttpMessage from string', +'HttpMessage::fromEnv' => 'Create HttpMessage from environment', +'HttpMessage::fromString' => 'Create HttpMessage from string', +'HttpMessage::getBody' => 'Get message body', +'HttpMessage::getHeader' => 'Get header', +'HttpMessage::getHeaders' => 'Get message headers', +'HttpMessage::getHttpVersion' => 'Get HTTP version', +'HttpMessage::getParentMessage' => 'Get parent message', +'HttpMessage::getRequestMethod' => 'Get request method', +'HttpMessage::getRequestUrl' => 'Get request URL', +'HttpMessage::getResponseCode' => 'Get response code', +'HttpMessage::getResponseStatus' => 'Get response status', +'HttpMessage::getType' => 'Get message type', +'HttpMessage::guessContentType' => 'Guess content type', +'HttpMessage::prepend' => 'Prepend message(s)', +'HttpMessage::reverse' => 'Reverse message chain', +'HttpMessage::send' => 'Send message', +'HttpMessage::setBody' => 'Set message body', +'HttpMessage::setHeaders' => 'Set headers', +'HttpMessage::setHttpVersion' => 'Set HTTP version', +'HttpMessage::setRequestMethod' => 'Set request method', +'HttpMessage::setRequestUrl' => 'Set request URL', +'HttpMessage::setResponseCode' => 'Set response code', +'HttpMessage::setResponseStatus' => 'Set response status', +'HttpMessage::setType' => 'Set message type', +'HttpMessage::toMessageTypeObject' => 'Create HTTP object regarding message type', +'HttpMessage::toString' => 'Get string representation', +'HttpQueryString::__construct' => 'HttpQueryString constructor', +'HttpQueryString::get' => 'Get (part of) query string', +'HttpQueryString::mod' => 'Modify query string copy', +'HttpQueryString::offsetExists' => 'Whether a offset exists', +'HttpQueryString::offsetGet' => 'Offset to retrieve', +'HttpQueryString::offsetSet' => 'Offset to set', +'HttpQueryString::offsetUnset' => 'Offset to unset', +'HttpQueryString::serialize' => 'String representation of object', +'HttpQueryString::set' => 'Set query string params', +'HttpQueryString::singleton' => 'HttpQueryString singleton', +'HttpQueryString::toArray' => 'Get query string as array', +'HttpQueryString::toString' => 'Get query string', +'HttpQueryString::unserialize' => 'Constructs the object', +'HttpQueryString::xlate' => 'Change query strings charset', +'HttpRequest::__construct' => 'HttpRequest constructor', +'HttpRequest::addCookies' => 'Add cookies', +'HttpRequest::addHeaders' => 'Add headers', +'HttpRequest::addPostFields' => 'Add post fields', +'HttpRequest::addPostFile' => 'Add post file', +'HttpRequest::addPutData' => 'Add put data', +'HttpRequest::addQueryData' => 'Add query data', +'HttpRequest::addRawPostData' => 'Add raw post data', +'HttpRequest::addSslOptions' => 'Add ssl options', +'HttpRequest::clearHistory' => 'Clear history', +'HttpRequest::enableCookies' => 'Enable cookies', +'HttpRequest::getContentType' => 'Get content type', +'HttpRequest::getCookies' => 'Get cookies', +'HttpRequest::getHeaders' => 'Get headers', +'HttpRequest::getHistory' => 'Get history', +'HttpRequest::getMethod' => 'Get method', +'HttpRequest::getOptions' => 'Get options', +'HttpRequest::getPostFields' => 'Get post fields', +'HttpRequest::getPostFiles' => 'Get post files', +'HttpRequest::getPutData' => 'Get put data', +'HttpRequest::getPutFile' => 'Get put file', +'HttpRequest::getQueryData' => 'Get query data', +'HttpRequest::getRawPostData' => 'Get raw post data', +'HttpRequest::getRawRequestMessage' => 'Get raw request message', +'HttpRequest::getRawResponseMessage' => 'Get raw response message', +'HttpRequest::getRequestMessage' => 'Get request message', +'HttpRequest::getResponseBody' => 'Get response body', +'HttpRequest::getResponseCode' => 'Get response code', +'HttpRequest::getResponseCookies' => 'Get response cookie(s)', +'HttpRequest::getResponseData' => 'Get response data', +'HttpRequest::getResponseHeader' => 'Get response header(s)', +'HttpRequest::getResponseInfo' => 'Get response info', +'HttpRequest::getResponseMessage' => 'Get response message', +'HttpRequest::getResponseStatus' => 'Get response status', +'HttpRequest::getSslOptions' => 'Get ssl options', +'HttpRequest::getUrl' => 'Get url', +'HttpRequest::resetCookies' => 'Reset cookies', +'HttpRequest::send' => 'Send request', +'HttpRequest::setContentType' => 'Set content type', +'HttpRequest::setCookies' => 'Set cookies', +'HttpRequest::setHeaders' => 'Set headers', +'HttpRequest::setMethod' => 'Set method', +'HttpRequest::setOptions' => 'Set options', +'HttpRequest::setPostFields' => 'Set post fields', +'HttpRequest::setPostFiles' => 'Set post files', +'HttpRequest::setPutData' => 'Set put data', +'HttpRequest::setPutFile' => 'Set put file', +'HttpRequest::setQueryData' => 'Set query data', +'HttpRequest::setRawPostData' => 'Set raw post data', +'HttpRequest::setSslOptions' => 'Set ssl options', +'HttpRequest::setUrl' => 'Set URL', +'HttpRequestPool::__construct' => 'HttpRequestPool constructor', +'HttpRequestPool::__destruct' => 'HttpRequestPool destructor', +'HttpRequestPool::attach' => 'Attach HttpRequest', +'HttpRequestPool::detach' => 'Detach HttpRequest', +'HttpRequestPool::getAttachedRequests' => 'Get attached requests', +'HttpRequestPool::getFinishedRequests' => 'Get finished requests', +'HttpRequestPool::reset' => 'Reset request pool', +'HttpRequestPool::send' => 'Send all requests', +'HttpRequestPool::socketPerform' => 'Perform socket actions', +'HttpRequestPool::socketSelect' => 'Perform socket select', +'HttpResponse::capture' => 'Capture script output', +'HttpResponse::getBufferSize' => 'Get buffer size', +'HttpResponse::getCache' => 'Get cache', +'HttpResponse::getCacheControl' => 'Get cache control', +'HttpResponse::getContentDisposition' => 'Get content disposition', +'HttpResponse::getContentType' => 'Get content type', +'HttpResponse::getData' => 'Get data', +'HttpResponse::getETag' => 'Get ETag', +'HttpResponse::getFile' => 'Get file', +'HttpResponse::getGzip' => 'Get gzip', +'HttpResponse::getHeader' => 'Get header', +'HttpResponse::getLastModified' => 'Get last modified', +'HttpResponse::getRequestBody' => 'Get request body', +'HttpResponse::getRequestBodyStream' => 'Get request body stream', +'HttpResponse::getRequestHeaders' => 'Get request headers', +'HttpResponse::getStream' => 'Get Stream', +'HttpResponse::getThrottleDelay' => 'Get throttle delay', +'HttpResponse::guessContentType' => 'Guess content type', +'HttpResponse::redirect' => 'Redirect', +'HttpResponse::send' => 'Send response', +'HttpResponse::setBufferSize' => 'Set buffer size', +'HttpResponse::setCache' => 'Set cache', +'HttpResponse::setCacheControl' => 'Set cache control', +'HttpResponse::setContentDisposition' => 'Set content disposition', +'HttpResponse::setContentType' => 'Set content type', +'HttpResponse::setData' => 'Set data', +'HttpResponse::setETag' => 'Set ETag', +'HttpResponse::setFile' => 'Set file', +'HttpResponse::setGzip' => 'Set gzip', +'HttpResponse::setHeader' => 'Set header', +'HttpResponse::setLastModified' => 'Set last modified', +'HttpResponse::setStream' => 'Set stream', +'HttpResponse::setThrottleDelay' => 'Set throttle delay', +'HttpResponse::status' => 'Send HTTP response status', +'hw_api::checkin' => 'Checks in an object', +'hw_api::checkout' => 'Checks out an object', +'hw_api::children' => 'Returns children of an object', +'hw_api::content' => 'Returns content of an object', +'hw_api::copy' => 'Copies physically', +'hw_api::dbstat' => 'Returns statistics about database server', +'hw_api::dcstat' => 'Returns statistics about document cache server', +'hw_api::dstanchors' => 'Returns a list of all destination anchors', +'hw_api::dstofsrcanchor' => 'Returns destination of a source anchor', +'hw_api::find' => 'Search for objects', +'hw_api::ftstat' => 'Returns statistics about fulltext server', +'hw_api::hwstat' => 'Returns statistics about Hyperwave server', +'hw_api::identify' => 'Log into Hyperwave Server', +'hw_api::info' => 'Returns information about server configuration', +'hw_api::insert' => 'Inserts a new object', +'hw_api::insertanchor' => 'Inserts a new object of type anchor', +'hw_api::insertcollection' => 'Inserts a new object of type collection', +'hw_api::insertdocument' => 'Inserts a new object of type document', +'hw_api::link' => 'Creates a link to an object', +'hw_api::lock' => 'Locks an object', +'hw_api::move' => 'Moves object between collections', +'hw_api::object' => 'Retrieve attribute information', +'hw_api::objectbyanchor' => 'Returns the object an anchor belongs to', +'hw_api::parents' => 'Returns parents of an object', +'hw_api::remove' => 'Delete an object', +'hw_api::replace' => 'Replaces an object', +'hw_api::setcommittedversion' => 'Commits version other than last version', +'hw_api::srcanchors' => 'Returns a list of all source anchors', +'hw_api::srcsofdst' => 'Returns source of a destination object', +'hw_api::unlock' => 'Unlocks a locked object', +'hw_api::user' => 'Returns the own user object', +'hw_api::userlist' => 'Returns a list of all logged in users', +'hw_api_attribute::key' => 'Returns key of the attribute', +'hw_api_attribute::langdepvalue' => 'Returns value for a given language', +'hw_api_attribute::value' => 'Returns value of the attribute', +'hw_api_attribute::values' => 'Returns all values of the attribute', +'hw_api_content::mimetype' => 'Returns mimetype', +'hw_api_content::read' => 'Read content', +'hw_api_error::count' => 'Returns number of reasons', +'hw_api_error::reason' => 'Returns reason of error', +'hw_api_object::assign' => 'Clones object', +'hw_api_object::attreditable' => 'Checks whether an attribute is editable', +'hw_api_object::count' => 'Returns number of attributes', +'hw_api_object::insert' => 'Inserts new attribute', +'hw_api_object::remove' => 'Removes attribute', +'hw_api_object::title' => 'Returns the title attribute', +'hw_api_object::value' => 'Returns value of attribute', +'hw_api_reason::description' => 'Returns description of reason', +'hw_api_reason::type' => 'Returns type of reason', +'hwapi_attribute_new' => 'Creates instance of class hw_api_attribute', +'hwapi_content_new' => 'Create new instance of class hw_api_content', +'hwapi_hgcsp' => 'Returns object of class hw_api', +'hwapi_object_new' => 'Creates a new instance of class hwapi_object_new', +'hypot' => 'Calculate the length of the hypotenuse of a right-angle triangle', +'ibase_add_user' => 'Add a user to a security database', +'ibase_affected_rows' => 'Return the number of rows that were affected by the previous query', +'ibase_backup' => 'Initiates a backup task in the service manager and returns immediately', +'ibase_blob_add' => 'Add data into a newly created blob', +'ibase_blob_cancel' => 'Cancel creating blob', +'ibase_blob_close' => 'Close blob', +'ibase_blob_create' => 'Create a new blob for adding data', +'ibase_blob_echo' => 'Output blob contents to browser', +'ibase_blob_get' => 'Get len bytes data from open blob', +'ibase_blob_import' => 'Create blob, copy file in it, and close it', +'ibase_blob_info' => 'Return blob length and other useful info', +'ibase_blob_open' => 'Open blob for retrieving data parts', +'ibase_close' => 'Close a connection to an InterBase database', +'ibase_commit' => 'Commit a transaction', +'ibase_commit_ret' => 'Commit a transaction without closing it', +'ibase_connect' => 'Open a connection to a database', +'ibase_db_info' => 'Request statistics about a database', +'ibase_delete_user' => 'Delete a user from a security database', +'ibase_drop_db' => 'Drops a database', +'ibase_errcode' => 'Return an error code', +'ibase_errmsg' => 'Return error messages', +'ibase_execute' => 'Execute a previously prepared query', +'ibase_fetch_assoc' => 'Fetch a result row from a query as an associative array', +'ibase_fetch_object' => 'Get an object from a InterBase database', +'ibase_fetch_row' => 'Fetch a row from an InterBase database', +'ibase_field_info' => 'Get information about a field', +'ibase_free_event_handler' => 'Cancels a registered event handler', +'ibase_free_query' => 'Free memory allocated by a prepared query', +'ibase_free_result' => 'Free a result set', +'ibase_gen_id' => 'Increments the named generator and returns its new value', +'ibase_maintain_db' => 'Execute a maintenance command on the database server', +'ibase_modify_user' => 'Modify a user to a security database', +'ibase_name_result' => 'Assigns a name to a result set', +'ibase_num_fields' => 'Get the number of fields in a result set', +'ibase_num_params' => 'Return the number of parameters in a prepared query', +'ibase_param_info' => 'Return information about a parameter in a prepared query', +'ibase_pconnect' => 'Open a persistent connection to an InterBase database', +'ibase_prepare' => 'Prepare a query for later binding of parameter placeholders and execution', +'ibase_query' => 'Execute a query on an InterBase database', +'ibase_restore' => 'Initiates a restore task in the service manager and returns immediately', +'ibase_rollback' => 'Roll back a transaction', +'ibase_rollback_ret' => 'Roll back a transaction without closing it', +'ibase_server_info' => 'Request information about a database server', +'ibase_service_attach' => 'Connect to the service manager', +'ibase_service_detach' => 'Disconnect from the service manager', +'ibase_set_event_handler' => 'Register a callback function to be called when events are posted', +'ibase_trans' => 'Begin a transaction', +'ibase_wait_event' => 'Wait for an event to be posted by the database', +'iconv' => 'Convert string to requested character encoding', +'iconv_get_encoding' => 'Retrieve internal configuration variables of iconv extension', +'iconv_mime_decode' => 'Decodes a MIME header field', +'iconv_mime_decode_headers' => 'Decodes multiple MIME header fields at once', +'iconv_mime_encode' => 'Composes a MIME header field', +'iconv_set_encoding' => 'Set current setting for character encoding conversion', +'iconv_strlen' => 'Returns the character count of string', +'iconv_strpos' => 'Finds position of first occurrence of a needle within a haystack', +'iconv_strrpos' => 'Finds the last occurrence of a needle within a haystack', +'iconv_substr' => 'Cut out part of a string', +'id3_get_frame_long_name' => 'Get the long name of an ID3v2 frame', +'id3_get_frame_short_name' => 'Get the short name of an ID3v2 frame', +'id3_get_genre_id' => 'Get the id for a genre', +'id3_get_genre_list' => 'Get all possible genre values', +'id3_get_genre_name' => 'Get the name for a genre id', +'id3_get_tag' => 'Get all information stored in an ID3 tag', +'id3_get_version' => 'Get version of an ID3 tag', +'id3_remove_tag' => 'Remove an existing ID3 tag', +'id3_set_tag' => 'Update information stored in an ID3 tag', +'idate' => 'Format a local time/date as integer', +'ifx_affected_rows' => 'Get number of rows affected by a query', +'ifx_blobinfile_mode' => 'Set the default blob mode for all select queries', +'ifx_byteasvarchar' => 'Set the default byte mode', +'ifx_close' => 'Close Informix connection', +'ifx_connect' => 'Open Informix server connection', +'ifx_copy_blob' => 'Duplicates the given blob object', +'ifx_create_blob' => 'Creates an blob object', +'ifx_create_char' => 'Creates an char object', +'ifx_do' => 'Execute a previously prepared SQL-statement', +'ifx_error' => 'Returns error code of last Informix call', +'ifx_errormsg' => 'Returns error message of last Informix call', +'ifx_fetch_row' => 'Get row as an associative array', +'ifx_fieldproperties' => 'List of SQL fieldproperties', +'ifx_fieldtypes' => 'List of Informix SQL fields', +'ifx_free_blob' => 'Deletes the blob object', +'ifx_free_char' => 'Deletes the char object', +'ifx_free_result' => 'Releases resources for the query', +'ifx_get_blob' => 'Return the content of a blob object', +'ifx_get_char' => 'Return the content of the char object', +'ifx_getsqlca' => 'Get the contents of sqlca.sqlerrd[0..5] after a query', +'ifx_htmltbl_result' => 'Formats all rows of a query into a HTML table', +'ifx_nullformat' => 'Sets the default return value on a fetch row', +'ifx_num_fields' => 'Returns the number of columns in the query', +'ifx_num_rows' => 'Count the rows already fetched from a query', +'ifx_pconnect' => 'Open persistent Informix connection', +'ifx_prepare' => 'Prepare an SQL-statement for execution', +'ifx_query' => 'Send Informix query', +'ifx_textasvarchar' => 'Set the default text mode', +'ifx_update_blob' => 'Updates the content of the blob object', +'ifx_update_char' => 'Updates the content of the char object', +'ifxus_close_slob' => 'Deletes the slob object', +'ifxus_create_slob' => 'Creates an slob object and opens it', +'ifxus_free_slob' => 'Deletes the slob object', +'ifxus_open_slob' => 'Opens an slob object', +'ifxus_read_slob' => 'Reads nbytes of the slob object', +'ifxus_seek_slob' => 'Sets the current file or seek position', +'ifxus_tell_slob' => 'Returns the current file or seek position', +'ifxus_write_slob' => 'Writes a string into the slob object', +'ignore_user_abort' => 'Set whether a client disconnect should abort script execution', +'iis_add_server' => 'Creates a new virtual web server', +'iis_get_dir_security' => 'Gets Directory Security', +'iis_get_script_map' => 'Gets script mapping on a virtual directory for a specific extension', +'iis_get_server_by_comment' => 'Return the instance number associated with the Comment', +'iis_get_server_by_path' => 'Return the instance number associated with the Path', +'iis_get_server_rights' => 'Gets server rights', +'iis_get_service_state' => 'Returns the state for the service defined by ServiceId', +'iis_remove_server' => 'Removes the virtual web server indicated by ServerInstance', +'iis_set_app_settings' => 'Creates application scope for a virtual directory', +'iis_set_dir_security' => 'Sets Directory Security', +'iis_set_script_map' => 'Sets script mapping on a virtual directory', +'iis_set_server_rights' => 'Sets server rights', +'iis_start_server' => 'Starts the virtual web server', +'iis_start_service' => 'Starts the service defined by ServiceId', +'iis_stop_server' => 'Stops the virtual web server', +'iis_stop_service' => 'Stops the service defined by ServiceId', +'image2wbmp' => '`gd.image.output`', +'image_type_to_extension' => 'Get file extension for image type', +'image_type_to_mime_type' => 'Get Mime-Type for image-type returned by getimagesize, exif_read_data, exif_thumbnail, exif_imagetype', +'imageaffine' => 'Return an image containing the affine transformed src image, using an optional clipping area', +'imageaffinematrixconcat' => 'Concatenate two affine transformation matrices', +'imageaffinematrixget' => 'Get an affine transformation matrix', +'imagealphablending' => 'Set the blending mode for an image', +'imageantialias' => 'Should antialias functions be used or not', +'imagearc' => 'Draws an arc', +'imagebmp' => 'Output a BMP image to browser or file', +'imagechar' => 'Draw a character horizontally', +'imagecharup' => 'Draw a character vertically', +'imagecolorallocate' => 'Allocate a color for an image', +'imagecolorallocatealpha' => 'Allocate a color for an image', +'imagecolorat' => 'Get the index of the color of a pixel', +'imagecolorclosest' => 'Get the index of the closest color to the specified color', +'imagecolorclosestalpha' => 'Get the index of the closest color to the specified color + alpha', +'imagecolorclosesthwb' => 'Get the index of the color which has the hue, white and blackness', +'imagecolordeallocate' => 'De-allocate a color for an image', +'imagecolorexact' => 'Get the index of the specified color', +'imagecolorexactalpha' => 'Get the index of the specified color + alpha', +'imagecolormatch' => 'Makes the colors of the palette version of an image more closely match the true color version', +'imagecolorresolve' => 'Get the index of the specified color or its closest possible alternative', +'imagecolorresolvealpha' => 'Get the index of the specified color + alpha or its closest possible alternative', +'imagecolorset' => 'Set the color for the specified palette index', +'imagecolorsforindex' => 'Get the colors for an index', +'imagecolorstotal' => 'Find out the number of colors in an image\'s palette', +'imagecolortransparent' => 'Define a color as transparent', +'imageconvolution' => 'Apply a 3x3 convolution matrix, using coefficient and offset', +'imagecopy' => 'Copy part of an image', +'imagecopymerge' => 'Copy and merge part of an image', +'imagecopymergegray' => 'Copy and merge part of an image with gray scale', +'imagecopyresampled' => 'Copy and resize part of an image with resampling', +'imagecopyresized' => 'Copy and resize part of an image', +'imagecreate' => 'Create a new palette based image', +'imagecreatefrombmp' => '`gd.image.new`', +'imagecreatefromgd' => 'Create a new image from GD file or URL', +'imagecreatefromgd2' => 'Create a new image from GD2 file or URL', +'imagecreatefromgd2part' => 'Create a new image from a given part of GD2 file or URL', +'imagecreatefromgif' => '`gd.image.new`', +'imagecreatefromjpeg' => '`gd.image.new`', +'imagecreatefrompng' => '`gd.image.new`', +'imagecreatefromstring' => 'Create a new image from the image stream in the string', +'imagecreatefromwbmp' => '`gd.image.new`', +'imagecreatefromwebp' => '`gd.image.new`', +'imagecreatefromxbm' => '`gd.image.new`', +'imagecreatefromxpm' => '`gd.image.new`', +'imagecreatetruecolor' => 'Create a new true color image', +'imagecrop' => 'Crop an image to the given rectangle', +'imagecropauto' => 'Crop an image automatically using one of the available modes', +'imagedashedline' => 'Draw a dashed line', +'imagedestroy' => 'Destroy an image', +'imageellipse' => 'Draw an ellipse', +'imagefill' => 'Flood fill', +'imagefilledarc' => 'Draw a partial arc and fill it', +'imagefilledellipse' => 'Draw a filled ellipse', +'imagefilledpolygon' => 'Draw a filled polygon', +'imagefilledrectangle' => 'Draw a filled rectangle', +'imagefilltoborder' => 'Flood fill to specific color', +'imagefilter' => 'Applies a filter to an image', +'imageflip' => 'Flips an image using a given mode', +'imagefontheight' => 'Get font height', +'imagefontwidth' => 'Get font width', +'imageftbbox' => 'Give the bounding box of a text using fonts via freetype2', +'imagefttext' => 'Write text to the image using fonts using FreeType 2', +'imagegammacorrect' => 'Apply a gamma correction to a GD image', +'imagegd' => 'Output GD image to browser or file', +'imagegd2' => 'Output GD2 image to browser or file', +'imagegetclip' => 'Get the clipping rectangle', +'imagegif' => '`gd.image.output`', +'imagegrabscreen' => 'Captures the whole screen', +'imagegrabwindow' => 'Captures a window', +'imageinterlace' => 'Enable or disable interlace', +'imageistruecolor' => 'Finds whether an image is a truecolor image', +'imagejpeg' => '`gd.image.output`', +'imagelayereffect' => 'Set the alpha blending flag to use layering effects', +'imageline' => 'Draw a line', +'imageloadfont' => 'Load a new font', +'imageObj::pasteImage' => 'Copy srcImg on top of the current imageObj. +transparentColorHex is the color (in 0xrrggbb format) from srcImg +that should be considered transparent (i.e. those pixels won\'t +be copied). Pass -1 if you don\'t want any transparent color. +If optional dstx,dsty are provided then it defines the position +where the image should be copied (dstx,dsty = top-left corner +position). +The optional angle is a value between 0 and 360 degrees to rotate +the source image counterclockwise. Note that if an angle is specified +(even if its value is zero) then the dstx and dsty coordinates +specify the CENTER of the destination area. +Note: this function works only with 8 bits GD images (PNG or GIF).', +'imageObj::saveImage' => 'Writes image object to specified filename. +Passing no filename or an empty filename sends output to stdout. In +this case, the PHP header() function should be used to set the +document\'s content-type prior to calling saveImage(). The output +format is the one that is currently selected in the map file. The +second argument oMap is not manadatory. It is usful when saving to +formats like GTIFF that needs georeference information contained in +the map file. On success, it returns either MS_SUCCESS if writing to an +external file, or the number of bytes written if output is sent to +stdout.', +'imageObj::saveWebImage' => 'Writes image to temp directory. Returns image URL. +The output format is the one that is currently selected in the +map file.', +'imageopenpolygon' => 'Draws an open polygon', +'imagepalettecopy' => 'Copy the palette from one image to another', +'imagepalettetotruecolor' => 'Converts a palette based image to true color', +'imagepng' => 'Output a PNG image to either the browser or a file', +'imagepolygon' => 'Draws a polygon', +'imagepsbbox' => 'Give the bounding box of a text rectangle using PostScript Type1 fonts', +'imagepsencodefont' => 'Change the character encoding vector of a font', +'imagepsextendfont' => 'Extend or condense a font', +'imagepsfreefont' => 'Free memory used by a PostScript Type 1 font', +'imagepsloadfont' => 'Load a PostScript Type 1 font from file', +'imagepsslantfont' => 'Slant a font', +'imagepstext' => 'Draws a text over an image using PostScript Type1 fonts', +'imagerectangle' => 'Draw a rectangle', +'imageresolution' => 'Get or set the resolution of the image', +'imagerotate' => 'Rotate an image with a given angle', +'imagesavealpha' => 'Whether to retain full alpha channel information when saving PNG images', +'imagescale' => 'Scale an image using the given new width and height', +'imagesetbrush' => 'Set the brush image for line drawing', +'imagesetclip' => 'Set the clipping rectangle', +'imagesetinterpolation' => 'Set the interpolation method', +'imagesetpixel' => 'Set a single pixel', +'imagesetstyle' => 'Set the style for line drawing', +'imagesetthickness' => 'Set the thickness for line drawing', +'imagesettile' => 'Set the tile image for filling', +'imagestring' => 'Draw a string horizontally', +'imagestringup' => 'Draw a string vertically', +'imagesx' => 'Get image width', +'imagesy' => 'Get image height', +'imagetruecolortopalette' => 'Convert a true color image to a palette image', +'imagettfbbox' => 'Give the bounding box of a text using TrueType fonts', +'imagettftext' => 'Write text to the image using TrueType fonts', +'imagetypes' => 'Return the image types supported by this PHP build', +'imagewbmp' => '`gd.image.output`', +'imagewebp' => 'Output a WebP image to browser or file', +'imagexbm' => 'Output an XBM image to browser or file', +'imagick::__construct' => 'The Imagick constructor', +'imagick::__toString' => 'Returns the image as a string', +'imagick::adaptiveBlurImage' => 'Adds adaptive blur filter to image', +'imagick::adaptiveResizeImage' => 'Adaptively resize image with data dependent triangulation', +'imagick::adaptiveSharpenImage' => 'Adaptively sharpen the image', +'imagick::adaptiveThresholdImage' => 'Selects a threshold for each pixel based on a range of intensity', +'imagick::addImage' => 'Adds new image to Imagick object image list', +'imagick::addNoiseImage' => 'Adds random noise to the image', +'imagick::affineTransformImage' => 'Transforms an image', +'imagick::animateImages' => 'Animates an image or images', +'imagick::annotateImage' => 'Annotates an image with text', +'imagick::appendImages' => 'Append a set of images', +'Imagick::autoGammaImage' => 'Extracts the \'mean\' from the image and adjust the image to try make set its gamma appropriately.', +'Imagick::autoOrient' => 'Adjusts an image so that its orientation is suitable $ for viewing (i.e. top-left orientation).', +'imagick::averageImages' => 'Average a set of images', +'imagick::blackThresholdImage' => 'Forces all pixels below the threshold into black', +'imagick::blurImage' => 'Adds blur filter to image', +'imagick::borderImage' => 'Surrounds the image with a border', +'Imagick::brightnessContrastImage' => 'Change the brightness and/or contrast of an image. It converts the brightness and contrast parameters into slope and intercept and calls a polynomical function to apply to the image.', +'imagick::charcoalImage' => 'Simulates a charcoal drawing', +'imagick::chopImage' => 'Removes a region of an image and trims', +'imagick::clear' => 'Clears all resources associated to Imagick object', +'imagick::clipImage' => 'Clips along the first path from the 8BIM profile', +'imagick::clipPathImage' => 'Clips along the named paths from the 8BIM profile', +'imagick::clone' => 'Makes an exact copy of the Imagick object', +'imagick::clutImage' => 'Replaces colors in the image', +'imagick::coalesceImages' => 'Composites a set of images', +'imagick::colorFloodfillImage' => 'Changes the color value of any pixel that matches target', +'imagick::colorizeImage' => 'Blends the fill color with the image', +'Imagick::colorMatrixImage' => 'Apply color transformation to an image. The method permits saturation changes, hue rotation, luminance to alpha, and various other effects. Although variable-sized transformation matrices can be used, typically one uses a 5x5 matrix for an RGBA image and a 6x6 for CMYKA (or RGBA with offsets). +The matrix is similar to those used by Adobe Flash except offsets are in column 6 rather than 5 (in support of CMYKA images) and offsets are normalized (divide Flash offset by 255)', +'imagick::combineImages' => 'Combines one or more images into a single image', +'imagick::commentImage' => 'Adds a comment to your image', +'imagick::compareImageChannels' => 'Returns the difference in one or more images', +'imagick::compareImageLayers' => 'Returns the maximum bounding region between images', +'imagick::compareImages' => 'Compares an image to a reconstructed image', +'imagick::compositeImage' => 'Composite one image onto another', +'Imagick::compositeImageGravity' => 'Composite one image onto another using the specified gravity.', +'imagick::contrastImage' => 'Change the contrast of the image', +'imagick::contrastStretchImage' => 'Enhances the contrast of a color image', +'imagick::convolveImage' => 'Applies a custom convolution kernel to the image', +'imagick::count' => 'Get the number of images', +'imagick::cropImage' => 'Extracts a region of the image', +'imagick::cropThumbnailImage' => 'Creates a crop thumbnail', +'imagick::current' => 'Returns a reference to the current Imagick object', +'imagick::cycleColormapImage' => 'Displaces an image\'s colormap', +'imagick::decipherImage' => 'Deciphers an image', +'imagick::deconstructImages' => 'Returns certain pixel differences between images', +'imagick::deleteImageArtifact' => 'Delete image artifact', +'Imagick::deleteImageProperty' => 'Deletes an image property.', +'imagick::deskewImage' => 'Removes skew from the image', +'imagick::despeckleImage' => 'Reduces the speckle noise in an image', +'imagick::destroy' => 'Destroys the Imagick object', +'imagick::displayImage' => 'Displays an image', +'imagick::displayImages' => 'Displays an image or image sequence', +'imagick::distortImage' => 'Distorts an image using various distortion methods', +'imagick::drawImage' => 'Renders the ImagickDraw object on the current image', +'imagick::edgeImage' => 'Enhance edges within the image', +'imagick::embossImage' => 'Returns a grayscale image with a three-dimensional effect', +'imagick::encipherImage' => 'Enciphers an image', +'imagick::enhanceImage' => 'Improves the quality of a noisy image', +'imagick::equalizeImage' => 'Equalizes the image histogram', +'imagick::evaluateImage' => 'Applies an expression to an image', +'Imagick::evaluateImages' => 'Merge multiple images of the same size together with the selected operator. https://www.imagemagick.org/Usage/layers/#evaluate-sequence', +'imagick::exportImagePixels' => 'Exports raw image pixels', +'imagick::extentImage' => 'Set image size', +'Imagick::filter' => 'Applies a custom convolution kernel to the image.', +'imagick::flattenImages' => 'Merges a sequence of images', +'imagick::flipImage' => 'Creates a vertical mirror image', +'imagick::floodFillPaintImage' => 'Changes the color value of any pixel that matches target', +'imagick::flopImage' => 'Creates a horizontal mirror image', +'Imagick::forwardFourierTransformimage' => 'Implements the discrete Fourier transform (DFT) of the image either as a magnitude / phase or real / imaginary image pair.', +'imagick::frameImage' => 'Adds a simulated three-dimensional border', +'imagick::functionImage' => 'Applies a function on the image', +'imagick::fxImage' => 'Evaluate expression for each pixel in the image', +'imagick::gammaImage' => 'Gamma-corrects an image', +'imagick::gaussianBlurImage' => 'Blurs an image', +'imagick::getColorspace' => 'Gets the colorspace', +'imagick::getCompression' => 'Gets the object compression type', +'imagick::getCompressionQuality' => 'Gets the object compression quality', +'Imagick::getConfigureOptions' => 'Returns any ImageMagick configure options that match the specified pattern (e.g. "*" for all). Options include NAME, VERSION, LIB_VERSION, etc.', +'imagick::getCopyright' => 'Returns the ImageMagick API copyright as a string', +'Imagick::getFeatures' => 'GetFeatures() returns the ImageMagick features that have been compiled into the runtime.', +'imagick::getFilename' => 'The filename associated with an image sequence', +'imagick::getFont' => 'Gets font', +'imagick::getFormat' => 'Returns the format of the Imagick object', +'imagick::getGravity' => 'Gets the gravity', +'imagick::getHomeURL' => 'Returns the ImageMagick home URL', +'imagick::getImage' => 'Returns a new Imagick object', +'imagick::getImageAlphaChannel' => 'Gets the image alpha channel', +'imagick::getImageArtifact' => 'Get image artifact', +'imagick::getImageBackgroundColor' => 'Returns the image background color', +'imagick::getImageBlob' => 'Returns the image sequence as a blob', +'imagick::getImageBluePrimary' => 'Returns the chromaticy blue primary point', +'imagick::getImageBorderColor' => 'Returns the image border color', +'imagick::getImageChannelDepth' => 'Gets the depth for a particular image channel', +'imagick::getImageChannelDistortion' => 'Compares image channels of an image to a reconstructed image', +'imagick::getImageChannelDistortions' => 'Gets channel distortions', +'imagick::getImageChannelExtrema' => 'Gets the extrema for one or more image channels', +'imagick::getImageChannelKurtosis' => 'The getImageChannelKurtosis purpose', +'imagick::getImageChannelMean' => 'Gets the mean and standard deviation', +'imagick::getImageChannelRange' => 'Gets channel range', +'imagick::getImageChannelStatistics' => 'Returns statistics for each channel in the image', +'imagick::getImageClipMask' => 'Gets image clip mask', +'imagick::getImageColormapColor' => 'Returns the color of the specified colormap index', +'imagick::getImageColors' => 'Gets the number of unique colors in the image', +'imagick::getImageColorspace' => 'Gets the image colorspace', +'imagick::getImageCompose' => 'Returns the composite operator associated with the image', +'imagick::getImageCompression' => 'Gets the current image\'s compression type', +'imagick::getImageCompressionQuality' => 'Gets the current image\'s compression quality', +'imagick::getImageDelay' => 'Gets the image delay', +'imagick::getImageDepth' => 'Gets the image depth', +'imagick::getImageDispose' => 'Gets the image disposal method', +'imagick::getImageDistortion' => 'Compares an image to a reconstructed image', +'imagick::getImageExtrema' => 'Gets the extrema for the image', +'imagick::getImageFilename' => 'Returns the filename of a particular image in a sequence', +'imagick::getImageFormat' => 'Returns the format of a particular image in a sequence', +'imagick::getImageGamma' => 'Gets the image gamma', +'imagick::getImageGeometry' => 'Gets the width and height as an associative array', +'imagick::getImageGravity' => 'Gets the image gravity', +'imagick::getImageGreenPrimary' => 'Returns the chromaticy green primary point', +'imagick::getImageHeight' => 'Returns the image height', +'imagick::getImageHistogram' => 'Gets the image histogram', +'imagick::getImageIndex' => 'Gets the index of the current active image', +'imagick::getImageInterlaceScheme' => 'Gets the image interlace scheme', +'imagick::getImageInterpolateMethod' => 'Returns the interpolation method', +'imagick::getImageIterations' => 'Gets the image iterations', +'imagick::getImageLength' => 'Returns the image length in bytes', +'imagick::getImageMagickLicense' => 'Returns a string containing the ImageMagick license', +'imagick::getImageMatte' => 'Return if the image has a matte channel', +'imagick::getImageMatteColor' => 'Returns the image matte color', +'Imagick::getImageMimeType' => '`@return string` Returns the image mime-type.', +'imagick::getImageOrientation' => 'Gets the image orientation', +'imagick::getImagePage' => 'Returns the page geometry', +'imagick::getImagePixelColor' => 'Returns the color of the specified pixel', +'imagick::getImageProfile' => 'Returns the named image profile', +'imagick::getImageProfiles' => 'Returns the image profiles', +'imagick::getImageProperties' => 'Returns the image properties', +'imagick::getImageProperty' => 'Returns the named image property', +'imagick::getImageRedPrimary' => 'Returns the chromaticity red primary point', +'imagick::getImageRegion' => 'Extracts a region of the image', +'imagick::getImageRenderingIntent' => 'Gets the image rendering intent', +'imagick::getImageResolution' => 'Gets the image X and Y resolution', +'imagick::getImagesBlob' => 'Returns all image sequences as a blob', +'imagick::getImageScene' => 'Gets the image scene', +'imagick::getImageSignature' => 'Generates an SHA-256 message digest', +'imagick::getImageSize' => 'Returns the image length in bytes', +'imagick::getImageTicksPerSecond' => 'Gets the image ticks-per-second', +'imagick::getImageTotalInkDensity' => 'Gets the image total ink density', +'imagick::getImageType' => 'Gets the potential image type', +'imagick::getImageUnits' => 'Gets the image units of resolution', +'imagick::getImageVirtualPixelMethod' => 'Returns the virtual pixel method', +'imagick::getImageWhitePoint' => 'Returns the chromaticity white point', +'imagick::getImageWidth' => 'Returns the image width', +'imagick::getInterlaceScheme' => 'Gets the object interlace scheme', +'imagick::getIteratorIndex' => 'Gets the index of the current active image', +'imagick::getNumberImages' => 'Returns the number of images in the object', +'imagick::getOption' => 'Returns a value associated with the specified key', +'imagick::getPackageName' => 'Returns the ImageMagick package name', +'imagick::getPage' => 'Returns the page geometry', +'imagick::getPixelIterator' => 'Returns a MagickPixelIterator', +'imagick::getPixelRegionIterator' => 'Get an ImagickPixelIterator for an image section', +'imagick::getPointSize' => 'Gets point size', +'Imagick::getQuantum' => 'Returns the ImageMagick quantum range as an integer.', +'imagick::getQuantumDepth' => 'Gets the quantum depth', +'imagick::getQuantumRange' => 'Returns the Imagick quantum range', +'Imagick::getRegistry' => 'Get the StringRegistry entry for the named key or false if not set.', +'imagick::getReleaseDate' => 'Returns the ImageMagick release date', +'imagick::getResource' => 'Returns the specified resource\'s memory usage', +'imagick::getResourceLimit' => 'Returns the specified resource limit', +'imagick::getSamplingFactors' => 'Gets the horizontal and vertical sampling factor', +'imagick::getSize' => 'Returns the size associated with the Imagick object', +'imagick::getSizeOffset' => 'Returns the size offset', +'imagick::getVersion' => 'Returns the ImageMagick API version', +'imagick::haldClutImage' => 'Replaces colors in the image', +'imagick::hasNextImage' => 'Checks if the object has more images', +'imagick::hasPreviousImage' => 'Checks if the object has a previous image', +'Imagick::identifyFormat' => 'Replaces any embedded formatting characters with the appropriate image property and returns the interpreted text. See https://www.imagemagick.org/script/escape.php for escape sequences.', +'imagick::identifyImage' => 'Identifies an image and fetches attributes', +'Imagick::identifyImageType' => 'Identifies the potential image type, returns one of the Imagick::IMGTYPE_* constants', +'imagick::implodeImage' => 'Creates a new image as a copy', +'imagick::importImagePixels' => 'Imports image pixels', +'Imagick::inverseFourierTransformImage' => 'Implements the inverse discrete Fourier transform (DFT) of the image either as a magnitude / phase or real / imaginary image pair.', +'imagick::labelImage' => 'Adds a label to an image', +'imagick::levelImage' => 'Adjusts the levels of an image', +'imagick::linearStretchImage' => 'Stretches with saturation the image intensity', +'imagick::liquidRescaleImage' => 'Animates an image or images', +'Imagick::listRegistry' => 'List all the registry settings. Returns an array of all the key/value pairs in the registry', +'Imagick::localContrastImage' => 'Attempts to increase the appearance of large-scale light-dark transitions.', +'imagick::magnifyImage' => 'Scales an image proportionally 2x', +'imagick::mapImage' => 'Replaces the colors of an image with the closest color from a reference image', +'imagick::matteFloodfillImage' => 'Changes the transparency value of a color', +'imagick::medianFilterImage' => 'Applies a digital filter', +'imagick::mergeImageLayers' => 'Merges image layers', +'imagick::minifyImage' => 'Scales an image proportionally to half its size', +'imagick::modulateImage' => 'Control the brightness, saturation, and hue', +'imagick::montageImage' => 'Creates a composite image', +'imagick::morphImages' => 'Method morphs a set of images', +'Imagick::morphology' => 'Applies a user supplied kernel to the image according to the given morphology method.', +'imagick::mosaicImages' => 'Forms a mosaic from images', +'imagick::motionBlurImage' => 'Simulates motion blur', +'imagick::negateImage' => 'Negates the colors in the reference image', +'imagick::newImage' => 'Creates a new image', +'imagick::newPseudoImage' => 'Creates a new image', +'imagick::nextImage' => 'Moves to the next image', +'imagick::normalizeImage' => 'Enhances the contrast of a color image', +'imagick::oilPaintImage' => 'Simulates an oil painting', +'imagick::opaquePaintImage' => 'Changes the color value of any pixel that matches target', +'imagick::optimizeImageLayers' => 'Removes repeated portions of images to optimize', +'imagick::orderedPosterizeImage' => 'Performs an ordered dither', +'imagick::paintFloodfillImage' => 'Changes the color value of any pixel that matches target', +'imagick::paintOpaqueImage' => 'Change any pixel that matches color', +'imagick::paintTransparentImage' => 'Changes any pixel that matches color with the color defined by fill', +'imagick::pingImage' => 'Fetch basic attributes about the image', +'imagick::pingImageBlob' => 'Quickly fetch attributes', +'imagick::pingImageFile' => 'Get basic image attributes in a lightweight manner', +'imagick::polaroidImage' => 'Simulates a Polaroid picture', +'imagick::posterizeImage' => 'Reduces the image to a limited number of color level', +'imagick::previewImages' => 'Quickly pin-point appropriate parameters for image processing', +'imagick::previousImage' => 'Move to the previous image in the object', +'imagick::profileImage' => 'Adds or removes a profile from an image', +'imagick::quantizeImage' => 'Analyzes the colors within a reference image', +'imagick::quantizeImages' => 'Analyzes the colors within a sequence of images', +'imagick::queryFontMetrics' => 'Returns an array representing the font metrics', +'imagick::queryFonts' => 'Returns the configured fonts', +'imagick::queryFormats' => 'Returns formats supported by Imagick', +'imagick::radialBlurImage' => 'Radial blurs an image', +'imagick::raiseImage' => 'Creates a simulated 3d button-like effect', +'imagick::randomThresholdImage' => 'Creates a high-contrast, two-color image', +'imagick::readImage' => 'Reads image from filename', +'imagick::readImageBlob' => 'Reads image from a binary string', +'imagick::readImageFile' => 'Reads image from open filehandle', +'imagick::recolorImage' => 'Recolors image', +'imagick::reduceNoiseImage' => 'Smooths the contours of an image', +'imagick::remapImage' => 'Remaps image colors', +'imagick::removeImage' => 'Removes an image from the image list', +'imagick::removeImageProfile' => 'Removes the named image profile and returns it', +'imagick::render' => 'Renders all preceding drawing commands', +'imagick::resampleImage' => 'Resample image to desired resolution', +'imagick::resetImagePage' => 'Reset image page', +'imagick::resizeImage' => 'Scales an image', +'imagick::rollImage' => 'Offsets an image', +'imagick::rotateImage' => 'Rotates an image', +'Imagick::rotationalBlurImage' => 'Rotational blurs an image.', +'imagick::roundCorners' => 'Rounds image corners', +'imagick::sampleImage' => 'Scales an image with pixel sampling', +'imagick::scaleImage' => 'Scales the size of an image', +'imagick::segmentImage' => 'Segments an image', +'Imagick::selectiveBlurImage' => 'Selectively blur an image within a contrast threshold. It is similar to the unsharpen mask that sharpens everything with contrast above a certain threshold.', +'imagick::separateImageChannel' => 'Separates a channel from the image', +'imagick::sepiaToneImage' => 'Sepia tones an image', +'Imagick::setAntiAlias' => 'Set whether antialiasing should be used for operations. On by default.', +'imagick::setBackgroundColor' => 'Sets the object\'s default background color', +'imagick::setColorspace' => 'Set colorspace', +'imagick::setCompression' => 'Sets the object\'s default compression type', +'imagick::setCompressionQuality' => 'Sets the object\'s default compression quality', +'imagick::setFilename' => 'Sets the filename before you read or write the image', +'imagick::setFirstIterator' => 'Sets the Imagick iterator to the first image', +'imagick::setFont' => 'Sets font', +'imagick::setFormat' => 'Sets the format of the Imagick object', +'imagick::setGravity' => 'Sets the gravity', +'imagick::setImage' => 'Replaces image in the object', +'Imagick::setImageAlpha' => 'Sets the image to the specified alpha level. Will replace ImagickDraw::setOpacity()', +'imagick::setImageAlphaChannel' => 'Sets image alpha channel', +'imagick::setImageArtifact' => 'Set image artifact', +'imagick::setImageBackgroundColor' => 'Sets the image background color', +'imagick::setImageBias' => 'Sets the image bias for any method that convolves an image', +'imagick::setImageBluePrimary' => 'Sets the image chromaticity blue primary point', +'imagick::setImageBorderColor' => 'Sets the image border color', +'imagick::setImageChannelDepth' => 'Sets the depth of a particular image channel', +'Imagick::setImageChannelMask' => 'Sets the image channel mask. Returns the previous set channel mask. +Only works with Imagick >=7', +'imagick::setImageClipMask' => 'Sets image clip mask', +'imagick::setImageColormapColor' => 'Sets the color of the specified colormap index', +'imagick::setImageColorspace' => 'Sets the image colorspace', +'imagick::setImageCompose' => 'Sets the image composite operator', +'imagick::setImageCompression' => 'Sets the image compression', +'imagick::setImageCompressionQuality' => 'Sets the image compression quality', +'imagick::setImageDelay' => 'Sets the image delay', +'imagick::setImageDepth' => 'Sets the image depth', +'imagick::setImageDispose' => 'Sets the image disposal method', +'imagick::setImageExtent' => 'Sets the image size', +'imagick::setImageFilename' => 'Sets the filename of a particular image', +'imagick::setImageFormat' => 'Sets the format of a particular image', +'imagick::setImageGamma' => 'Sets the image gamma', +'imagick::setImageGravity' => 'Sets the image gravity', +'imagick::setImageGreenPrimary' => 'Sets the image chromaticity green primary point', +'imagick::setImageIndex' => 'Set the iterator position', +'imagick::setImageInterlaceScheme' => 'Sets the image compression', +'imagick::setImageInterpolateMethod' => 'Sets the image interpolate pixel method', +'imagick::setImageIterations' => 'Sets the image iterations', +'imagick::setImageMatte' => 'Sets the image matte channel', +'imagick::setImageMatteColor' => 'Sets the image matte color', +'imagick::setImageOpacity' => 'Sets the image opacity level', +'imagick::setImageOrientation' => 'Sets the image orientation', +'imagick::setImagePage' => 'Sets the page geometry of the image', +'imagick::setImageProfile' => 'Adds a named profile to the Imagick object', +'imagick::setImageProperty' => 'Sets an image property', +'imagick::setImageRedPrimary' => 'Sets the image chromaticity red primary point', +'imagick::setImageRenderingIntent' => 'Sets the image rendering intent', +'imagick::setImageResolution' => 'Sets the image resolution', +'imagick::setImageScene' => 'Sets the image scene', +'imagick::setImageTicksPerSecond' => 'Sets the image ticks-per-second', +'imagick::setImageType' => 'Sets the image type', +'imagick::setImageUnits' => 'Sets the image units of resolution', +'imagick::setImageVirtualPixelMethod' => 'Sets the image virtual pixel method', +'imagick::setImageWhitePoint' => 'Sets the image chromaticity white point', +'imagick::setInterlaceScheme' => 'Sets the image compression', +'imagick::setIteratorIndex' => 'Set the iterator position', +'imagick::setLastIterator' => 'Sets the Imagick iterator to the last image', +'imagick::setOption' => 'Set an option', +'imagick::setPage' => 'Sets the page geometry of the Imagick object', +'imagick::setPointSize' => 'Sets point size', +'Imagick::setProgressMonitor' => 'Set a callback that will be called during the processing of the Imagick image.', +'Imagick::setRegistry' => 'Sets the ImageMagick registry entry named key to value. This is most useful for setting "temporary-path" which controls where ImageMagick creates temporary images e.g. while processing PDFs.', +'imagick::setResolution' => 'Sets the image resolution', +'imagick::setResourceLimit' => 'Sets the limit for a particular resource', +'imagick::setSamplingFactors' => 'Sets the image sampling factors', +'imagick::setSize' => 'Sets the size of the Imagick object', +'imagick::setSizeOffset' => 'Sets the size and offset of the Imagick object', +'imagick::setType' => 'Sets the image type attribute', +'imagick::shadeImage' => 'Creates a 3D effect', +'imagick::shadowImage' => 'Simulates an image shadow', +'imagick::sharpenImage' => 'Sharpens an image', +'imagick::shaveImage' => 'Shaves pixels from the image edges', +'imagick::shearImage' => 'Creating a parallelogram', +'imagick::sigmoidalContrastImage' => 'Adjusts the contrast of an image', +'Imagick::similarityImage' => 'Is an alias of Imagick::subImageMatch', +'imagick::sketchImage' => 'Simulates a pencil sketch', +'imagick::solarizeImage' => 'Applies a solarizing effect to the image', +'imagick::sparseColorImage' => 'Interpolates colors', +'imagick::spliceImage' => 'Splices a solid color into the image', +'imagick::spreadImage' => 'Randomly displaces each pixel in a block', +'Imagick::statisticImage' => 'Replace each pixel with corresponding statistic from the neighborhood of the specified width and height.', +'imagick::steganoImage' => 'Hides a digital watermark within the image', +'imagick::stereoImage' => 'Composites two images', +'imagick::stripImage' => 'Strips an image of all profiles and comments', +'Imagick::subImageMatch' => 'Searches for a subimage in the current image and returns a similarity image such that an exact match location is +completely white and if none of the pixels match, black, otherwise some gray level in-between. +You can also pass in the optional parameters bestMatch and similarity. After calling the function similarity will +be set to the \'score\' of the similarity between the subimage and the matching position in the larger image, +bestMatch will contain an associative array with elements x, y, width, height that describe the matching region.', +'imagick::swirlImage' => 'Swirls the pixels about the center of the image', +'imagick::textureImage' => 'Repeatedly tiles the texture image', +'imagick::thresholdImage' => 'Changes the value of individual pixels based on a threshold', +'imagick::thumbnailImage' => 'Changes the size of an image', +'imagick::tintImage' => 'Applies a color vector to each pixel in the image', +'imagick::transformImage' => 'Convenience method for setting crop size and the image geometry', +'imagick::transformImageColorspace' => 'Transforms an image to a new colorspace', +'imagick::transparentPaintImage' => 'Paints pixels transparent', +'imagick::transposeImage' => 'Creates a vertical mirror image', +'imagick::transverseImage' => 'Creates a horizontal mirror image', +'imagick::trimImage' => 'Remove edges from the image', +'imagick::uniqueImageColors' => 'Discards all but one of any pixel color', +'imagick::unsharpMaskImage' => 'Sharpens an image', +'imagick::valid' => 'Checks if the current item is valid', +'imagick::vignetteImage' => 'Adds vignette filter to the image', +'imagick::waveImage' => 'Applies wave filter to the image', +'imagick::whiteThresholdImage' => 'Force all pixels above the threshold into white', +'imagick::writeImage' => 'Writes an image to the specified filename', +'imagick::writeImageFile' => 'Writes an image to a filehandle', +'imagick::writeImages' => 'Writes an image or image sequence', +'imagick::writeImagesFile' => 'Writes frames to a filehandle', +'imagickdraw::__construct' => 'The ImagickDraw constructor', +'imagickdraw::affine' => 'Adjusts the current affine transformation matrix', +'imagickdraw::annotation' => 'Draws text on the image', +'imagickdraw::arc' => 'Draws an arc', +'imagickdraw::bezier' => 'Draws a bezier curve', +'imagickdraw::circle' => 'Draws a circle', +'imagickdraw::clear' => 'Clears the ImagickDraw', +'imagickdraw::clone' => 'Makes an exact copy of the specified ImagickDraw object', +'imagickdraw::color' => 'Draws color on image', +'imagickdraw::comment' => 'Adds a comment', +'imagickdraw::composite' => 'Composites an image onto the current image', +'imagickdraw::destroy' => 'Frees all associated resources', +'imagickdraw::ellipse' => 'Draws an ellipse on the image', +'ImagickDraw::getBorderColor' => 'Returns the border color used for drawing bordered objects.', +'imagickdraw::getClipPath' => 'Obtains the current clipping path ID', +'imagickdraw::getClipRule' => 'Returns the current polygon fill rule', +'imagickdraw::getClipUnits' => 'Returns the interpretation of clip path units', +'ImagickDraw::getDensity' => 'Obtains the vertical and horizontal resolution.', +'imagickdraw::getFillColor' => 'Returns the fill color', +'imagickdraw::getFillOpacity' => 'Returns the opacity used when drawing', +'imagickdraw::getFillRule' => 'Returns the fill rule', +'imagickdraw::getFont' => 'Returns the font', +'imagickdraw::getFontFamily' => 'Returns the font family', +'ImagickDraw::getFontResolution' => 'Gets the image X and Y resolution.', +'imagickdraw::getFontSize' => 'Returns the font pointsize', +'imagickdraw::getFontStyle' => 'Returns the font style', +'imagickdraw::getFontWeight' => 'Returns the font weight', +'imagickdraw::getGravity' => 'Returns the text placement gravity', +'ImagickDraw::getOpacity' => 'Returns the opacity used when drawing with the fill or stroke color or texture. Fully opaque is 1.0.', +'imagickdraw::getStrokeAntialias' => 'Returns the current stroke antialias setting', +'imagickdraw::getStrokeColor' => 'Returns the color used for stroking object outlines', +'imagickdraw::getStrokeDashArray' => 'Returns an array representing the pattern of dashes and gaps used to stroke paths', +'imagickdraw::getStrokeDashOffset' => 'Returns the offset into the dash pattern to start the dash', +'imagickdraw::getStrokeLineCap' => 'Returns the shape to be used at the end of open subpaths when they are stroked', +'imagickdraw::getStrokeLineJoin' => 'Returns the shape to be used at the corners of paths when they are stroked', +'imagickdraw::getStrokeMiterLimit' => 'Returns the stroke miter limit', +'imagickdraw::getStrokeOpacity' => 'Returns the opacity of stroked object outlines', +'imagickdraw::getStrokeWidth' => 'Returns the width of the stroke used to draw object outlines', +'imagickdraw::getTextAlignment' => 'Returns the text alignment', +'imagickdraw::getTextAntialias' => 'Returns the current text antialias setting', +'imagickdraw::getTextDecoration' => 'Returns the text decoration', +'ImagickDraw::getTextDirection' => 'Returns the direction that will be used when annotating with text.', +'imagickdraw::getTextEncoding' => 'Returns the code set used for text annotations', +'imagickdraw::getTextUnderColor' => 'Returns the text under color', +'imagickdraw::getVectorGraphics' => 'Returns a string containing vector graphics', +'imagickdraw::line' => 'Draws a line', +'imagickdraw::matte' => 'Paints on the image\'s opacity channel', +'imagickdraw::pathClose' => 'Adds a path element to the current path', +'imagickdraw::pathCurveToAbsolute' => 'Draws a cubic Bezier curve', +'imagickdraw::pathCurveToQuadraticBezierAbsolute' => 'Draws a quadratic Bezier curve', +'imagickdraw::pathCurveToQuadraticBezierRelative' => 'Draws a quadratic Bezier curve', +'imagickdraw::pathCurveToQuadraticBezierSmoothAbsolute' => 'Draws a quadratic Bezier curve', +'imagickdraw::pathCurveToQuadraticBezierSmoothRelative' => 'Draws a quadratic Bezier curve', +'imagickdraw::pathCurveToRelative' => 'Draws a cubic Bezier curve', +'imagickdraw::pathCurveToSmoothAbsolute' => 'Draws a cubic Bezier curve', +'imagickdraw::pathCurveToSmoothRelative' => 'Draws a cubic Bezier curve', +'imagickdraw::pathEllipticArcAbsolute' => 'Draws an elliptical arc', +'imagickdraw::pathEllipticArcRelative' => 'Draws an elliptical arc', +'imagickdraw::pathFinish' => 'Terminates the current path', +'imagickdraw::pathLineToAbsolute' => 'Draws a line path', +'imagickdraw::pathLineToHorizontalAbsolute' => 'Draws a horizontal line path', +'imagickdraw::pathLineToHorizontalRelative' => 'Draws a horizontal line', +'imagickdraw::pathLineToRelative' => 'Draws a line path', +'imagickdraw::pathLineToVerticalAbsolute' => 'Draws a vertical line', +'imagickdraw::pathLineToVerticalRelative' => 'Draws a vertical line path', +'imagickdraw::pathMoveToAbsolute' => 'Starts a new sub-path', +'imagickdraw::pathMoveToRelative' => 'Starts a new sub-path', +'imagickdraw::pathStart' => 'Declares the start of a path drawing list', +'imagickdraw::point' => 'Draws a point', +'imagickdraw::polygon' => 'Draws a polygon', +'imagickdraw::polyline' => 'Draws a polyline', +'imagickdraw::pop' => 'Destroys the current ImagickDraw in the stack, and returns to the previously pushed ImagickDraw', +'imagickdraw::popClipPath' => 'Terminates a clip path definition', +'imagickdraw::popDefs' => 'Terminates a definition list', +'imagickdraw::popPattern' => 'Terminates a pattern definition', +'imagickdraw::push' => 'Clones the current ImagickDraw and pushes it to the stack', +'imagickdraw::pushClipPath' => 'Starts a clip path definition', +'imagickdraw::pushDefs' => 'Indicates that following commands create named elements for early processing', +'imagickdraw::pushPattern' => 'Indicates that subsequent commands up to a ImagickDraw::opPattern() command comprise the definition of a named pattern', +'imagickdraw::rectangle' => 'Draws a rectangle', +'imagickdraw::render' => 'Renders all preceding drawing commands onto the image', +'imagickdraw::rotate' => 'Applies the specified rotation to the current coordinate space', +'imagickdraw::roundRectangle' => 'Draws a rounded rectangle', +'imagickdraw::scale' => 'Adjusts the scaling factor', +'ImagickDraw::setBorderColor' => 'Sets the border color to be used for drawing bordered objects.', +'imagickdraw::setClipPath' => 'Associates a named clipping path with the image', +'imagickdraw::setClipRule' => 'Set the polygon fill rule to be used by the clipping path', +'imagickdraw::setClipUnits' => 'Sets the interpretation of clip path units', +'ImagickDraw::setDensity' => 'Sets the vertical and horizontal resolution.', +'imagickdraw::setFillAlpha' => 'Sets the opacity to use when drawing using the fill color or fill texture', +'imagickdraw::setFillColor' => 'Sets the fill color to be used for drawing filled objects', +'imagickdraw::setFillOpacity' => 'Sets the opacity to use when drawing using the fill color or fill texture', +'imagickdraw::setFillPatternURL' => 'Sets the URL to use as a fill pattern for filling objects', +'imagickdraw::setFillRule' => 'Sets the fill rule to use while drawing polygons', +'imagickdraw::setFont' => 'Sets the fully-specified font to use when annotating with text', +'imagickdraw::setFontFamily' => 'Sets the font family to use when annotating with text', +'ImagickDraw::setFontResolution' => 'Sets the image font resolution.', +'imagickdraw::setFontSize' => 'Sets the font pointsize to use when annotating with text', +'imagickdraw::setFontStretch' => 'Sets the font stretch to use when annotating with text', +'imagickdraw::setFontStyle' => 'Sets the font style to use when annotating with text', +'imagickdraw::setFontWeight' => 'Sets the font weight', +'imagickdraw::setGravity' => 'Sets the text placement gravity', +'ImagickDraw::setOpacity' => 'Sets the opacity to use when drawing using the fill or stroke color or texture. Fully opaque is 1.0.', +'imagickdraw::setStrokeAlpha' => 'Specifies the opacity of stroked object outlines', +'imagickdraw::setStrokeAntialias' => 'Controls whether stroked outlines are antialiased', +'imagickdraw::setStrokeColor' => 'Sets the color used for stroking object outlines', +'imagickdraw::setStrokeDashArray' => 'Specifies the pattern of dashes and gaps used to stroke paths', +'imagickdraw::setStrokeDashOffset' => 'Specifies the offset into the dash pattern to start the dash', +'imagickdraw::setStrokeLineCap' => 'Specifies the shape to be used at the end of open subpaths when they are stroked', +'imagickdraw::setStrokeLineJoin' => 'Specifies the shape to be used at the corners of paths when they are stroked', +'imagickdraw::setStrokeMiterLimit' => 'Specifies the miter limit', +'imagickdraw::setStrokeOpacity' => 'Specifies the opacity of stroked object outlines', +'imagickdraw::setStrokePatternURL' => 'Sets the pattern used for stroking object outlines', +'imagickdraw::setStrokeWidth' => 'Sets the width of the stroke used to draw object outlines', +'imagickdraw::setTextAlignment' => 'Specifies a text alignment', +'imagickdraw::setTextAntialias' => 'Controls whether text is antialiased', +'imagickdraw::setTextDecoration' => 'Specifies a decoration', +'ImagickDraw::setTextDirection' => 'Sets the font style to use when annotating with text. The AnyStyle enumeration acts as a wild-card "don\'t care" option.', +'imagickdraw::setTextEncoding' => 'Specifies the text code set', +'imagickdraw::setTextUnderColor' => 'Specifies the color of a background rectangle', +'imagickdraw::setVectorGraphics' => 'Sets the vector graphics', +'imagickdraw::setViewbox' => 'Sets the overall canvas size', +'imagickdraw::skewX' => 'Skews the current coordinate system in the horizontal direction', +'imagickdraw::skewY' => 'Skews the current coordinate system in the vertical direction', +'imagickdraw::translate' => 'Applies a translation to the current coordinate system', +'ImagickKernel::addKernel' => 'Attach another kernel to this kernel to allow them to both be applied in a single morphology or filter function. Returns the new combined kernel.', +'ImagickKernel::addUnityKernel' => 'Adds a given amount of the \'Unity\' Convolution Kernel to the given pre-scaled and normalized Kernel. This in effect adds that amount of the original image into the resulting convolution kernel. The resulting effect is to convert the defined kernels into blended soft-blurs, unsharp kernels or into sharpening kernels.', +'ImagickKernel::fromBuiltin' => 'Create a kernel from a builtin in kernel. See https://www.imagemagick.org/Usage/morphology/#kernel for examples.
+Currently the \'rotation\' symbols are not supported. Example: $diamondKernel = ImagickKernel::fromBuiltIn(\Imagick::KERNEL_DIAMOND, "2");', +'ImagickKernel::fromMatrix' => 'Create a kernel from a builtin in kernel. See https://www.imagemagick.org/Usage/morphology/#kernel for examples.
+Currently the \'rotation\' symbols are not supported. Example: $diamondKernel = ImagickKernel::fromBuiltIn(\Imagick::KERNEL_DIAMOND, "2");', +'ImagickKernel::getMatrix' => 'Get the 2d matrix of values used in this kernel. The elements are either float for elements that are used or \'false\' if the element should be skipped.', +'ImagickKernel::scale' => 'ScaleKernelInfo() scales the given kernel list by the given amount, with or without normalization of the sum of the kernel values (as per given flags).
+The exact behaviour of this function depends on the normalization type being used please see https://www.imagemagick.org/api/morphology.php#ScaleKernelInfo for details.
+Flag should be one of Imagick::NORMALIZE_KERNEL_VALUE, Imagick::NORMALIZE_KERNEL_CORRELATE, Imagick::NORMALIZE_KERNEL_PERCENT or not set.', +'ImagickKernel::seperate' => 'Separates a linked set of kernels and returns an array of ImagickKernels.', +'imagickpixel::__construct' => 'The ImagickPixel constructor', +'imagickpixel::clear' => 'Clears resources associated with this object', +'imagickpixel::destroy' => 'Deallocates resources associated with this object', +'imagickpixel::getColor' => 'Returns the color', +'imagickpixel::getColorAsString' => 'Returns the color as a string', +'imagickpixel::getColorCount' => 'Returns the color count associated with this color', +'ImagickPixel::getColorQuantum' => 'Returns the color of the pixel in an array as Quantum values. If ImageMagick was compiled as HDRI these will be floats, otherwise they will be integers.', +'imagickpixel::getColorValue' => 'Gets the normalized value of the provided color channel', +'imagickpixel::getHSL' => 'Returns the normalized HSL color of the ImagickPixel object', +'imagickpixel::isPixelSimilar' => 'Check the distance between this color and another', +'ImagickPixel::isPixelSimilarQuantum' => 'Returns true if the distance between two colors is less than the specified distance. The fuzz value should be in the range 0-QuantumRange.
+The maximum value represents the longest possible distance in the colorspace. e.g. from RGB(0, 0, 0) to RGB(255, 255, 255) for the RGB colorspace', +'imagickpixel::isSimilar' => 'Check the distance between this color and another', +'imagickpixel::setColor' => 'Sets the color', +'ImagickPixel::setColorFromPixel' => 'Sets the color count associated with this color from another ImagickPixel object.', +'imagickpixel::setColorValue' => 'Sets the normalized value of one of the channels', +'imagickpixel::setHSL' => 'Sets the normalized HSL color', +'imagickpixeliterator::__construct' => 'The ImagickPixelIterator constructor', +'imagickpixeliterator::clear' => 'Clear resources associated with a PixelIterator', +'imagickpixeliterator::destroy' => 'Deallocates resources associated with a PixelIterator', +'imagickpixeliterator::getCurrentIteratorRow' => 'Returns the current row of ImagickPixel objects', +'imagickpixeliterator::getIteratorRow' => 'Returns the current pixel iterator row', +'imagickpixeliterator::getNextIteratorRow' => 'Returns the next row of the pixel iterator', +'imagickpixeliterator::getPreviousIteratorRow' => 'Returns the previous row', +'imagickpixeliterator::newPixelIterator' => 'Returns a new pixel iterator', +'imagickpixeliterator::newPixelRegionIterator' => 'Returns a new pixel iterator', +'imagickpixeliterator::resetIterator' => 'Resets the pixel iterator', +'imagickpixeliterator::setIteratorFirstRow' => 'Sets the pixel iterator to the first pixel row', +'imagickpixeliterator::setIteratorLastRow' => 'Sets the pixel iterator to the last pixel row', +'imagickpixeliterator::setIteratorRow' => 'Set the pixel iterator row', +'imagickpixeliterator::syncIterator' => 'Syncs the pixel iterator', +'imap_8bit' => 'Convert an 8bit string to a quoted-printable string', +'imap_alerts' => 'Returns all IMAP alert messages that have occurred', +'imap_append' => 'Append a string message to a specified mailbox', +'imap_base64' => 'Decode BASE64 encoded text', +'imap_binary' => 'Convert an 8bit string to a base64 string', +'imap_body' => 'Read the message body', +'imap_bodystruct' => 'Read the structure of a specified body section of a specific message', +'imap_check' => 'Check current mailbox', +'imap_clearflag_full' => 'Clears flags on messages', +'imap_close' => 'Close an IMAP stream', +'imap_create' => 'Alias of imap_createmailbox', +'imap_createmailbox' => 'Create a new mailbox', +'imap_delete' => 'Mark a message for deletion from current mailbox', +'imap_deletemailbox' => 'Delete a mailbox', +'imap_errors' => 'Returns all of the IMAP errors that have occurred', +'imap_expunge' => 'Delete all messages marked for deletion', +'imap_fetch_overview' => 'Read an overview of the information in the headers of the given message', +'imap_fetchbody' => 'Fetch a particular section of the body of the message', +'imap_fetchheader' => 'Returns header for a message', +'imap_fetchmime' => 'Fetch MIME headers for a particular section of the message', +'imap_fetchstructure' => 'Read the structure of a particular message', +'imap_fetchtext' => 'Alias of imap_body', +'imap_gc' => 'Clears IMAP cache', +'imap_get_quota' => 'Retrieve the quota level settings, and usage statics per mailbox', +'imap_get_quotaroot' => 'Retrieve the quota settings per user', +'imap_getacl' => 'Gets the ACL for a given mailbox', +'imap_getmailboxes' => 'Read the list of mailboxes, returning detailed information on each one', +'imap_getsubscribed' => 'List all the subscribed mailboxes', +'imap_header' => 'Alias of imap_headerinfo', +'imap_headerinfo' => 'Read the header of the message', +'imap_headers' => 'Returns headers for all messages in a mailbox', +'imap_last_error' => 'Gets the last IMAP error that occurred during this page request', +'imap_list' => 'Read the list of mailboxes', +'imap_listmailbox' => 'Alias of imap_list', +'imap_listscan' => 'Returns the list of mailboxes that matches the given text', +'imap_listsubscribed' => 'Alias of imap_lsub', +'imap_lsub' => 'List all the subscribed mailboxes', +'imap_mail' => 'Send an email message', +'imap_mail_compose' => 'Create a MIME message based on given envelope and body sections', +'imap_mail_copy' => 'Copy specified messages to a mailbox', +'imap_mail_move' => 'Move specified messages to a mailbox', +'imap_mailboxmsginfo' => 'Get information about the current mailbox', +'imap_mime_header_decode' => 'Decode MIME header elements', +'imap_msgno' => 'Gets the message sequence number for the given UID', +'imap_mutf7_to_utf8' => 'Decode a modified UTF-7 string to UTF-8', +'imap_num_msg' => 'Gets the number of messages in the current mailbox', +'imap_num_recent' => 'Gets the number of recent messages in current mailbox', +'imap_open' => 'Open an IMAP stream to a mailbox', +'imap_ping' => 'Check if the IMAP stream is still active', +'imap_qprint' => 'Convert a quoted-printable string to an 8 bit string', +'imap_rename' => 'Alias of imap_renamemailbox', +'imap_renamemailbox' => 'Rename an old mailbox to new mailbox', +'imap_reopen' => 'Reopen IMAP stream to new mailbox', +'imap_rfc822_parse_adrlist' => 'Parses an address string', +'imap_rfc822_parse_headers' => 'Parse mail headers from a string', +'imap_rfc822_write_address' => 'Returns a properly formatted email address given the mailbox, host, and personal info', +'imap_savebody' => 'Save a specific body section to a file', +'imap_scan' => 'Alias of imap_listscan', +'imap_scanmailbox' => 'Alias of imap_listscan', +'imap_search' => 'This function returns an array of messages matching the given search criteria', +'imap_set_quota' => 'Sets a quota for a given mailbox', +'imap_setacl' => 'Sets the ACL for a given mailbox', +'imap_setflag_full' => 'Sets flags on messages', +'imap_sort' => 'Gets and sort messages', +'imap_status' => 'Returns status information on a mailbox', +'imap_subscribe' => 'Subscribe to a mailbox', +'imap_thread' => 'Returns a tree of threaded message', +'imap_timeout' => 'Set or fetch imap timeout', +'imap_uid' => 'This function returns the UID for the given message sequence number', +'imap_undelete' => 'Unmark the message which is marked deleted', +'imap_unsubscribe' => 'Unsubscribe from a mailbox', +'imap_utf7_decode' => 'Decodes a modified UTF-7 encoded string', +'imap_utf7_encode' => 'Converts ISO-8859-1 string to modified UTF-7 text', +'imap_utf8' => 'Converts MIME-encoded text to UTF-8', +'imap_utf8_to_mutf7' => 'Encode a UTF-8 string to modified UTF-7', +'implode' => 'Join array elements with a string', +'import_request_variables' => 'Import GET/POST/Cookie variables into the global scope', +'in_array' => 'Checks if a value exists in an array', +'inclued_get_data' => 'Get the inclued data', +'inet_ntop' => 'Converts a packed internet address to a human readable representation', +'inet_pton' => 'Converts a human readable IP address to its packed in_addr representation', +'infiniteiterator::__construct' => 'Constructs an InfiniteIterator', +'InfiniteIterator::current' => 'Get the current value', +'InfiniteIterator::getInnerIterator' => 'Get the inner iterator', +'InfiniteIterator::key' => 'Get the key of the current element', +'infiniteiterator::next' => 'Moves the inner Iterator forward or rewinds it', +'InfiniteIterator::rewind' => 'Rewind to the first element', +'InfiniteIterator::valid' => 'Checks if the iterator is valid', +'inflate_add' => 'Incrementally inflate encoded data', +'inflate_get_read_len' => 'Get number of bytes read so far', +'inflate_get_status' => 'Get decompression status', +'inflate_init' => 'Initialize an incremental inflate context', +'ingres_autocommit' => 'Switch autocommit on or off', +'ingres_autocommit_state' => 'Test if the connection is using autocommit', +'ingres_charset' => 'Returns the installation character set', +'ingres_close' => 'Close an Ingres database connection', +'ingres_commit' => 'Commit a transaction', +'ingres_connect' => 'Open a connection to an Ingres database', +'ingres_cursor' => 'Get a cursor name for a given result resource', +'ingres_errno' => 'Get the last Ingres error number generated', +'ingres_error' => 'Get a meaningful error message for the last error generated', +'ingres_errsqlstate' => 'Get the last SQLSTATE error code generated', +'ingres_escape_string' => 'Escape special characters for use in a query', +'ingres_execute' => 'Execute a prepared query', +'ingres_fetch_array' => 'Fetch a row of result into an array', +'ingres_fetch_assoc' => 'Fetch a row of result into an associative array', +'ingres_fetch_object' => 'Fetch a row of result into an object', +'ingres_fetch_proc_return' => 'Get the return value from a procedure call', +'ingres_fetch_row' => 'Fetch a row of result into an enumerated array', +'ingres_field_length' => 'Get the length of a field', +'ingres_field_name' => 'Get the name of a field in a query result', +'ingres_field_nullable' => 'Test if a field is nullable', +'ingres_field_precision' => 'Get the precision of a field', +'ingres_field_scale' => 'Get the scale of a field', +'ingres_field_type' => 'Get the type of a field in a query result', +'ingres_free_result' => 'Free the resources associated with a result identifier', +'ingres_next_error' => 'Get the next Ingres error', +'ingres_num_fields' => 'Get the number of fields returned by the last query', +'ingres_num_rows' => 'Get the number of rows affected or returned by a query', +'ingres_pconnect' => 'Open a persistent connection to an Ingres database', +'ingres_prepare' => 'Prepare a query for later execution', +'ingres_query' => 'Send an SQL query to Ingres', +'ingres_result_seek' => 'Set the row position before fetching data', +'ingres_rollback' => 'Roll back a transaction', +'ingres_set_environment' => 'Set environment features controlling output options', +'ingres_unbuffered_query' => 'Send an unbuffered SQL query to Ingres', +'ini_alter' => 'Alias of ini_set', +'ini_get' => 'Gets the value of a configuration option', +'ini_get_all' => 'Gets all configuration options', +'ini_restore' => 'Restores the value of a configuration option', +'ini_set' => 'Sets the value of a configuration option', +'inotify_add_watch' => 'Add a watch to an initialized inotify instance', +'inotify_init' => 'Initialize an inotify instance', +'inotify_queue_len' => 'Return a number upper than zero if there are pending events', +'inotify_read' => 'Read events from an inotify instance', +'inotify_rm_watch' => 'Remove an existing watch from an inotify instance', +'intcal_get_maximum' => '(PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
+Get the global maximum value for a field', +'intdiv' => 'Integer division', +'interface_exists' => 'Checks if the interface has been defined', +'intl_get' => '(PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
+Get the value for a field', +'intlbreakiterator::__construct' => 'Private constructor for disallowing instantiation', +'intlbreakiterator::createCharacterInstance' => 'Create break iterator for boundaries of combining character sequences', +'intlbreakiterator::createCodePointInstance' => 'Create break iterator for boundaries of code points', +'intlbreakiterator::createLineInstance' => 'Create break iterator for logically possible line breaks', +'intlbreakiterator::createSentenceInstance' => 'Create break iterator for sentence breaks', +'intlbreakiterator::createTitleInstance' => 'Create break iterator for title-casing breaks', +'intlbreakiterator::createWordInstance' => 'Create break iterator for word breaks', +'intlbreakiterator::current' => 'Get index of current position', +'intlbreakiterator::first' => 'Set position to the first character in the text', +'intlbreakiterator::following' => 'Advance the iterator to the first boundary following specified offset', +'intlbreakiterator::getErrorCode' => 'Get last error code on the object', +'intlbreakiterator::getErrorMessage' => 'Get last error message on the object', +'intlbreakiterator::getLocale' => 'Get the locale associated with the object', +'intlbreakiterator::getPartsIterator' => 'Create iterator for navigating fragments between boundaries', +'intlbreakiterator::getText' => 'Get the text being scanned', +'intlbreakiterator::isBoundary' => 'Tell whether an offset is a boundaryʼs offset', +'intlbreakiterator::last' => 'Set the iterator position to index beyond the last character', +'intlbreakiterator::next' => 'Advance the iterator the next boundary', +'intlbreakiterator::preceding' => 'Set the iterator position to the first boundary before an offset', +'intlbreakiterator::previous' => 'Set the iterator position to the boundary immediately before the current', +'intlbreakiterator::setText' => 'Set the text being scanned', +'intlcal_greates_minimum' => '(PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
+Get the largest local minimum value for a field', +'intlcalendar::__construct' => 'Private constructor for disallowing instantiation', +'intlcalendar::add' => 'Add a (signed) amount of time to a field', +'intlcalendar::after' => 'Whether this objectʼs time is after that of the passed object', +'intlcalendar::before' => 'Whether this objectʼs time is before that of the passed object', +'intlcalendar::clear' => 'Clear a field or all fields', +'intlcalendar::createInstance' => 'Create a new IntlCalendar', +'intlcalendar::equals' => 'Compare time of two IntlCalendar objects for equality', +'intlcalendar::fieldDifference' => 'Calculate difference between given time and this objectʼs time', +'intlcalendar::fromDateTime' => 'Create an IntlCalendar from a DateTime object or string', +'intlcalendar::get' => 'Get the value for a field', +'intlcalendar::getActualMaximum' => 'The maximum value for a field, considering the objectʼs current time', +'intlcalendar::getActualMinimum' => 'The minimum value for a field, considering the objectʼs current time', +'intlcalendar::getAvailableLocales' => 'Get array of locales for which there is data', +'intlcalendar::getDayOfWeekType' => 'Tell whether a day is a weekday, weekend or a day that has a transition between the two', +'intlcalendar::getErrorCode' => 'Get last error code on the object', +'intlcalendar::getErrorMessage' => 'Get last error message on the object', +'intlcalendar::getFirstDayOfWeek' => 'Get the first day of the week for the calendarʼs locale', +'intlcalendar::getGreatestMinimum' => 'Get the largest local minimum value for a field', +'intlcalendar::getKeywordValuesForLocale' => 'Get set of locale keyword values', +'intlcalendar::getLeastMaximum' => 'Get the smallest local maximum for a field', +'intlcalendar::getLocale' => 'Get the locale associated with the object', +'intlcalendar::getMaximum' => 'Get the global maximum value for a field', +'intlcalendar::getMinimalDaysInFirstWeek' => 'Get minimal number of days the first week in a year or month can have', +'intlcalendar::getMinimum' => 'Get the global minimum value for a field', +'intlcalendar::getNow' => 'Get number representing the current time', +'intlcalendar::getRepeatedWallTimeOption' => 'Get behavior for handling repeating wall time', +'intlcalendar::getSkippedWallTimeOption' => 'Get behavior for handling skipped wall time', +'intlcalendar::getTime' => 'Get time currently represented by the object', +'intlcalendar::getTimeZone' => 'Get the objectʼs timezone', +'intlcalendar::getType' => 'Get the calendar type', +'intlcalendar::getWeekendTransition' => 'Get time of the day at which weekend begins or ends', +'intlcalendar::inDaylightTime' => 'Whether the objectʼs time is in Daylight Savings Time', +'intlcalendar::isEquivalentTo' => 'Whether another calendar is equal but for a different time', +'intlcalendar::isLenient' => 'Whether date/time interpretation is in lenient mode', +'intlcalendar::isSet' => 'Whether a field is set', +'intlcalendar::isWeekend' => 'Whether a certain date/time is in the weekend', +'intlcalendar::roll' => 'Add value to field without carrying into more significant fields', +'intlcalendar::set' => 'Set a time field or several common fields at once', +'intlcalendar::setFirstDayOfWeek' => 'Set the day on which the week is deemed to start', +'intlcalendar::setLenient' => 'Set whether date/time interpretation is to be lenient', +'intlcalendar::setMinimalDaysInFirstWeek' => 'Set minimal number of days the first week in a year or month can have', +'intlcalendar::setRepeatedWallTimeOption' => 'Set behavior for handling repeating wall times at negative timezone offset transitions', +'intlcalendar::setSkippedWallTimeOption' => 'Set behavior for handling skipped wall times at positive timezone offset transitions', +'intlcalendar::setTime' => 'Set the calendar time in milliseconds since the epoch', +'intlcalendar::setTimeZone' => 'Set the timezone used by this calendar', +'intlcalendar::toDateTime' => 'Convert an IntlCalendar into a DateTime object', +'intlchar::charAge' => 'Get the "age" of the code point', +'intlchar::charDigitValue' => 'Get the decimal digit value of a decimal digit character', +'intlchar::charDirection' => 'Get bidirectional category value for a code point', +'intlchar::charFromName' => 'Find Unicode character by name and return its code point value', +'intlchar::charMirror' => 'Get the "mirror-image" character for a code point', +'intlchar::charName' => 'Retrieve the name of a Unicode character', +'intlchar::charType' => 'Get the general category value for a code point', +'intlchar::chr' => 'Return Unicode character by code point value', +'intlchar::digit' => 'Get the decimal digit value of a code point for a given radix', +'intlchar::enumCharNames' => 'Enumerate all assigned Unicode characters within a range', +'intlchar::enumCharTypes' => 'Enumerate all code points with their Unicode general categories', +'intlchar::foldCase' => 'Perform case folding on a code point', +'intlchar::forDigit' => 'Get character representation for a given digit and radix', +'intlchar::getBidiPairedBracket' => 'Get the paired bracket character for a code point', +'intlchar::getBlockCode' => 'Get the Unicode allocation block containing a code point', +'intlchar::getCombiningClass' => 'Get the combining class of a code point', +'intlchar::getFC_NFKC_Closure' => 'Get the FC_NFKC_Closure property for a code point', +'intlchar::getIntPropertyMaxValue' => 'Get the max value for a Unicode property', +'intlchar::getIntPropertyMinValue' => 'Get the min value for a Unicode property', +'intlchar::getIntPropertyValue' => 'Get the value for a Unicode property for a code point', +'intlchar::getNumericValue' => 'Get the numeric value for a Unicode code point', +'intlchar::getPropertyEnum' => 'Get the property constant value for a given property name', +'intlchar::getPropertyName' => 'Get the Unicode name for a property', +'intlchar::getPropertyValueEnum' => 'Get the property value for a given value name', +'intlchar::getPropertyValueName' => 'Get the Unicode name for a property value', +'intlchar::getUnicodeVersion' => 'Get the Unicode version', +'intlchar::hasBinaryProperty' => 'Check a binary Unicode property for a code point', +'intlchar::isalnum' => 'Check if code point is an alphanumeric character', +'intlchar::isalpha' => 'Check if code point is a letter character', +'intlchar::isbase' => 'Check if code point is a base character', +'intlchar::isblank' => 'Check if code point is a "blank" or "horizontal space" character', +'intlchar::iscntrl' => 'Check if code point is a control character', +'intlchar::isdefined' => 'Check whether the code point is defined', +'intlchar::isdigit' => 'Check if code point is a digit character', +'intlchar::isgraph' => 'Check if code point is a graphic character', +'intlchar::isIDIgnorable' => 'Check if code point is an ignorable character', +'intlchar::isIDPart' => 'Check if code point is permissible in an identifier', +'intlchar::isIDStart' => 'Check if code point is permissible as the first character in an identifier', +'intlchar::isISOControl' => 'Check if code point is an ISO control code', +'intlchar::isJavaIDPart' => 'Check if code point is permissible in a Java identifier', +'intlchar::isJavaIDStart' => 'Check if code point is permissible as the first character in a Java identifier', +'intlchar::isJavaSpaceChar' => 'Check if code point is a space character according to Java', +'intlchar::islower' => 'Check if code point is a lowercase letter', +'intlchar::isMirrored' => 'Check if code point has the Bidi_Mirrored property', +'intlchar::isprint' => 'Check if code point is a printable character', +'intlchar::ispunct' => 'Check if code point is punctuation character', +'intlchar::isspace' => 'Check if code point is a space character', +'intlchar::istitle' => 'Check if code point is a titlecase letter', +'intlchar::isUAlphabetic' => 'Check if code point has the Alphabetic Unicode property', +'intlchar::isULowercase' => 'Check if code point has the Lowercase Unicode property', +'intlchar::isupper' => 'Check if code point has the general category "Lu" (uppercase letter)', +'intlchar::isUUppercase' => 'Check if code point has the Uppercase Unicode property', +'intlchar::isUWhiteSpace' => 'Check if code point has the White_Space Unicode property', +'intlchar::isWhitespace' => 'Check if code point is a whitespace character according to ICU', +'intlchar::isxdigit' => 'Check if code point is a hexadecimal digit', +'intlchar::ord' => 'Return Unicode code point value of character', +'intlchar::tolower' => 'Make Unicode character lowercase', +'intlchar::totitle' => 'Make Unicode character titlecase', +'intlchar::toupper' => 'Make Unicode character uppercase', +'IntlCodePointBreakIterator::createCharacterInstance' => 'Create break iterator for boundaries of combining character sequences', +'IntlCodePointBreakIterator::createCodePointInstance' => 'Create break iterator for boundaries of code points', +'IntlCodePointBreakIterator::createLineInstance' => 'Create break iterator for logically possible line breaks', +'IntlCodePointBreakIterator::createSentenceInstance' => 'Create break iterator for sentence breaks', +'IntlCodePointBreakIterator::createTitleInstance' => 'Create break iterator for title-casing breaks', +'IntlCodePointBreakIterator::createWordInstance' => 'Create break iterator for word breaks', +'IntlCodePointBreakIterator::current' => 'Get index of current position', +'IntlCodePointBreakIterator::first' => 'Set position to the first character in the text', +'IntlCodePointBreakIterator::following' => 'Advance the iterator to the first boundary following specified offset', +'IntlCodePointBreakIterator::getErrorCode' => 'Get last error code on the object', +'IntlCodePointBreakIterator::getErrorMessage' => 'Get last error message on the object', +'intlcodepointbreakiterator::getLastCodePoint' => 'Get last code point passed over after advancing or receding the iterator', +'IntlCodePointBreakIterator::getLocale' => 'Get the locale associated with the object', +'IntlCodePointBreakIterator::getPartsIterator' => 'Create iterator for navigating fragments between boundaries', +'IntlCodePointBreakIterator::getText' => 'Get the text being scanned', +'IntlCodePointBreakIterator::isBoundary' => 'Tell whether an offset is a boundaryʼs offset', +'IntlCodePointBreakIterator::last' => 'Set the iterator position to index beyond the last character', +'IntlCodePointBreakIterator::next' => 'Advance the iterator the next boundary', +'IntlCodePointBreakIterator::preceding' => 'Set the iterator position to the first boundary before an offset', +'IntlCodePointBreakIterator::previous' => 'Set the iterator position to the boundary immediately before the current', +'IntlCodePointBreakIterator::setText' => 'Set the text being scanned', +'IntlDateFormatter::create' => 'Create a date formatter', +'intldateformatter::format' => 'Format the date/time value as a string', +'intldateformatter::formatObject' => 'Formats an object', +'intldateformatter::getCalendar' => 'Get the calendar type used for the IntlDateFormatter', +'intldateformatter::getCalendarObject' => 'Get copy of formatterʼs calendar object', +'intldateformatter::getDateType' => 'Get the datetype used for the IntlDateFormatter', +'intldateformatter::getErrorCode' => 'Get the error code from last operation', +'intldateformatter::getErrorMessage' => 'Get the error text from the last operation', +'intldateformatter::getLocale' => 'Get the locale used by formatter', +'intldateformatter::getPattern' => 'Get the pattern used for the IntlDateFormatter', +'intldateformatter::getTimeType' => 'Get the timetype used for the IntlDateFormatter', +'intldateformatter::getTimeZone' => 'Get formatterʼs timezone', +'intldateformatter::getTimeZoneId' => 'Get the timezone-id used for the IntlDateFormatter', +'intldateformatter::isLenient' => 'Get the lenient used for the IntlDateFormatter', +'intldateformatter::localtime' => 'Parse string to a field-based time value', +'intldateformatter::parse' => 'Parse string to a timestamp value', +'intldateformatter::setCalendar' => 'Sets the calendar type used by the formatter', +'intldateformatter::setLenient' => 'Set the leniency of the parser', +'intldateformatter::setPattern' => 'Set the pattern used for the IntlDateFormatter', +'intldateformatter::setTimeZone' => 'Sets formatterʼs timezone', +'intldateformatter::setTimeZoneId' => 'Sets the time zone to use', +'IntlException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'IntlException::__toString' => 'String representation of the exception', +'IntlException::getCode' => 'Gets the Exception code', +'IntlException::getFile' => 'Gets the file in which the exception occurred', +'IntlException::getLine' => 'Gets the line in which the exception occurred', +'IntlException::getMessage' => 'Gets the Exception message', +'IntlException::getPrevious' => 'Returns previous Exception', +'IntlException::getTrace' => 'Gets the stack trace', +'IntlException::getTraceAsString' => 'Gets the stack trace as a string', +'intlgregoriancalendar::__construct' => 'Create the Gregorian Calendar class', +'IntlGregorianCalendar::add' => 'Add a (signed) amount of time to a field', +'IntlGregorianCalendar::after' => 'Whether this objectʼs time is after that of the passed object', +'IntlGregorianCalendar::before' => 'Whether this objectʼs time is before that of the passed object', +'IntlGregorianCalendar::clear' => 'Clear a field or all fields', +'IntlGregorianCalendar::createInstance' => 'Create a new IntlCalendar', +'IntlGregorianCalendar::equals' => 'Compare time of two IntlCalendar objects for equality', +'IntlGregorianCalendar::fieldDifference' => 'Calculate difference between given time and this objectʼs time', +'IntlGregorianCalendar::fromDateTime' => 'Create an IntlCalendar from a DateTime object or string', +'IntlGregorianCalendar::get' => 'Get the value for a field', +'IntlGregorianCalendar::getActualMaximum' => 'The maximum value for a field, considering the objectʼs current time', +'IntlGregorianCalendar::getActualMinimum' => 'The minimum value for a field, considering the objectʼs current time', +'IntlGregorianCalendar::getAvailableLocales' => 'Get array of locales for which there is data', +'IntlGregorianCalendar::getDayOfWeekType' => 'Tell whether a day is a weekday, weekend or a day that has a transition between the two', +'IntlGregorianCalendar::getErrorCode' => 'Get last error code on the object', +'IntlGregorianCalendar::getErrorMessage' => 'Get last error message on the object', +'IntlGregorianCalendar::getFirstDayOfWeek' => 'Get the first day of the week for the calendarʼs locale', +'IntlGregorianCalendar::getGreatestMinimum' => 'Get the largest local minimum value for a field', +'intlgregoriancalendar::getGregorianChange' => 'Get the Gregorian Calendar change date', +'IntlGregorianCalendar::getKeywordValuesForLocale' => 'Get set of locale keyword values', +'IntlGregorianCalendar::getLeastMaximum' => 'Get the smallest local maximum for a field', +'IntlGregorianCalendar::getLocale' => 'Get the locale associated with the object', +'IntlGregorianCalendar::getMaximum' => 'Get the global maximum value for a field', +'IntlGregorianCalendar::getMinimalDaysInFirstWeek' => 'Get minimal number of days the first week in a year or month can have', +'IntlGregorianCalendar::getMinimum' => 'Get the global minimum value for a field', +'IntlGregorianCalendar::getNow' => 'Get number representing the current time', +'IntlGregorianCalendar::getRepeatedWallTimeOption' => 'Get behavior for handling repeating wall time', +'IntlGregorianCalendar::getSkippedWallTimeOption' => 'Get behavior for handling skipped wall time', +'IntlGregorianCalendar::getTime' => 'Get time currently represented by the object', +'IntlGregorianCalendar::getTimeZone' => 'Get the objectʼs timezone', +'IntlGregorianCalendar::getType' => 'Get the calendar type', +'IntlGregorianCalendar::getWeekendTransition' => 'Get time of the day at which weekend begins or ends', +'IntlGregorianCalendar::inDaylightTime' => 'Whether the objectʼs time is in Daylight Savings Time', +'IntlGregorianCalendar::isEquivalentTo' => 'Whether another calendar is equal but for a different time', +'intlgregoriancalendar::isLeapYear' => 'Determine if the given year is a leap year', +'IntlGregorianCalendar::isLenient' => 'Whether date/time interpretation is in lenient mode', +'IntlGregorianCalendar::isSet' => 'Whether a field is set', +'IntlGregorianCalendar::isWeekend' => 'Whether a certain date/time is in the weekend', +'IntlGregorianCalendar::roll' => 'Add value to field without carrying into more significant fields', +'IntlGregorianCalendar::set' => 'Set a time field or several common fields at once', +'IntlGregorianCalendar::setFirstDayOfWeek' => 'Set the day on which the week is deemed to start', +'intlgregoriancalendar::setGregorianChange' => 'Set the Gregorian Calendar the change date', +'IntlGregorianCalendar::setLenient' => 'Set whether date/time interpretation is to be lenient', +'IntlGregorianCalendar::setMinimalDaysInFirstWeek' => 'Set minimal number of days the first week in a year or month can have', +'IntlGregorianCalendar::setRepeatedWallTimeOption' => 'Set behavior for handling repeating wall times at negative timezone offset transitions', +'IntlGregorianCalendar::setSkippedWallTimeOption' => 'Set behavior for handling skipped wall times at positive timezone offset transitions', +'IntlGregorianCalendar::setTime' => 'Set the calendar time in milliseconds since the epoch', +'IntlGregorianCalendar::setTimeZone' => 'Set the timezone used by this calendar', +'IntlGregorianCalendar::toDateTime' => 'Convert an IntlCalendar into a DateTime object', +'intliterator::current' => 'Get the current element', +'intliterator::key' => 'Get the current key', +'intliterator::next' => 'Move forward to the next element', +'intliterator::rewind' => 'Rewind the iterator to the first element', +'intliterator::valid' => 'Check if current position is valid', +'IntlPartsIterator::current' => 'Get the current element', +'intlpartsiterator::getBreakIterator' => 'Get IntlBreakIterator backing this parts iterator', +'IntlPartsIterator::key' => 'Get the current key', +'IntlPartsIterator::next' => 'Move forward to the next element', +'IntlPartsIterator::rewind' => 'Rewind the iterator to the first element', +'IntlPartsIterator::valid' => 'Check if current position is valid', +'intlrulebasedbreakiterator::__construct' => 'Create iterator from ruleset', +'IntlRuleBasedBreakIterator::createCharacterInstance' => 'Create break iterator for boundaries of combining character sequences', +'IntlRuleBasedBreakIterator::createCodePointInstance' => 'Create break iterator for boundaries of code points', +'IntlRuleBasedBreakIterator::createLineInstance' => 'Create break iterator for logically possible line breaks', +'IntlRuleBasedBreakIterator::createSentenceInstance' => 'Create break iterator for sentence breaks', +'IntlRuleBasedBreakIterator::createTitleInstance' => 'Create break iterator for title-casing breaks', +'IntlRuleBasedBreakIterator::createWordInstance' => 'Create break iterator for word breaks', +'IntlRuleBasedBreakIterator::current' => 'Get index of current position', +'IntlRuleBasedBreakIterator::first' => 'Set position to the first character in the text', +'IntlRuleBasedBreakIterator::following' => 'Advance the iterator to the first boundary following specified offset', +'intlrulebasedbreakiterator::getBinaryRules' => 'Get the binary form of compiled rules', +'IntlRuleBasedBreakIterator::getErrorCode' => 'Get last error code on the object', +'IntlRuleBasedBreakIterator::getErrorMessage' => 'Get last error message on the object', +'IntlRuleBasedBreakIterator::getLocale' => 'Get the locale associated with the object', +'IntlRuleBasedBreakIterator::getPartsIterator' => 'Create iterator for navigating fragments between boundaries', +'intlrulebasedbreakiterator::getRules' => 'Get the rule set used to create this object', +'intlrulebasedbreakiterator::getRuleStatus' => 'Get the largest status value from the break rules that determined the current break position', +'intlrulebasedbreakiterator::getRuleStatusVec' => 'Get the status values from the break rules that determined the current break position', +'IntlRuleBasedBreakIterator::getText' => 'Get the text being scanned', +'IntlRuleBasedBreakIterator::isBoundary' => 'Tell whether an offset is a boundaryʼs offset', +'IntlRuleBasedBreakIterator::last' => 'Set the iterator position to index beyond the last character', +'IntlRuleBasedBreakIterator::next' => 'Advance the iterator the next boundary', +'IntlRuleBasedBreakIterator::preceding' => 'Set the iterator position to the first boundary before an offset', +'IntlRuleBasedBreakIterator::previous' => 'Set the iterator position to the boundary immediately before the current', +'IntlRuleBasedBreakIterator::setText' => 'Set the text being scanned', +'intltimezone::countEquivalentIDs' => 'Get the number of IDs in the equivalency group that includes the given ID', +'intltimezone::createDefault' => 'Create a new copy of the default timezone for this host', +'intltimezone::createEnumeration' => 'Get an enumeration over time zone IDs associated with the given country or offset', +'intltimezone::createTimeZone' => 'Create a timezone object for the given ID', +'intltimezone::createTimeZoneIDEnumeration' => 'Get an enumeration over system time zone IDs with the given filter conditions', +'intltimezone::fromDateTimeZone' => 'Create a timezone object from DateTimeZone', +'intltimezone::getCanonicalID' => 'Get the canonical system timezone ID or the normalized custom time zone ID for the given time zone ID', +'intltimezone::getDisplayName' => 'Get a name of this time zone suitable for presentation to the user', +'intltimezone::getDSTSavings' => 'Get the amount of time to be added to local standard time to get local wall clock time', +'intltimezone::getEquivalentID' => 'Get an ID in the equivalency group that includes the given ID', +'intltimezone::getErrorCode' => 'Get last error code on the object', +'intltimezone::getErrorMessage' => 'Get last error message on the object', +'intltimezone::getGMT' => 'Create GMT (UTC) timezone', +'intltimezone::getID' => 'Get timezone ID', +'intltimezone::getIDForWindowsID' => 'Translate a Windows timezone into a system timezone', +'intltimezone::getOffset' => 'Get the time zone raw and GMT offset for the given moment in time', +'intltimezone::getRawOffset' => 'Get the raw GMT offset (before taking daylight savings time into account', +'intltimezone::getRegion' => 'Get the region code associated with the given system time zone ID', +'intltimezone::getTZDataVersion' => 'Get the timezone data version currently used by ICU', +'intltimezone::getUnknown' => 'Get the "unknown" time zone', +'intltimezone::getWindowsID' => 'Translate a system timezone into a Windows timezone', +'intltimezone::hasSameRules' => 'Check if this zone has the same rules and offset as another zone', +'intltimezone::toDateTimeZone' => 'Convert to DateTimeZone object', +'intltimezone::useDaylightTime' => 'Check if this time zone uses daylight savings time', +'intltz_getGMT' => '(PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
+Create GMT (UTC) timezone', +'intval' => 'Get the integer value of a variable', +'InvalidArgumentException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'InvalidArgumentException::__toString' => 'String representation of the exception', +'InvalidArgumentException::getCode' => 'Gets the Exception code', +'InvalidArgumentException::getFile' => 'Gets the file in which the exception occurred', +'InvalidArgumentException::getLine' => 'Gets the line in which the exception occurred', +'InvalidArgumentException::getMessage' => 'Gets the Exception message', +'InvalidArgumentException::getPrevious' => 'Returns previous Exception', +'InvalidArgumentException::getTrace' => 'Gets the stack trace', +'InvalidArgumentException::getTraceAsString' => 'Gets the stack trace as a string', +'ip2long' => 'Converts a string containing an (IPv4) Internet Protocol dotted address into a long integer', +'iptcembed' => 'Embeds binary IPTC data into a JPEG image', +'iptcparse' => 'Parse a binary IPTC block into single tags', +'is_a' => 'Checks if the object is of this class or has this class as one of its parents', +'is_array' => 'Finds whether a variable is an array', +'is_bool' => 'Finds out whether a variable is a boolean', +'is_callable' => 'Verify that the contents of a variable can be called as a function', +'is_countable' => 'Verify that the contents of a variable is a countable value', +'is_dir' => 'Tells whether the filename is a directory', +'is_double' => 'Alias of is_float', +'is_executable' => 'Tells whether the filename is executable', +'is_file' => 'Tells whether the filename is a regular file', +'is_finite' => 'Finds whether a value is a legal finite number', +'is_float' => 'Finds whether the type of a variable is float', +'is_infinite' => 'Finds whether a value is infinite', +'is_int' => 'Find whether the type of a variable is integer', +'is_integer' => 'Alias of is_int', +'is_iterable' => 'Verify that the contents of a variable is an iterable value', +'is_link' => 'Tells whether the filename is a symbolic link', +'is_long' => 'Alias of is_int', +'is_nan' => 'Finds whether a value is not a number', +'is_null' => 'Finds whether a variable is `null`', +'is_numeric' => 'Finds whether a variable is a number or a numeric string', +'is_object' => 'Finds whether a variable is an object', +'is_readable' => 'Tells whether a file exists and is readable', +'is_real' => 'Alias of is_float', +'is_resource' => 'Finds whether a variable is a resource', +'is_scalar' => 'Finds whether a variable is a scalar', +'is_soap_fault' => 'Checks if a SOAP call has failed', +'is_string' => 'Find whether the type of a variable is string', +'is_subclass_of' => 'Checks if the object has this class as one of its parents or implements it', +'is_tainted' => 'Checks whether a string is tainted', +'is_uploaded_file' => 'Tells whether the file was uploaded via HTTP POST', +'is_writable' => 'Tells whether the filename is writable', +'is_writeable' => 'Alias of is_writable', +'isset' => 'Determine if a variable is set and is not `null`', +'Iterator::current' => 'Return the current element', +'Iterator::key' => 'Return the key of the current element', +'Iterator::next' => 'Move forward to next element', +'Iterator::rewind' => 'Rewind the Iterator to the first element', +'Iterator::valid' => 'Checks if current position is valid', +'iterator_apply' => 'Call a function for every element in an iterator', +'iterator_count' => 'Count the elements in an iterator', +'iterator_to_array' => 'Copy the iterator into an array', +'IteratorAggregate::getIterator' => 'Retrieve an external iterator', +'iteratoriterator::__construct' => 'Create an iterator from anything that is traversable', +'iteratoriterator::current' => 'Get the current value', +'iteratoriterator::getInnerIterator' => 'Get the inner iterator', +'iteratoriterator::key' => 'Get the key of the current element', +'iteratoriterator::next' => 'Forward to the next element', +'iteratoriterator::rewind' => 'Rewind to the first element', +'iteratoriterator::valid' => 'Checks if the iterator is valid', +'java' => 'Create Java object', +'java::java' => 'Create Java object', +'JavaException::getCause' => 'Get Java exception that led to this exception', +'jddayofweek' => 'Returns the day of the week', +'jdmonthname' => 'Returns a month name', +'jdtofrench' => 'Converts a Julian Day Count to the French Republican Calendar', +'jdtogregorian' => 'Converts Julian Day Count to Gregorian date', +'jdtojewish' => 'Converts a Julian day count to a Jewish calendar date', +'jdtojulian' => 'Converts a Julian Day Count to a Julian Calendar Date', +'jdtounix' => 'Convert Julian Day to Unix timestamp', +'jewishtojd' => 'Converts a date in the Jewish Calendar to Julian Day Count', +'join' => 'Alias of implode', +'jpeg2wbmp' => 'Convert JPEG image file to WBMP image file', +'json_decode' => 'Decodes a JSON string', +'json_encode' => 'Returns the JSON representation of a value', +'json_last_error' => 'Returns the last error occurred', +'json_last_error_msg' => 'Returns the error string of the last json_encode() or json_decode() call', +'JsonException::__clone' => 'Clone the exception +Tries to clone the Exception, which results in Fatal error.', +'JsonException::__toString' => 'String representation of the exception', +'JsonException::getCode' => 'Gets the Exception code', +'JsonException::getFile' => 'Gets the file in which the exception occurred', +'JsonException::getLine' => 'Gets the line in which the exception occurred', +'JsonException::getMessage' => 'Gets the Exception message', +'JsonException::getPrevious' => 'Returns previous Exception', +'JsonException::getTrace' => 'Gets the stack trace', +'JsonException::getTraceAsString' => 'Gets the stack trace as a string', +'jsonserializable::jsonSerialize' => 'Specify data which should be serialized to JSON', +'judy::__construct' => 'Construct a new Judy object', +'judy::__destruct' => 'Destruct a Judy object', +'judy::byCount' => 'Locate the Nth index present in the Judy array', +'judy::count' => 'Count the number of elements in the Judy array', +'judy::first' => 'Search for the first index in the Judy array', +'judy::firstEmpty' => 'Search for the first absent index in the Judy array', +'judy::free' => 'Free the entire Judy array', +'judy::getType' => 'Return the type of the current Judy array', +'judy::last' => 'Search for the last index in the Judy array', +'judy::lastEmpty' => 'Search for the last absent index in the Judy array', +'judy::memoryUsage' => 'Return the memory used by the Judy array', +'judy::next' => 'Search for the next index in the Judy array', +'judy::nextEmpty' => 'Search for the next absent index in the Judy array', +'judy::offsetExists' => 'Whether a offset exists', +'judy::offsetGet' => 'Offset to retrieve', +'judy::offsetSet' => 'Offset to set', +'judy::offsetUnset' => 'Offset to unset', +'judy::prev' => 'Search for the previous index in the Judy array', +'judy::prevEmpty' => 'Search for the previous absent index in the Judy array', +'judy::size' => 'Return the size of the current Judy array', +'judy_type' => 'Return the type of a Judy array', +'judy_version' => 'Return or print the current PHP Judy version', +'juliantojd' => 'Converts a Julian Calendar date to Julian Day Count', +'kadm5_chpass_principal' => 'Changes the principal\'s password', +'kadm5_create_principal' => 'Creates a kerberos principal with the given parameters', +'kadm5_delete_principal' => 'Deletes a kerberos principal', +'kadm5_destroy' => 'Closes the connection to the admin server and releases all related resources', +'kadm5_flush' => 'Flush all changes to the Kerberos database', +'kadm5_get_policies' => 'Gets all policies from the Kerberos database', +'kadm5_get_principal' => 'Gets the principal\'s entries from the Kerberos database', +'kadm5_get_principals' => 'Gets all principals from the Kerberos database', +'kadm5_init_with_password' => 'Opens a connection to the KADM5 library', +'kadm5_modify_principal' => 'Modifies a kerberos principal with the given parameters', +'key' => 'Fetch a key from an array', +'key_exists' => 'Alias of array_key_exists', +'krsort' => 'Sort an array by key in reverse order', +'ksort' => 'Sort an array by key', +'ktaglib_id3v2_frame::getDescription' => 'Returns a description for the picture in a picture frame', +'ktaglib_id3v2_frame::getMimeType' => 'Returns the mime type of the picture', +'ktaglib_id3v2_frame::getType' => 'Returns the type of the image', +'ktaglib_id3v2_frame::savePicture' => 'Saves the picture to a file', +'ktaglib_id3v2_frame::setMimeType' => 'Set\'s the mime type of the picture', +'ktaglib_id3v2_frame::setPicture' => 'Sets the frame picture to the given image', +'ktaglib_id3v2_frame::setType' => 'Set the type of the image', +'ktaglib_mpeg_audioproperties::getBitrate' => 'Returns the bitrate of the MPEG file', +'ktaglib_mpeg_audioproperties::getChannels' => 'Returns the amount of channels of a MPEG file', +'ktaglib_mpeg_audioproperties::getLayer' => 'Returns the layer of a MPEG file', +'ktaglib_mpeg_audioproperties::getLength' => 'Returns the length of a MPEG file', +'ktaglib_mpeg_audioproperties::getSampleBitrate' => 'Returns the sample bitrate of a MPEG file', +'ktaglib_mpeg_audioproperties::getVersion' => 'Returns the version of a MPEG file', +'ktaglib_mpeg_audioproperties::isCopyrighted' => 'Returns the copyright status of an MPEG file', +'ktaglib_mpeg_audioproperties::isOriginal' => 'Returns if the file is marked as the original file', +'ktaglib_mpeg_audioproperties::isProtectionEnabled' => 'Returns if protection mechanisms of an MPEG file are enabled', +'ktaglib_mpeg_file::__construct' => 'Opens a new file', +'ktaglib_mpeg_file::getAudioProperties' => 'Returns an object that provides access to the audio properties', +'ktaglib_mpeg_file::getID3v1Tag' => 'Returns an object representing an ID3v1 tag', +'ktaglib_mpeg_file::getID3v2Tag' => 'Returns a ID3v2 object', +'labelcacheObj::freeCache' => 'Free the label cache. Always returns MS_SUCCESS. +Ex : map->labelcache->freeCache();', +'labelObj::convertToString' => 'Saves the object to a string. Provides the inverse option for +updateFromString.', +'labelObj::deleteStyle' => 'Delete the style specified by the style index. If there are any +style that follow the deleted style, their index will decrease by 1.', +'labelObj::free' => 'Free the object properties and break the internal references. +Note that you have to unset the php variable to free totally the +resources.', +'labelObj::getBinding' => 'Get the attribute binding for a specified label property. Returns +NULL if there is no binding for this property. +Example: +.. code-block:: php +$oLabel->setbinding(MS_LABEL_BINDING_COLOR, "FIELD_NAME_COLOR"); +echo $oLabel->getbinding(MS_LABEL_BINDING_COLOR); // FIELD_NAME_COLOR', +'labelObj::getExpressionString' => 'Returns the label expression string.', +'labelObj::getStyle' => 'Return the style object using an index. index >= 0 && +index < label->numstyles.', +'labelObj::getTextString' => 'Returns the label text string.', +'labelObj::moveStyleDown' => 'The style specified by the style index will be moved down into +the array of classes. Returns MS_SUCCESS or MS_FAILURE. +ex label->movestyledown(0) will have the effect of moving style 0 +up to position 1, and the style at position 1 will be moved +to position 0.', +'labelObj::moveStyleUp' => 'The style specified by the style index will be moved up into +the array of classes. Returns MS_SUCCESS or MS_FAILURE. +ex label->movestyleup(1) will have the effect of moving style 1 +up to position 0, and the style at position 0 will be moved +to position 1.', +'labelObj::removeBinding' => 'Remove the attribute binding for a specfiled style property. +Example: +.. code-block:: php +$oStyle->removebinding(MS_LABEL_BINDING_COLOR);', +'labelObj::set' => 'Set object property to a new value.', +'labelObj::setBinding' => 'Set the attribute binding for a specified label property. +Example: +.. code-block:: php +$oLabel->setbinding(MS_LABEL_BINDING_COLOR, "FIELD_NAME_COLOR"); +This would bind the color parameter with the data (ie will extract +the value of the color from the field called "FIELD_NAME_COLOR"', +'labelObj::setExpression' => 'Set the label expression.', +'labelObj::setText' => 'Set the label text.', +'labelObj::updateFromString' => 'Update a label from a string snippet. Returns MS_SUCCESS/MS_FAILURE.', +'lapack::eigenValues' => 'This function returns the eigenvalues for a given square matrix', +'lapack::identity' => 'Return an identity matrix', +'lapack::leastSquaresByFactorisation' => 'Calculate the linear least squares solution of a matrix using QR factorisation', +'lapack::leastSquaresBySVD' => 'Solve the linear least squares problem, using SVD', +'lapack::pseudoInverse' => 'Calculate the inverse of a matrix', +'lapack::singularValues' => 'Calculated the singular values of a matrix', +'lapack::solveLinearEquation' => 'Solve a system of linear equations', +'layerObj::addFeature' => 'Add a new feature in a layer. Returns MS_SUCCESS or MS_FAILURE on +error.', +'layerObj::applySLD' => 'Apply the :ref:`SLD ` document to the layer object. +The matching between the sld document and the layer will be done +using the layer\'s name. +If a namedlayer argument is passed (argument is optional), +the NamedLayer in the sld that matches it will be used to style +the layer. +See :ref:`SLD HowTo ` for more information on the SLD support.', +'layerObj::applySLDURL' => 'Apply the :ref:`SLD ` document pointed by the URL to the +layer object. The matching between the sld document and the layer +will be done using the layer\'s name. If a namedlayer argument is +passed (argument is optional), the NamedLayer in the sld that +matches it will be used to style the layer. See :ref:`SLD HowTo +` for more information on the SLD support.', +'layerObj::clearProcessing' => 'Clears all the processing strings.', +'layerObj::close' => 'Close layer previously opened with open().', +'layerObj::convertToString' => 'Saves the object to a string. Provides the inverse option for +updateFromString.', +'layerObj::draw' => 'Draw a single layer, add labels to cache if required. +Returns MS_SUCCESS or MS_FAILURE on error.', +'layerObj::drawQuery' => 'Draw query map for a single layer. +string executeWFSGetfeature() +Executes a GetFeature request on a WFS layer and returns the +name of the temporary GML file created. Returns an empty +string on error.', +'layerObj::free' => 'Free the object properties and break the internal references. +Note that you have to unset the php variable to free totally the +resources.', +'layerObj::generateSLD' => 'Returns an SLD XML string based on all the classes found in the +layer (the layer must have `STATUS` `on`).', +'layerObj::getClass' => 'Returns a classObj from the layer given an index value (0=first class)', +'layerObj::getClassIndex' => 'Get the class index of a shape for a given scale. Returns -1 if no +class matches. classgroup is an array of class ids to check +(Optional). numclasses is the number of classes that the classgroup +array contains. By default, all the layer classes will be checked.', +'layerObj::getExtent' => 'Returns the layer\'s data extents or NULL on error. +If the layer\'s EXTENT member is set then this value is used, +otherwise this call opens/closes the layer to read the +extents. This is quick on shapefiles, but can be +an expensive operation on some file formats or data sources. +This function is safe to use on both opened or closed layers: it +is not necessary to call open()/close() before/after calling it.', +'layerObj::getFilterString' => 'Returns the :ref:`expression ` for this layer or NULL +on error.', +'layerObj::getGridIntersectionCoordinates' => 'Returns an array containing the grid intersection coordinates. If +there are no coordinates, it returns an empty array.', +'layerObj::getItems' => 'Returns an array containing the items. Must call open function first. +If there are no items, it returns an empty array.', +'layerObj::getMetaData' => 'Fetch layer metadata entry by name. Returns "" if no entry +matches the name. Note that the search is case sensitive. +.. note:: +getMetaData\'s query is case sensitive.', +'layerObj::getNumResults' => 'Returns the number of results in the last query.', +'layerObj::getProcessing' => 'Returns an array containing the processing strings. +If there are no processing strings, it returns an empty array.', +'layerObj::getProjection' => 'Returns a string representation of the :ref:`projection `. +Returns NULL on error or if no projection is set.', +'layerObj::getResult' => 'Returns a resultObj by index from a layer object with +index in the range 0 to numresults-1. +Returns a valid object or FALSE(0) if index is invalid.', +'layerObj::getResultsBounds' => 'Returns the bounding box of the latest result.', +'layerObj::getShape' => 'If the resultObj passed has a valid resultindex, retrieve shapeObj from +a layer\'s resultset. (You get it from the resultObj returned by +getResult() for instance). Otherwise, it will do a single query on +the layer to fetch the shapeindex +.. code-block:: php +$map = new mapObj("gmap75.map"); +$l = $map->getLayerByName("popplace"); +$l->queryByRect($map->extent); +for ($i=0; $i<$l->getNumResults();$i++){ +$s = $l->getShape($l->getResult($i)); +echo $s->getValue($l,"Name"); +echo "\n"; +}', +'layerObj::getWMSFeatureInfoURL' => 'Returns a WMS GetFeatureInfo URL (works only for WMS layers) +clickX, clickY is the location of to query in pixel coordinates +with (0,0) at the top left of the image. +featureCount is the number of results to return. +infoFormat is the format the format in which the result should be +requested. Depends on remote server\'s capabilities. MapServer +WMS servers support only "MIME" (and should support "GML.1" soon). +Returns "" and outputs a warning if layer is not a WMS layer +or if it is not queryable.', +'layerObj::isVisible' => 'Returns MS_TRUE/MS_FALSE depending on whether the layer is +currently visible in the map (i.e. turned on, in scale, etc.).', +'layerObj::moveclassdown' => 'The class specified by the class index will be moved down into +the array of layers. Returns MS_SUCCESS or MS_FAILURE. +ex layer->moveclassdown(0) will have the effect of moving class 0 +up to position 1, and the class at position 1 will be moved +to position 0.', +'layerObj::moveclassup' => 'The class specified by the class index will be moved up into +the array of layers. Returns MS_SUCCESS or MS_FAILURE. +ex layer->moveclassup(1) will have the effect of moving class 1 +up to position 0, and the class at position 0 will be moved +to position 1.', +'layerObj::ms_newLayerObj' => 'Old style constructor', +'layerObj::nextShape' => 'Called after msWhichShapes has been called to actually retrieve +shapes within a given area. Returns a shape object or NULL on +error. +.. code-block:: php +$map = ms_newmapobj("d:/msapps/gmap-ms40/htdocs/gmap75.map"); +$layer = $map->getLayerByName(\'road\'); +$status = $layer->open(); +$status = $layer->whichShapes($map->extent); +while ($shape = $layer->nextShape()) +{ +echo $shape->index ."
\n"; +} +$layer->close();', +'layerObj::open' => 'Open the layer for use with getShape(). +Returns MS_SUCCESS/MS_FAILURE.', +'layerObj::queryByAttributes' => 'Query layer for shapes that intersect current map extents. qitem +is the item (attribute) on which the query is performed, and +qstring is the expression to match. The query is performed on all +the shapes that are part of a :ref:`CLASS` that contains a +:ref:`TEMPLATE