From af14d50e2b5d1b2cf91813059cdc827be63fdb20 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 5 Jan 2024 15:25:52 +0100 Subject: [PATCH] Fix falsy isset for all expressions --- src/Analyser/TypeSpecifier.php | 26 ++-- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/bug-10373.php | 142 ++++++++++++++++++ 3 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-10373.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 2f3e1fdacf..4e7b61cc25 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -638,18 +638,18 @@ public function specifyTypesInCondition( return new SpecifiedTypes(); } - if ($issetExpr instanceof Expr\Variable && is_string($issetExpr->name)) { - $type = $scope->getType($issetExpr); - $isNullable = !$type->isNull()->no(); + $type = $scope->getType($issetExpr); + $isNullable = !$type->isNull()->no(); + $exprType = $this->create( + $issetExpr, + new NullType(), + $context->negate(), + false, + $scope, + $rootExpr, + ); - $exprType = $this->create( - $issetExpr, - new NullType(), - $context->negate(), - false, - $scope, - $rootExpr, - ); + if ($issetExpr instanceof Expr\Variable && is_string($issetExpr->name)) { if ($isset === true) { if ($isNullable) { return $exprType; @@ -689,6 +689,10 @@ public function specifyTypesInCondition( ); } + if ($isNullable && $isset === true) { + return $exprType; + } + return new SpecifiedTypes(); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 6fca61789d..6a7b6d32ea 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1314,6 +1314,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/list-shapes.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3013.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7607.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10373.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/ibm_db2.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/benevolent-union-math.php'); diff --git a/tests/PHPStan/Analyser/data/bug-10373.php b/tests/PHPStan/Analyser/data/bug-10373.php new file mode 100644 index 0000000000..c7fa732dd2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10373.php @@ -0,0 +1,142 @@ + $options Array of options. Possible keys are: + * + * - `cache` - Can either be `true`, to enable caching using the config in View::$elementCache. Or an array + * If an array, the following keys can be used: + * + * - `config` - Used to store the cached element in a custom cache configuration. + * - `key` - Used to define the key used in the Cache::write(). It will be prefixed with `element_` + * + * - `callbacks` - Set to true to fire beforeRender and afterRender helper callbacks for this element. + * Defaults to false. + * - `ignoreMissing` - Used to allow missing elements. Set to true to not throw exceptions. + * - `plugin` - setting to false will force to use the application's element from plugin templates, when the + * plugin has element with same name. Defaults to true + * @return string Rendered Element + * @psalm-param array{cache?:array|true, callbacks?:bool, plugin?:string|false, ignoreMissing?:bool} $options + */ + public function element(string $name, array $data = [], array $options = []): string + { + assertType('array|true', $options['cache']); + $options += ['callbacks' => false, 'cache' => null, 'plugin' => null, 'ignoreMissing' => false]; + assertType('array|true|null', $options['cache']); + if (isset($options['cache'])) { + $options['cache'] = $this->_elementCache( + $name, + $data, + array_diff_key($options, ['callbacks' => false, 'plugin' => null, 'ignoreMissing' => null]) + ); + assertType('array{key: string, config: string}', $options['cache']); + } else { + assertType('null', $options['cache']); + } + assertType('array{key: string, config: string}|null', $options['cache']); + + $pluginCheck = $options['plugin'] !== false; + $file = $this->_getElementFileName($name, $pluginCheck); + if ($file && $options['cache']) { + assertType('array{key: string, config: string}', $options['cache']); + return $this->cache(function (): void { + echo ''; + }, $options['cache']); + } + + return $file; + } + + /** + * @param string $name + * @param bool $pluginCheck + */ + protected function _getElementFileName(string $name, bool $pluginCheck): string + { + return $name; + } + + /** + * @param callable $block The block of code that you want to cache the output of. + * @param array $options The options defining the cache key etc. + * @return string The rendered content. + * @throws \InvalidArgumentException When $options is lacking a 'key' option. + */ + public function cache(callable $block, array $options = []): string + { + $options += ['key' => '', 'config' => []]; + if (empty($options['key'])) { + throw new \InvalidArgumentException('Cannot cache content with an empty key'); + } + /** @var string $result */ + $result = $options['key']; + if ($result) { + return $result; + } + + $bufferLevel = ob_get_level(); + ob_start(); + + try { + $block(); + } catch (\Throwable $exception) { + while (ob_get_level() > $bufferLevel) { + ob_end_clean(); + } + + throw $exception; + } + + $result = (string)ob_get_clean(); + + return $result; + } + + /** + * Generate the cache configuration options for an element. + * + * @param string $name Element name + * @param array $data Data + * @param array $options Element options + * @return array Element Cache configuration. + * @psalm-return array{key:string, config:string} + */ + protected function _elementCache(string $name, array $data, array $options): array + { + [$plugin, $name] = explode(':', $name, 2); + + $pluginKey = null; + if ($plugin) { + $pluginKey = str_replace('/', '_', $plugin); + } + $elementKey = str_replace(['\\', '/'], '_', $name); + + $cache = $options['cache']; + unset($options['cache']); + $keys = array_merge( + [$pluginKey, $elementKey], + array_keys($options), + array_keys($data) + ); + $config = [ + 'config' => [], + 'key' => implode('_', array_keys($keys)), + ]; + if (is_array($cache)) { + $config = $cache + $config; + } + $config['key'] = 'element_' . $config['key']; + + /** @var array{config: string, key: string} */ + return $config; + } +}