diff --git a/src/Rules/Keywords/RequireFileExistsRule.php b/src/Rules/Keywords/RequireFileExistsRule.php index 4ea63f57159..77278a6d8e2 100644 --- a/src/Rules/Keywords/RequireFileExistsRule.php +++ b/src/Rules/Keywords/RequireFileExistsRule.php @@ -104,7 +104,7 @@ private function doesFileExist(string $path, Scope $scope): bool $directories = array_merge( [$this->currentWorkingDirectory], explode(PATH_SEPARATOR, get_include_path()), - [dirname($scope->getFile())], + [dirname($this->getScopeFile($scope))], ); foreach ($directories as $directory) { @@ -116,6 +116,24 @@ private function doesFileExist(string $path, Scope $scope): bool return false; } + /** + * The "calling script's own directory" fallback of a relative include is resolved + * at compile time, so inside a trait it is the directory of the file the trait is + * declared in - not the file of the class that uses it, which is what + * Scope::getFile() returns in a trait context. + */ + private function getScopeFile(Scope $scope): string + { + if ($scope->isInTrait()) { + $traitFileName = $scope->getTraitReflection()->getFileName(); + if ($traitFileName !== null) { + return $this->fileHelper->normalizePath($traitFileName); + } + } + + return $scope->getFile(); + } + private function doesFileExistForDirectory(string $path, string $workingDirectory): bool { $fileHelper = new FileHelper($workingDirectory); diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php index 7cad2911c20..9760d640fcb 100644 --- a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php @@ -158,6 +158,19 @@ public function testBug12203(): void ]); } + public function testRelativePathInTrait(): void + { + $this->analyse([ + __DIR__ . '/data/include-relative-in-trait/trait-dir/IncludeRelativeTrait.php', + __DIR__ . '/data/include-relative-in-trait/class-dir/IncludeRelativeClass.php', + ], [ + [ + 'Path in include() "only-in-class-dir.php" is not a file or it does not exist.', + 15, + ], + ]); + } + public function testInFileExists(): void { $this->analyse([__DIR__ . '/data/include-in-file-exists.php'], []); diff --git a/tests/PHPStan/Rules/Keywords/data/include-relative-in-trait/class-dir/IncludeRelativeClass.php b/tests/PHPStan/Rules/Keywords/data/include-relative-in-trait/class-dir/IncludeRelativeClass.php new file mode 100644 index 00000000000..8df1c59f6b7 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/include-relative-in-trait/class-dir/IncludeRelativeClass.php @@ -0,0 +1,10 @@ + true]; diff --git a/tests/PHPStan/Rules/Keywords/data/include-relative-in-trait/trait-dir/IncludeRelativeTrait.php b/tests/PHPStan/Rules/Keywords/data/include-relative-in-trait/trait-dir/IncludeRelativeTrait.php new file mode 100644 index 00000000000..b82857cce0c --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/include-relative-in-trait/trait-dir/IncludeRelativeTrait.php @@ -0,0 +1,18 @@ + true];