diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b0f15a8a69d..63b72d28f39 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -34,6 +34,8 @@ jobs: fail-fast: false matrix: include: + - script: bash e2e/result-cache-name-scopes/test.sh + php-version: '8.3' - script: | cd e2e/result-cache-1 echo -n > phpstan-baseline.neon @@ -1682,7 +1684,7 @@ jobs: uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0 with: coverage: "none" - php-version: "8.2" + php-version: ${{ matrix.php-version || '8.2' }} extensions: mbstring ini-values: memory_limit=256M @@ -2166,6 +2168,9 @@ jobs: ../bashunit -a not_contains 'parallel worker' "$(grep -F 'parallel worker' "$RUNNER_TEMP/small-shm.txt" || true)" diff "$RUNNER_TEMP/reference.txt" "$RUNNER_TEMP/small-shm.txt" + - name: "Verify PHPDoc name scopes survive partial analyses" + run: bash e2e/result-cache-name-scopes/test.sh + turbo-fork-unsafe-extension-e2e-tests: name: "Turbo fork-unsafe extension E2E tests" runs-on: "ubuntu-latest" diff --git a/conf/parsers.neon b/conf/parsers.neon index 05b1fabb788..7b81c9c5e69 100644 --- a/conf/parsers.neon +++ b/conf/parsers.neon @@ -87,6 +87,17 @@ services: factory: @pathRoutingParser autowired: false + # Persist complete name scopes independently of the analysed paths, while + # retaining the PHP 8 parser routing for version-independent stub files. + fileTypeMapperParser: + class: PHPStan\Parser\PathRoutingParser + arguments: + currentPhpVersionRichParser: @cachedRichParser + currentPhpVersionSimpleParser: @cachedRichParser + php8Parser: @php8Parser + singleReflectionFile: null + autowired: false + freshStubParser: class: PHPStan\Parser\StubParser arguments: diff --git a/e2e/result-cache-name-scopes/phpstan.neon b/e2e/result-cache-name-scopes/phpstan.neon new file mode 100644 index 00000000000..ae391920378 --- /dev/null +++ b/e2e/result-cache-name-scopes/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: max + paths: + - src + scanFiles: + - src/helpers.php diff --git a/e2e/result-cache-name-scopes/src/helpers.php b/e2e/result-cache-name-scopes/src/helpers.php new file mode 100644 index 00000000000..dc40b28c680 --- /dev/null +++ b/e2e/result-cache-name-scopes/src/helpers.php @@ -0,0 +1,28 @@ += 8.3 + +declare(strict_types=1); + +interface Inbox +{ + /** @return list */ + public function search(string $words): array; +} + +/** + * @param Closure(string): list $search + */ +function inbox(Closure $search): Inbox +{ + return new readonly class($search) implements Inbox + { + /** + * @param Closure(string): list $search + */ + public function __construct(private Closure $search) {} + + public function search(string $words): array + { + return ($this->search)($words); + } + }; +} diff --git a/e2e/result-cache-name-scopes/src/usage.php b/e2e/result-cache-name-scopes/src/usage.php new file mode 100644 index 00000000000..09a8e130999 --- /dev/null +++ b/e2e/result-cache-name-scopes/src/usage.php @@ -0,0 +1,8 @@ + [$words])->search('a b'); +} diff --git a/e2e/result-cache-name-scopes/test.sh b/e2e/result-cache-name-scopes/test.sh new file mode 100644 index 00000000000..31634476fd9 --- /dev/null +++ b/e2e/result-cache-name-scopes/test.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +COLD_CACHE=$(mktemp -d) +WARM_CACHE=$(mktemp -d) +for CACHE in "$COLD_CACHE" "$WARM_CACHE"; do + printf 'includes:\n\t- %s/phpstan.neon\nparameters:\n\ttmpDir: %s\n' "$PWD" "$CACHE" > "$CACHE/phpstan.neon" +done + +../../bin/phpstan analyse --debug --no-progress --error-format=raw -c "$COLD_CACHE/phpstan.neon" +../../bin/phpstan analyse --debug --no-progress --error-format=raw -c "$WARM_CACHE/phpstan.neon" src/usage.php +../../bin/phpstan clear-result-cache -q -c "$WARM_CACHE/phpstan.neon" +../../bin/phpstan analyse --debug --no-progress --error-format=raw -c "$WARM_CACHE/phpstan.neon" diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 673a2bb89ba..338e18a1c8f 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -75,7 +75,7 @@ final class FileTypeMapper public function __construct( private ReflectionProviderProvider $reflectionProviderProvider, - #[AutowiredParameter(ref: '@defaultAnalysisParser')] + #[AutowiredParameter(ref: '@fileTypeMapperParser')] private Parser $phpParser, private PhpDocStringResolver $phpDocStringResolver, private PhpDocNodeResolver $phpDocNodeResolver, @@ -371,7 +371,8 @@ private function getNameScopeMap(string $fileName): array // and the content hashes cannot tell them apart from real ones. // v7: IntermediaryNameScope refers to a NamespaceUses shared with the other name scopes // created under the same use statements - $variableCacheKey = sprintf('v7-%s', ComposerHelper::getPhpDocParserVersion()); + // v8: earlier entries can lack name scopes for classes inside cleaned function bodies. + $variableCacheKey = sprintf('v8-%s', ComposerHelper::getPhpDocParserVersion()); $cached = $this->loadCachedPhpDocNodeMap($cacheKey, $variableCacheKey); if ($cached === null) { [$nameScopeMap, $files] = $this->createPhpDocNodeMap($fileName, null, null, [], $fileName);