diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml
new file mode 100644
index 0000000000..48a940b76c
--- /dev/null
+++ b/.github/workflows/reflection-golden-test.yml
@@ -0,0 +1,191 @@
+# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
+
+name: "Reflection golden test"
+
+on:
+ pull_request:
+ paths-ignore:
+ - 'compiler/**'
+ - 'apigen/**'
+ - 'changelog-generator/**'
+ - 'issue-bot/**'
+ push:
+ branches:
+ - "1.10.x"
+ paths-ignore:
+ - 'compiler/**'
+ - 'apigen/**'
+ - 'changelog-generator/**'
+ - 'issue-bot/**'
+
+env:
+ COMPOSER_ROOT_VERSION: "1.10.x-dev"
+ REFLECTION_GOLDEN_TEST_FILE: "/tmp/reflection-golden.test"
+ REFLECTION_GOLDEN_SYMBOLS_FILE: "/tmp/reflection-golden-symbols.txt"
+
+concurrency:
+ group: reflection-golden-test-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches
+ cancel-in-progress: true
+
+jobs:
+ dump-php-symbols:
+ name: "Dump PHP symbols"
+ runs-on: "ubuntu-latest"
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v3
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "8.3"
+ # Include exotic extensions to discover more symbols
+ extensions: ds,mbstring,runkit7,scoutapm,seaslog,simdjson,var_representation,yac
+
+ - name: "Install dependencies"
+ run: "composer install --no-interaction --no-progress"
+
+ - name: "Dump phpSymbols.txt"
+ run: "php tests/dump-reflection-test-symbols.php"
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: phpSymbols
+ path: ${{ env.REFLECTION_GOLDEN_SYMBOLS_FILE }}
+
+ reflection-golden-test:
+ name: "Reflection golden test"
+ needs: dump-php-symbols
+ runs-on: "ubuntu-latest"
+ timeout-minutes: 60
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php-version:
+ - "7.3"
+ - "7.4"
+ - "8.0"
+ - "8.1"
+ - "8.2"
+ - "8.3"
+
+ steps:
+ - name: "Download phpSymbols.txt"
+ uses: actions/download-artifact@v3
+ with:
+ name: phpSymbols
+ path: /tmp
+
+ - name: "Checkout base commit"
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.base.sha || github.event.push.before }}
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "${{ matrix.php-version }}"
+ tools: pecl
+ extensions: ds,mbstring
+ ini-file: development
+ ini-values: memory_limit=2G
+
+ - name: "Install dependencies"
+ run: "composer install --no-interaction --no-progress"
+
+ - name: "Install PHP for code transform"
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: 8.1
+ extensions: mbstring, intl
+
+ - name: "Rector downgrade cache key"
+ id: rector-cache-key-base
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ run: echo "sha=$(php build/rector-cache-files-hash.php)" >> $GITHUB_OUTPUT
+
+ - name: "Rector downgrade cache"
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ uses: actions/cache@v3
+ with:
+ path: ./tmp/rectorCache.php
+ key: "rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key-base.outputs.sha }}"
+ restore-keys: |
+ rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-
+
+ - name: "Transform source code"
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ shell: bash
+ run: "build/transform-source ${{ matrix.php-version }}"
+
+ - name: "Reinstall matrix PHP version"
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "${{ matrix.php-version }}"
+ tools: pecl
+ extensions: ds,mbstring
+ ini-file: development
+ ini-values: memory_limit=2G
+
+ - name: "Dump previous reflection data"
+ run: "php tests/generate-reflection-test.php"
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: reflection-${{ matrix.php-version }}.test
+ path: ${{ env.REFLECTION_GOLDEN_TEST_FILE }}
+
+ - name: "Checkout"
+ uses: actions/checkout@v3
+
+ - name: "Install dependencies"
+ run: "composer install --no-interaction --no-progress"
+
+ - name: "Install PHP for code transform"
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: 8.1
+ extensions: mbstring, intl
+
+ - name: "Rector downgrade cache key"
+ id: rector-cache-key-head
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ run: echo "sha=$(php build/rector-cache-files-hash.php)" >> $GITHUB_OUTPUT
+
+ - name: "Rector downgrade cache"
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ uses: actions/cache@v3
+ with:
+ path: ./tmp/rectorCache.php
+ key: "rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key-head.outputs.sha }}"
+ restore-keys: |
+ rector-v3-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-
+
+ - name: "Transform source code"
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ shell: bash
+ run: "build/transform-source ${{ matrix.php-version }}"
+
+ - name: "Reinstall matrix PHP version"
+ if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3'
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "${{ matrix.php-version }}"
+ tools: pecl
+ extensions: ds,mbstring
+ ini-file: development
+ ini-values: memory_limit=2G
+
+ - name: "Reflection golden test"
+ run: "make tests-golden-reflection"
diff --git a/.gitignore b/.gitignore
index 65cd70f464..f138e3cb50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@
!.idea/icon.png
/tests/tmp
/tests/.phpunit.result.cache
+/tests/PHPStan/Reflection/data/golden/
tmp/.memory_limit
diff --git a/Makefile b/Makefile
index 9fa83455bb..67614d0a22 100644
--- a/Makefile
+++ b/Makefile
@@ -14,6 +14,9 @@ tests-levels:
tests-coverage:
php vendor/bin/paratest --runner WrapperRunner
+tests-golden-reflection:
+ php vendor/bin/paratest --runner WrapperRunner --no-coverage tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php
+
lint:
php vendor/bin/parallel-lint --colors \
--exclude tests/PHPStan/Analyser/data \
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 37501d1395..ecd5ee56f5 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -1742,6 +1742,11 @@ parameters:
count: 1
path: tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php
+ -
+ message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
+ count: 1
+ path: tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php
+
-
message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
count: 1
diff --git a/phpunit.xml b/phpunit.xml
index 5ca403e84c..03fcbf718f 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -25,6 +25,7 @@
tests/PHPStan
+ tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php
diff --git a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php
new file mode 100644
index 0000000000..9cdeb0b2d9
--- /dev/null
+++ b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php
@@ -0,0 +1,648 @@
+> */
+ public static function data(): iterable
+ {
+ $inputFile = self::getTestInputFile();
+ $contents = file_get_contents($inputFile);
+
+ if ($contents === false) {
+ self::fail('Input file \'' . $inputFile . '\' is missing.');
+ }
+
+ $parts = explode('-----', $contents);
+
+ for ($i = 1; $i + 1 < count($parts); $i += 2) {
+ $input = trim($parts[$i]);
+ $output = trim($parts[$i + 1]);
+
+ yield $input => [
+ $input,
+ $output,
+ ];
+ }
+ }
+
+ /** @dataProvider data */
+ public function test(string $input, string $expectedOutput): void
+ {
+ $output = self::generateSymbolDescription($input);
+ $output = trim($output);
+ $this->assertSame($expectedOutput, $output);
+ }
+
+ private static function generateSymbolDescription(string $symbol): string
+ {
+ [$type, $name] = explode(' ', $symbol);
+
+ try {
+ switch ($type) {
+ case 'FUNCTION':
+ return self::generateFunctionDescription($name);
+ case 'CLASS':
+ return self::generateClassDescription($name);
+ case 'METHOD':
+ return self::generateClassMethodDescription($name);
+ case 'PROPERTY':
+ return self::generateClassPropertyDescription($name);
+ default:
+ self::fail('Unknown symbol type ' . $type);
+ }
+ } catch (Throwable $e) {
+ // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly
+ if ($e instanceof \PHPUnit\Exception) {
+ throw $e;
+ }
+
+ // Skip stack trace - it's not fully consistent between dump and test.
+ return "Generating symbol description failed:\n"
+ . get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() . "\n";
+ }
+ }
+
+ public static function dumpOutput(): void
+ {
+ $symbolsTxt = file_get_contents(self::getPhpSymbolsFile());
+
+ if ($symbolsTxt === false) {
+ throw new ShouldNotHappenException('Cannot read phpSymbols.txt');
+ }
+
+ $symbols = explode("\n", $symbolsTxt);
+ $separator = '-----';
+ $contents = '';
+
+ foreach ($symbols as $line) {
+ $contents .= $separator . "\n";
+ $contents .= $line . "\n";
+ $contents .= $separator . "\n";
+ $contents .= self::generateSymbolDescription($line);
+ }
+
+ $result = file_put_contents(self::getTestInputFile(), $contents);
+
+ if ($result !== false) {
+ return;
+ }
+
+ throw new ShouldNotHappenException('Failed write dump for reflection golden test.');
+ }
+
+ private static function getTestInputFile(): string
+ {
+ $fileFromEnv = getenv('REFLECTION_GOLDEN_TEST_FILE');
+
+ if ($fileFromEnv !== false) {
+ return $fileFromEnv;
+ }
+
+ $first = (int) floor(PHP_VERSION_ID / 10000);
+ $second = (int) (floor(PHP_VERSION_ID % 10000) / 100);
+ $currentVersion = $first . '.' . $second;
+
+ return __DIR__ . '/data/golden/reflection-' . $currentVersion . '.test';
+ }
+
+ private static function getPhpSymbolsFile(): string
+ {
+ $fileFromEnv = getenv('REFLECTION_GOLDEN_SYMBOLS_FILE');
+
+ if ($fileFromEnv !== false) {
+ return $fileFromEnv;
+ }
+
+ return __DIR__ . '/data/golden/phpSymbols.txt';
+ }
+
+ private static function generateFunctionDescription(string $functionName): string
+ {
+ $nameNode = new Name($functionName);
+ $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class);
+
+ if (! $reflectionProvider->hasFunction($nameNode, null)) {
+ return "MISSING\n";
+ }
+
+ $functionReflection = $reflectionProvider->getFunction($nameNode, null);
+ $result = self::generateFunctionMethodBaseDescription($functionReflection);
+
+ if (! $functionReflection->isBuiltin()) {
+ $result .= "NOT BUILTIN\n";
+ }
+
+ $result .= self::generateVariantsDescription($functionReflection->getName(), $functionReflection->getVariants());
+
+ return $result;
+ }
+
+ private static function generateClassDescription(string $className): string
+ {
+ $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class);
+
+ if (! $reflectionProvider->hasClass($className)) {
+ return "MISSING\n";
+ }
+
+ $result = '';
+ $classReflection = $reflectionProvider->getClass($className);
+
+ if ($classReflection->isDeprecated()) {
+ $result .= "Deprecated\n";
+ }
+
+ if (! $classReflection->isBuiltin()) {
+ $result .= "Not builtin\n";
+ }
+
+ if ($classReflection->isInternal()) {
+ $result .= "Internal\n";
+ }
+
+ if ($classReflection->isImmutable()) {
+ $result .= "Immutable\n";
+ }
+
+ if ($classReflection->hasConsistentConstructor()) {
+ $result .= "Consistent constructor\n";
+ }
+
+ $parentReflection = $classReflection->getParentClass();
+ $extends = '';
+
+ if ($parentReflection !== null) {
+ $extends = ' extends ' . $parentReflection->getName();
+ }
+
+ $attributes = [];
+
+ if ($classReflection->allowsDynamicProperties()) {
+ $attributes[] = "#[AllowDynamicProperties]\n";
+ }
+
+ $attributesTxt = implode('', $attributes);
+ $abstractTxt = $classReflection->isAbstract()
+ ? 'abstract '
+ : '';
+
+ switch (true) {
+ case $classReflection->isEnum():
+ $keyword = 'enum';
+ break;
+ case $classReflection->isInterface():
+ $keyword = 'interface';
+ break;
+ case $classReflection->isTrait():
+ $keyword = 'trait';
+ break;
+ case $classReflection->isClass():
+ $keyword = 'class';
+ break;
+ default:
+ $keyword = self::fail();
+ }
+
+ $verbosityLevel = VerbosityLevel::precise();
+ $backedEnumType = $classReflection->getBackedEnumType();
+ $backedEnumTypeTxt = $backedEnumType !== null
+ ? ': ' . $backedEnumType->describe($verbosityLevel)
+ : '';
+ $readonlyTxt = $classReflection->isReadOnly()
+ ? 'readonly '
+ : '';
+ $interfaceNames = array_keys($classReflection->getImmediateInterfaces());
+ $implementsTxt = $interfaceNames !== []
+ ? ($classReflection->isInterface() ? ' extends ' : ' implements ') . implode(', ', $interfaceNames)
+ : '';
+ $finalTxt = $classReflection->isFinal()
+ ? 'final '
+ : '';
+ $result .= $attributesTxt . $finalTxt . $readonlyTxt . $abstractTxt . $keyword . ' '
+ . $classReflection->getName() . $extends . $implementsTxt . $backedEnumTypeTxt . "\n";
+ $result .= "{\n";
+ $ident = ' ';
+
+ foreach (array_keys($classReflection->getTraits()) as $trait) {
+ $result .= $ident . 'use ' . $trait . ";\n";
+ }
+
+ $result .= "}\n";
+
+ return $result;
+ }
+
+ private static function generateClassMethodDescription(string $classMethodName): string
+ {
+ [$className, $methodName] = explode('::', $classMethodName);
+
+ $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class);
+
+ if (! $reflectionProvider->hasClass($className)) {
+ return "MISSING\n";
+ }
+
+ $classReflection = $reflectionProvider->getClass($className);
+
+ if (! $classReflection->hasNativeMethod($methodName)) {
+ return "MISSING\n";
+ }
+
+ $methodReflection = $classReflection->getNativeMethod($methodName);
+ $result = self::generateFunctionMethodBaseDescription($methodReflection);
+ $verbosityLevel = VerbosityLevel::precise();
+
+ if ($methodReflection->getSelfOutType() !== null) {
+ $result .= 'Self out type: ' . $methodReflection->getSelfOutType()->describe($verbosityLevel) . "\n";
+ }
+
+ if ($methodReflection->isStatic()) {
+ $result .= "Static\n";
+ }
+
+ switch (true) {
+ case $methodReflection->isPublic():
+ $visibility = 'public';
+ break;
+ case $methodReflection->isPrivate():
+ $visibility = 'private';
+ break;
+ default:
+ $visibility = 'protected';
+ break;
+ }
+
+ $result .= 'Visibility: ' . $visibility . "\n";
+ $result .= self::generateVariantsDescription($methodReflection->getName(), $methodReflection->getVariants());
+
+ return $result;
+ }
+
+ /** @param FunctionReflection|ExtendedMethodReflection $reflection */
+ private static function generateFunctionMethodBaseDescription($reflection): string
+ {
+ $result = '';
+
+ if (! $reflection->isDeprecated()->no()) {
+ $result .= 'Is deprecated: ' . $reflection->isDeprecated()->describe() . "\n";
+ }
+
+ if (! $reflection->isFinal()->no()) {
+ $result .= 'Is final: ' . $reflection->isFinal()->describe() . "\n";
+ }
+
+ if (! $reflection->isInternal()->no()) {
+ $result .= 'Is internal: ' . $reflection->isInternal()->describe() . "\n";
+ }
+
+ if (! $reflection->returnsByReference()->no()) {
+ $result .= 'Returns by reference: ' . $reflection->returnsByReference()->describe() . "\n";
+ }
+
+ if (! $reflection->hasSideEffects()->no()) {
+ $result .= 'Has side-effects: ' . $reflection->hasSideEffects()->describe() . "\n";
+ }
+
+ if ($reflection->getThrowType() !== null) {
+ $result .= 'Throw type: ' . $reflection->getThrowType()->describe(VerbosityLevel::precise()) . "\n";
+ }
+
+ return $result;
+ }
+
+ /** @param ParametersAcceptorWithPhpDocs[] $variants */
+ private static function generateVariantsDescription(string $name, array $variants): string
+ {
+ $variantCount = count($variants);
+ $result = 'Variants: ' . $variantCount . "\n";
+ $variantIdent = ' ';
+ $verbosityLevel = VerbosityLevel::precise();
+
+ foreach ($variants as $variant) {
+ $paramsNative = [];
+ $paramsPhpDoc = [];
+
+ foreach ($variant->getParameters() as $param) {
+ $paramsPhpDoc[] = $variantIdent . ' * @param ' . $param->getType()->describe($verbosityLevel) . ' $' . $param->getName() . "\n";
+
+ if ($param->getOutType() !== null) {
+ $paramsPhpDoc[] = $variantIdent . ' * @param-out ' . $param->getOutType()->describe($verbosityLevel) . ' $' . $param->getName() . "\n";
+ }
+
+ $passedByRef = $param->passedByReference();
+
+ if ($passedByRef->no()) {
+ $refDes = '';
+ } elseif ($passedByRef->createsNewVariable()) {
+ $refDes = '&rw';
+ } else {
+ $refDes = '&r';
+ }
+
+ $variadicDesc = $param->isVariadic() ? '...' : '';
+ $defValueDesc = $param->getDefaultValue() !== null
+ ? ' = ' . $param->getDefaultValue()->describe($verbosityLevel)
+ : '';
+
+ $paramsNative[] = $param->getNativeType()->describe($verbosityLevel) . ' ' . $variadicDesc . $refDes . '$' . $param->getName() . $defValueDesc;
+ }
+
+ $result .= $variantIdent . "/**\n";
+ $result .= implode('', $paramsPhpDoc);
+ $result .= $variantIdent . ' * @return ' . $variant->getReturnType()->describe($verbosityLevel) . "\n";
+ $result .= $variantIdent . " */\n";
+ $paramsTxt = implode(', ', $paramsNative);
+ $result .= $variantIdent . 'function ' . $name . '(' . $paramsTxt . '): ' . $variant->getNativeReturnType()->describe($verbosityLevel) . "\n";
+ }
+
+ return $result;
+ }
+
+ private static function generateClassPropertyDescription(string $propertyName): string
+ {
+ [$className, $propertyName] = explode('::', $propertyName);
+
+ $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class);
+
+ if (! $reflectionProvider->hasClass($className)) {
+ return "MISSING\n";
+ }
+
+ $classReflection = $reflectionProvider->getClass($className);
+
+ if (! $classReflection->hasNativeProperty($propertyName)) {
+ return "MISSING\n";
+ }
+
+ $result = '';
+ $propertyReflection = $classReflection->getNativeProperty($propertyName);
+
+ if (! $propertyReflection->isDeprecated()->no()) {
+ $result .= 'Is deprecated: ' . $propertyReflection->isDeprecated()->describe() . "\n";
+ }
+
+ if (! $propertyReflection->isInternal()->no()) {
+ $result .= 'Is internal: ' . $propertyReflection->isDeprecated()->describe() . "\n";
+ }
+
+ if ($propertyReflection->isStatic()) {
+ $result .= "Static\n";
+ }
+
+ if ($propertyReflection->isReadOnly()) {
+ $result .= "Readonly\n";
+ }
+
+ switch (true) {
+ case $propertyReflection->isPublic():
+ $visibility = 'public';
+ break;
+ case $propertyReflection->isPrivate():
+ $visibility = 'private';
+ break;
+ default:
+ $visibility = 'protected';
+ break;
+ }
+
+ $result .= 'Visibility: ' . $visibility . "\n";
+ $verbosityLevel = VerbosityLevel::precise();
+
+ if ($propertyReflection->isReadable()) {
+ $result .= 'Read type: ' . $propertyReflection->getReadableType()->describe($verbosityLevel) . "\n";
+ }
+
+ if ($propertyReflection->isWritable()) {
+ $result .= 'Write type: ' . $propertyReflection->getWritableType()->describe($verbosityLevel) . "\n";
+ }
+
+ return $result;
+ }
+
+ public static function dumpInputSymbols(): void
+ {
+ $symbols = self::scrapeInputSymbols();
+ $symbolsFile = self::getPhpSymbolsFile();
+ @mkdir(dirname($symbolsFile), 0777, true);
+ $result = file_put_contents($symbolsFile, implode("\n", $symbols));
+
+ if ($result !== false) {
+ return;
+ }
+
+ throw new ShouldNotHappenException('Failed write dump for reflection golden test.');
+ }
+
+ /** @return list */
+ public static function scrapeInputSymbols(): array
+ {
+ $result = array_keys(
+ self::scrapeInputSymbolsFromFunctionMap()
+ + self::scrapeInputSymbolsFromPhp8Stubs()
+ + self::scrapeInputSymbolsFromPhpStormStubs()
+ + self::scrapeInputSymbolsFromReflection(),
+ );
+ sort($result);
+
+ return $result;
+ }
+
+ /** @return array */
+ private static function scrapeInputSymbolsFromFunctionMap(): array
+ {
+ $finder = new Finder();
+ $files = $finder->files()->name('functionMap*.php')->in(__DIR__ . '/../../../resources');
+ $combinedMap = [];
+
+ foreach ($files as $file) {
+ if ($file->getBasename() === 'functionMap.php') {
+ $combinedMap += require $file->getPathname();
+ continue;
+ }
+
+ $deltaMap = require $file->getPathname();
+
+ // Deltas have new/old sections which contain the same format as the base functionMap.php
+ foreach ($deltaMap as $functionMap) {
+ $combinedMap += $functionMap;
+ }
+ }
+
+ $result = [];
+
+ foreach (array_keys($combinedMap) as $symbol) {
+ // skip duplicated variants
+ if (strpos($symbol, "'") !== false) {
+ continue;
+ }
+
+ $parts = explode('::', $symbol);
+
+ switch (count($parts)) {
+ case 1:
+ $result['FUNCTION ' . $symbol] = true;
+ break;
+ case 2:
+ $result['CLASS ' . $parts[0]] = true;
+ $result['METHOD ' . $symbol] = true;
+ break;
+ default:
+ throw new ShouldNotHappenException('Invalid symbol ' . $symbol);
+ }
+ }
+
+ return $result;
+ }
+
+ /** @return array */
+ private static function scrapeInputSymbolsFromPhp8Stubs(): array
+ {
+ // Currently the Php8StubsMap only adds symbols for later versions, so let's max it.
+ $map = new Php8StubsMap(PHP_INT_MAX);
+ $files = [];
+
+ foreach (array_merge($map->classes, $map->functions) as $file) {
+ $files[] = __DIR__ . '/../../../vendor/phpstan/php-8-stubs/' . $file;
+ }
+
+ return self::scrapeSymbolsFromStubs($files);
+ }
+
+ /** @return array */
+ private static function scrapeInputSymbolsFromPhpStormStubs(): array
+ {
+ $files = [];
+
+ foreach (PhpStormStubsMap::CLASSES as $file) {
+ $files[] = PhpStormStubsMap::DIR . '/' . $file;
+ }
+
+ return self::scrapeSymbolsFromStubs($files);
+ }
+
+ /** @return array */
+ private static function scrapeInputSymbolsFromReflection(): array
+ {
+ $result = [];
+
+ foreach (get_defined_functions()['internal'] as $function) {
+ $result['FUNCTION ' . $function] = true;
+ }
+
+ foreach (get_declared_classes() as $class) {
+ $reflection = new ReflectionClass($class);
+
+ if ($reflection->getFileName() !== false) {
+ continue;
+ }
+
+ $className = $reflection->getName();
+ $result['CLASS ' . $className] = true;
+
+ foreach ($reflection->getMethods() as $method) {
+ $result['METHOD ' . $className . '::' . $method->getName()] = true;
+ }
+
+ foreach ($reflection->getProperties() as $property) {
+ $result['PROPERTY ' . $className . '::$' . $property->getName()] = true;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param array $stubFiles
+ * @return array
+ */
+ private static function scrapeSymbolsFromStubs(array $stubFiles): array
+ {
+ $parser = self::getContainer()->getService('defaultAnalysisParser');
+ self::assertInstanceOf(Parser::class, $parser);
+ $visitor = new class () extends NodeVisitorAbstract {
+
+ /** @var array */
+ public array $symbols = [];
+
+ private Node\Stmt\ClassLike $classLike;
+
+ public function enterNode(Node $node)
+ {
+ if ($node instanceof Node\Stmt\ClassLike && $node->namespacedName !== null) {
+ $this->symbols['CLASS ' . $node->namespacedName->toString()] = true;
+ $this->classLike = $node;
+ }
+
+ if ($node instanceof Node\Stmt\ClassMethod && isset($this->classLike->namespacedName)) {
+ $this->symbols['METHOD ' . $this->classLike->namespacedName->toString() . '::' . $node->name->name] = true;
+ }
+
+ if ($node instanceof Node\Stmt\PropertyProperty && isset($this->classLike->namespacedName)) {
+ $this->symbols['PROPERTY ' . $this->classLike->namespacedName->toString() . '::$' . $node->name->toString()] = true;
+ }
+
+ if ($node instanceof Node\Stmt\Function_) {
+ $this->symbols['FUNCTION ' . $node->name->name] = true;
+ }
+
+ return null;
+ }
+
+ public function leaveNode(Node $node)
+ {
+ if ($node instanceof Node\Stmt\ClassLike) {
+ unset($this->classLike);
+ }
+
+ return null;
+ }
+
+ };
+ $traverser = new NodeTraverser();
+ $traverser->addVisitor($visitor);
+
+ foreach ($stubFiles as $file) {
+ $ast = $parser->parseFile($file);
+ $traverser->traverse($ast);
+ }
+
+ return $visitor->symbols;
+ }
+
+}
diff --git a/tests/dump-reflection-test-symbols.php b/tests/dump-reflection-test-symbols.php
new file mode 100644
index 0000000000..17281662c6
--- /dev/null
+++ b/tests/dump-reflection-test-symbols.php
@@ -0,0 +1,10 @@
+