diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index c0bdcb9480..b1bdee7855 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,10 +6,10 @@ on: pull_request: push: branches: - - "master" + - "**" env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: backward-compatibility: diff --git a/.github/workflows/compiler-tests.yml b/.github/workflows/compiler-tests.yml index 65e1c2e58d..3333d665fe 100644 --- a/.github/workflows/compiler-tests.yml +++ b/.github/workflows/compiler-tests.yml @@ -6,10 +6,10 @@ on: pull_request: push: branches: - - "master" + - "**" env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: compiler-tests: @@ -48,20 +48,20 @@ jobs: integration-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/integration-tests.yml@master + uses: phpstan/phpstan/.github/workflows/integration-tests.yml@1.5.x with: - ref: master + ref: 1.5.x extension-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/extension-tests.yml@master + uses: phpstan/phpstan/.github/workflows/extension-tests.yml@1.5.x with: - ref: master + ref: 1.5.x other-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/other-tests.yml@master + uses: phpstan/phpstan/.github/workflows/other-tests.yml@1.5.x with: - ref: master + ref: 1.5.x diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 136ef4969e..2c7ff7add9 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -8,12 +8,12 @@ on: - 'compiler/**' push: branches: - - "master" + - "**" paths-ignore: - 'compiler/**' env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: result-cache-e2e-tests: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6c7afe5f5a..0d64be5317 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,10 +6,10 @@ on: pull_request: push: branches: - - "master" + - "**" env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: lint: diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index 678701860a..6eb406ffd4 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -5,7 +5,7 @@ name: "Compile PHAR" on: push: branches: - - "master" + - "1.5.x" tags: - '1.*' @@ -79,13 +79,13 @@ jobs: git config user.email "ondrej@mirtes.cz" && \ git config user.name "Ondrej Mirtes" - - name: "Commit PHAR - master" + - name: "Commit PHAR - development" working-directory: phpstan-dist if: "!startsWith(github.ref, 'refs/tags/')" run: | git add phpstan.phar phpstan.phar.asc && \ git commit -S -m "Updated PHPStan to commit ${{ github.event.after }}" -m "${{ steps.git-log.outputs.log }}" && \ - git push --quiet origin master + git push --quiet origin 1.5.x - name: "Commit PHAR - tag" working-directory: phpstan-dist @@ -93,6 +93,6 @@ jobs: run: | git add phpstan.phar phpstan.phar.asc && \ git commit -S -m "PHPStan ${GITHUB_REF#refs/tags/}" -m "${{ steps.git-log.outputs.log }}" && \ - git push --quiet origin master && \ + git push --quiet origin 1.5.x && \ git tag -s ${GITHUB_REF#refs/tags/} -m "${GITHUB_REF#refs/tags/}" && \ git push --quiet origin ${GITHUB_REF#refs/tags/} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 543254e867..7a2e880070 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -8,12 +8,12 @@ on: - 'compiler/**' push: branches: - - "master" + - "**" paths-ignore: - 'compiler/**' env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: static-analysis: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f9f0e8b07f..cfdbc37dfe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,12 +8,12 @@ on: - 'compiler/**' push: branches: - - "master" + - "**" paths-ignore: - 'compiler/**' env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: tests: diff --git a/composer.json b/composer.json index f9943dc0ee..7c820554aa 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nikic/php-parser": "^4.13.2", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "5.0.7.2", - "phpstan/php-8-stubs": "0.1.48", + "phpstan/php-8-stubs": "0.1.49", "phpstan/phpdoc-parser": "^1.2.0", "react/child-process": "^0.6.4", "react/event-loop": "^1.2", @@ -73,9 +73,6 @@ } }, "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - }, "patcher": { "search": "patches" } diff --git a/composer.lock b/composer.lock index 3497da7b42..d7d12c9729 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ede0704e050d946882afd2613bdbb349", + "content-hash": "73507c3702c02cea0fd38e149543faee", "packages": [ { "name": "clue/block-react", @@ -2248,16 +2248,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.1.48", + "version": "0.1.49", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "6fc5082cf5a2d629c54543c4067d3fe6e68c6f8e" + "reference": "095a11fc8ba747bd200fc91701ef1c824818bf49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/6fc5082cf5a2d629c54543c4067d3fe6e68c6f8e", - "reference": "6fc5082cf5a2d629c54543c4067d3fe6e68c6f8e", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/095a11fc8ba747bd200fc91701ef1c824818bf49", + "reference": "095a11fc8ba747bd200fc91701ef1c824818bf49", "shasum": "" }, "type": "library", @@ -2274,9 +2274,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.1.48" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.1.49" }, - "time": "2022-03-10T00:11:46+00:00" + "time": "2022-03-11T00:14:16+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -2907,12 +2907,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2962,12 +2962,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\Stream\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "React\\Promise\\Stream\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3045,12 +3045,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\Timer\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "React\\Promise\\Timer\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3301,12 +3301,12 @@ } }, "autoload": { - "psr-4": { - "RingCentral\\Psr7\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "RingCentral\\Psr7\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3594,12 +3594,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3673,12 +3673,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3754,12 +3754,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -3841,12 +3841,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3918,12 +3918,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3994,12 +3994,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4073,12 +4073,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php74\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Php74\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4153,12 +4153,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4386,12 +4386,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -4670,12 +4670,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, "files": [ "src/DeepCopy/deep_copy.php" - ] + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5799,11 +5799,11 @@ } }, "autoload": { - "classmap": [ - "src/" - ], "files": [ "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", diff --git a/conf/config.neon b/conf/config.neon index 0b86120409..d404e76236 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1178,6 +1178,11 @@ services: tags: - phpstan.dynamicStaticMethodThrowTypeExtension + - + class: PHPStan\Type\Php\StrContainingTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension tags: diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 7bdd515031..ffe61015bc 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2660,7 +2660,14 @@ private function resolveType(Expr $node): Type continue; } - return $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall($functionReflection, $node, $this); + $resolvedType = $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall( + $functionReflection, + $node, + $this, + ); + if ($resolvedType !== null) { + return $resolvedType; + } } return ParametersAcceptorSelector::selectFromArgs( @@ -5659,7 +5666,16 @@ private function exactInstantiation(New_ $node, string $className): ?Type continue; } - $resolvedTypes[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($constructorMethod, $methodCall, $this); + $resolvedType = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall( + $constructorMethod, + $methodCall, + $this, + ); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } if (count($resolvedTypes) > 0) { @@ -5835,7 +5851,12 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, continue; } - $resolvedTypes[] = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $methodCall, $this); + $resolvedType = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $methodCall, $this); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } } else { foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($className) as $dynamicStaticMethodReturnTypeExtension) { @@ -5843,7 +5864,16 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, continue; } - $resolvedTypes[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($methodReflection, $methodCall, $this); + $resolvedType = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall( + $methodReflection, + $methodCall, + $this, + ); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } } } diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 8e175c7725..73aab3de3e 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -7,11 +7,13 @@ use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; use PHPStan\Internal\BytesHelper; use PHPStan\PhpDoc\StubValidator; +use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Input\InputInterface; use function array_merge; use function count; use function is_string; use function memory_get_peak_usage; +use function microtime; use function sprintf; class AnalyseApplication @@ -148,15 +150,21 @@ private function runAnalyser( $errorOutput->getStyle()->progressAdvance($step); }; } else { - $preFileCallback = static function (string $file) use ($stdOutput): void { + $startTime = null; + $preFileCallback = static function (string $file) use ($stdOutput, &$startTime): void { $stdOutput->writeLineFormatted($file); + $startTime = microtime(true); }; $postFileCallback = null; if ($stdOutput->isDebug()) { $previousMemory = memory_get_peak_usage(true); - $postFileCallback = static function () use ($stdOutput, &$previousMemory): void { + $postFileCallback = static function () use ($stdOutput, &$previousMemory, &$startTime): void { + if ($startTime === null) { + throw new ShouldNotHappenException(); + } $currentTotalMemory = memory_get_peak_usage(true); - $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory))); + $elapsedTime = microtime(true) - $startTime; + $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s, took %.2f s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory), $elapsedTime)); $previousMemory = $currentTotalMemory; }; } diff --git a/src/Type/DynamicFunctionReturnTypeExtension.php b/src/Type/DynamicFunctionReturnTypeExtension.php index ac4750ce53..92941735a3 100644 --- a/src/Type/DynamicFunctionReturnTypeExtension.php +++ b/src/Type/DynamicFunctionReturnTypeExtension.php @@ -12,6 +12,6 @@ interface DynamicFunctionReturnTypeExtension public function isFunctionSupported(FunctionReflection $functionReflection): bool; - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type; + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type; } diff --git a/src/Type/DynamicMethodReturnTypeExtension.php b/src/Type/DynamicMethodReturnTypeExtension.php index 58635acca7..35f5b505ca 100644 --- a/src/Type/DynamicMethodReturnTypeExtension.php +++ b/src/Type/DynamicMethodReturnTypeExtension.php @@ -14,6 +14,6 @@ public function getClass(): string; public function isMethodSupported(MethodReflection $methodReflection): bool; - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type; + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type; } diff --git a/src/Type/DynamicStaticMethodReturnTypeExtension.php b/src/Type/DynamicStaticMethodReturnTypeExtension.php index e2de530f9f..94560039a6 100644 --- a/src/Type/DynamicStaticMethodReturnTypeExtension.php +++ b/src/Type/DynamicStaticMethodReturnTypeExtension.php @@ -14,6 +14,6 @@ public function getClass(): string; public function isStaticMethodSupported(MethodReflection $methodReflection): bool; - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type; + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type; } diff --git a/src/Type/Php/StrContainingTypeSpecifyingExtension.php b/src/Type/Php/StrContainingTypeSpecifyingExtension.php new file mode 100644 index 0000000000..4de8a25cf0 --- /dev/null +++ b/src/Type/Php/StrContainingTypeSpecifyingExtension.php @@ -0,0 +1,79 @@ +typeSpecifier = $typeSpecifier; + } + + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool + { + return in_array(strtolower($functionReflection->getName()), $this->strContainingFunctions, true) + && $context->true(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + + if (count($args) >= 2) { + $haystackType = $scope->getType($args[0]->value); + $needleType = $scope->getType($args[1]->value); + + if ($needleType->isNonEmptyString()->yes() && $haystackType->isString()->yes()) { + $accessories = [ + new StringType(), + new AccessoryNonEmptyStringType(), + ]; + + if ($haystackType->isLiteralString()->yes()) { + $accessories[] = new AccessoryLiteralStringType(); + } + if ($haystackType->isNumericString()->yes()) { + $accessories[] = new AccessoryNumericStringType(); + } + + return $this->typeSpecifier->create( + $args[0]->value, + new IntersectionType($accessories), + $context, + false, + $scope, + ); + } + } + + return new SpecifiedTypes(); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index ebc906aacb..ce82ac9ed7 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -791,6 +791,8 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-str-contains.php'); } /** diff --git a/tests/PHPStan/Analyser/data/non-empty-string-str-contains.php b/tests/PHPStan/Analyser/data/non-empty-string-str-contains.php new file mode 100644 index 0000000000..96c251a967 --- /dev/null +++ b/tests/PHPStan/Analyser/data/non-empty-string-str-contains.php @@ -0,0 +1,60 @@ +