diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index edd8d3eef6..bdcb30ac38 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -32,6 +32,7 @@ use function is_array; use function is_callable; use function is_file; +use function is_null; use function ltrim; use function md5; use function sprintf; @@ -109,21 +110,35 @@ public function getResolvedPhpDoc( return $this->createResolvedPhpDocBlock($phpDocKey, $nameScopeMap[$nameScopeKey], $docComment, $fileName); } + $keyFileName = $fileName; if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits + if (!is_null($className)) { + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + if ($reflectionProvider->hasClass($className)) { + $classFileName = $reflectionProvider->getClass($className)->getFileName(); + $nameScopeKeyCandidate = $this->getNameScopeKey($classFileName, $className, $traitName, $functionName); + if (isset($this->inProcess[$classFileName][$nameScopeKeyCandidate])) { + $keyFileName = $classFileName; + $nameScopeKey = $nameScopeKeyCandidate; + } + } + } + } + if (!isset($this->inProcess[$keyFileName][$nameScopeKey])) { return ResolvedPhpDocBlock::createEmpty(); } - if ($this->inProcess[$fileName][$nameScopeKey] === true) { // PHPDoc has cyclic dependency + if ($this->inProcess[$keyFileName][$nameScopeKey] === true) { // PHPDoc has cyclic dependency return ResolvedPhpDocBlock::createEmpty(); } - if (is_callable($this->inProcess[$fileName][$nameScopeKey])) { - $resolveCallback = $this->inProcess[$fileName][$nameScopeKey]; - $this->inProcess[$fileName][$nameScopeKey] = true; - $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback(); + if (is_callable($this->inProcess[$keyFileName][$nameScopeKey])) { + $resolveCallback = $this->inProcess[$keyFileName][$nameScopeKey]; + $this->inProcess[$keyFileName][$nameScopeKey] = true; + $this->inProcess[$keyFileName][$nameScopeKey] = $resolveCallback(); } - return $this->createResolvedPhpDocBlock($phpDocKey, $this->inProcess[$fileName][$nameScopeKey], $docComment, $fileName); + return $this->createResolvedPhpDocBlock($phpDocKey, $this->inProcess[$keyFileName][$nameScopeKey], $docComment, $fileName); } private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameScope, string $phpDocString, ?string $fileName): ResolvedPhpDocBlock diff --git a/tests/PHPStan/Generics/GenericsWithMultipleFilesIntegrationTest.php b/tests/PHPStan/Generics/GenericsWithMultipleFilesIntegrationTest.php new file mode 100644 index 0000000000..f29d56e056 --- /dev/null +++ b/tests/PHPStan/Generics/GenericsWithMultipleFilesIntegrationTest.php @@ -0,0 +1,95 @@ +> + */ + public function dataTopics(): array + { + return [ + 'bug9630' => ['bug9630', [9]], + 'bug9630_2' => ['bug9630_2', [9]], + ]; + } + + /** + * @dataProvider dataTopics + * + * @param int[] $levels + */ + public function testDir(string $dir, array $levels): void + { + $dir = $this->getDataPath() . DIRECTORY_SEPARATOR . $dir; + $this->assertDirectoryIsReadable($dir); + $phpstanCmd = escapeshellcmd($this->getPhpStanExecutablePath()); + $configPath = $this->getPhpStanConfigPath(); + + $configOption = is_null($configPath) ? '' : '--configuration ' . escapeshellarg($configPath); + + exec(sprintf('%s %s clear-result-cache %s 2>&1', escapeshellcmd(PHP_BINARY), $phpstanCmd, $configOption), $clearResultCacheOutputLines, $clearResultCacheExitCode); + + if ($clearResultCacheExitCode !== 0) { + throw new ShouldNotHappenException('Could not clear result cache: ' . implode("\n", $clearResultCacheOutputLines)); + } + + putenv('__PHPSTAN_FORCE_VALIDATE_STUB_FILES=1'); + + foreach ($levels as $level) { + unset($outputLines); + + $toExec = sprintf('%s %s analyse --no-progress --error-format=prettyJson --level=%d %s %s', escapeshellcmd(PHP_BINARY), $phpstanCmd, $level, $configOption, escapeshellarg($dir)); + + exec($toExec, $outputLines); + + $output = implode("\n", $outputLines); + + try { + $actualJson = Json::decode($output, Json::FORCE_ARRAY); + } catch (JsonException) { + throw new JsonException(sprintf('Cannot decode: %s', $output)); + } + + // Check that there was no error during the execution of PHPStan + foreach ($actualJson['files'] as $file => $fileJson) { + $this->assertCount(0, $fileJson['messages'], 'The file ' . $file . ' contains errors. The command that produced the error was: ' . $toExec); + } + } + } + + public function getDataPath(): string + { + return __DIR__ . '/data'; + } + + public function getPhpStanExecutablePath(): string + { + return __DIR__ . '/../../../bin/phpstan'; + } + + public function getPhpStanConfigPath(): string + { + return __DIR__ . '/generics.neon'; + } + +} diff --git a/tests/PHPStan/Generics/data/bug9630/C1.php b/tests/PHPStan/Generics/data/bug9630/C1.php new file mode 100644 index 0000000000..64dc09e552 --- /dev/null +++ b/tests/PHPStan/Generics/data/bug9630/C1.php @@ -0,0 +1,19 @@ + + */ +class C1 implements B +{ + /** + * @use T1 + */ + use T1; + + public function f(): ?A + { + return $this->getParam(new A2()); + } +} diff --git a/tests/PHPStan/Generics/data/bug9630/T1.php b/tests/PHPStan/Generics/data/bug9630/T1.php new file mode 100644 index 0000000000..8a18ca9ab5 --- /dev/null +++ b/tests/PHPStan/Generics/data/bug9630/T1.php @@ -0,0 +1,24 @@ +> + */ + use T2; + + /** + * @param T $p + * @return template-type + */ + public function getParam(A $p): ?A + { + return $this->getParamFromT2($p->getOther()); + } +} + diff --git a/tests/PHPStan/Generics/data/bug9630/T2.php b/tests/PHPStan/Generics/data/bug9630/T2.php new file mode 100644 index 0000000000..e5c1796239 --- /dev/null +++ b/tests/PHPStan/Generics/data/bug9630/T2.php @@ -0,0 +1,19 @@ + + */ +class A1 implements A +{ + public function getOther(): A2 + { + return new A2(); + } +} + +/** + * @implements A + */ +class A2 implements A +{ + public function getOther(): A1 + { + return new A1(); + } +} + +/** + * @template T of A + */ +interface B +{ + /** + * @return T|null + */ + public function f(): ?A; +} + diff --git a/tests/PHPStan/Generics/data/bug9630_2/C1.php b/tests/PHPStan/Generics/data/bug9630_2/C1.php new file mode 100644 index 0000000000..040561a857 --- /dev/null +++ b/tests/PHPStan/Generics/data/bug9630_2/C1.php @@ -0,0 +1,19 @@ + + */ +class C1 implements B +{ + /** + * @use T1 + */ + use T1; + + public function f(): ?A + { + return $this->getParam(new A2()); + } +} diff --git a/tests/PHPStan/Generics/data/bug9630_2/T1.php b/tests/PHPStan/Generics/data/bug9630_2/T1.php new file mode 100644 index 0000000000..da0bd2fb82 --- /dev/null +++ b/tests/PHPStan/Generics/data/bug9630_2/T1.php @@ -0,0 +1,24 @@ +> + */ + use T2; + + /** + * @param T $p + * @return template-type + */ + public function getParam(A $p): ?A + { + return $this->getParamFromT2($p->getOther()); + } +} + diff --git a/tests/PHPStan/Generics/data/bug9630_2/T2.php b/tests/PHPStan/Generics/data/bug9630_2/T2.php new file mode 100644 index 0000000000..3ba61d5f33 --- /dev/null +++ b/tests/PHPStan/Generics/data/bug9630_2/T2.php @@ -0,0 +1,19 @@ + + */ +class A1 implements A +{ + public function getOther(): A2 + { + return new A2(); + } +} + +/** + * @implements A + */ +class A2 implements A +{ + public function getOther(): A1 + { + return new A1(); + } +} + +/** + * @template T of A + */ +interface B +{ + /** + * @return T|null + */ + public function f(): ?A; +} +