From bbbf310ff99f2edeca32c4dfdec7a235aac2a953 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 13 Jun 2026 15:52:25 +0200 Subject: [PATCH] Fix "Differing behaviors when requiring or including relatively vs using __DIR__" --- src/Rules/Keywords/RequireFileExistsRule.php | 48 +++++++++++++---- ...equireFileExistsRuleNoConstantPathTest.php | 52 +++++++++++++++++++ .../Keywords/RequireFileExistsRuleTest.php | 17 ++++-- .../PHPStan/Rules/Keywords/data/bug-12203.php | 6 +++ 4 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php diff --git a/src/Rules/Keywords/RequireFileExistsRule.php b/src/Rules/Keywords/RequireFileExistsRule.php index b47817e4d6..d7e4076ca4 100644 --- a/src/Rules/Keywords/RequireFileExistsRule.php +++ b/src/Rules/Keywords/RequireFileExistsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PhpParser\Node\Arg; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Include_; use PhpParser\Node\Name\FullyQualified; @@ -11,10 +12,12 @@ use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\File\FileHelper; +use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Constant\ConstantStringType; use function array_merge; use function dirname; use function explode; @@ -33,6 +36,7 @@ final class RequireFileExistsRule implements Rule public function __construct( #[AutowiredParameter] private string $currentWorkingDirectory, + private ExprPrinter $exprPrinter, ) { } @@ -49,14 +53,23 @@ public function processNode(Node $node, Scope $scope): array } $errors = []; - $paths = $this->resolveFilePaths($node, $scope); + $usedMagicDirFallback = false; + $paths = $this->resolveFilePaths($node->expr, $scope, $usedMagicDirFallback); foreach ($paths as $path) { + $path = $path->getValue(); + if ($this->doesFileExist($path, $scope)) { continue; } - $errors[] = $this->getErrorMessage($node, $path); + if ($usedMagicDirFallback) { + $pathExpr = $this->exprPrinter->printExpr($node->expr); + } else { + $pathExpr = '"' . $path . '"'; + } + + $errors[] = $this->getErrorMessage($node, $pathExpr); } return $errors; @@ -97,7 +110,7 @@ private function doesFileExistForDirectory(string $path, string $workingDirector private function getErrorMessage(Include_ $node, string $filePath): IdentifierRuleError { - $message = 'Path in %s() "%s" is not a file or it does not exist.'; + $message = 'Path in %s() %s is not a file or it does not exist.'; switch ($node->type) { case Include_::TYPE_REQUIRE: @@ -132,18 +145,33 @@ private function getErrorMessage(Include_ $node, string $filePath): IdentifierRu } /** - * @return array + * @return list */ - private function resolveFilePaths(Include_ $node, Scope $scope): array + private function resolveFilePaths(Expr $expr, Scope $scope, bool &$magicDirFallback): array { - $paths = []; - $type = $scope->getType($node->expr); - $constantStrings = $type->getConstantStrings(); + $magicDirFallback = false; - foreach ($constantStrings as $constantString) { - $paths[] = $constantString->getValue(); + if (!$expr instanceof Expr\BinaryOp\Concat) { + return $scope->getType($expr)->getConstantStrings(); } + if ($expr->left instanceof Node\Scalar\MagicConst\Dir) { + $magicDirFallback = true; + + $paths = []; + foreach ($scope->getType($expr->right)->getConstantStrings() as $constantString) { + $paths[] = new ConstantStringType(dirname($scope->getFile()) . $constantString->getValue()); + } + return $paths; + } + + $paths = []; + $rightPaths = $this->resolveFilePaths($expr->right, $scope, $magicDirFallback); + foreach ($this->resolveFilePaths($expr->left, $scope, $magicDirFallback) as $left) { + foreach ($rightPaths as $rightPath) { + $paths[] = new ConstantStringType($left->getValue() . $rightPath->getValue()); + } + } return $paths; } diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php new file mode 100644 index 0000000000..311915eaa7 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php @@ -0,0 +1,52 @@ + + */ +class RequireFileExistsRuleNoConstantPathTest extends RuleTestCase +{ + + private string $currentWorkingDirectory = __DIR__ . '/../'; + + protected function getRule(): Rule + { + return new RequireFileExistsRule( + $this->currentWorkingDirectory, + self::getContainer()->getByType(ExprPrinter::class), + ); + } + + public function testBug12203NoConstantPath(): void + { + $this->analyse([__DIR__ . '/data/bug-12203.php'], [ + [ + 'Path in require_once() "../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 5, + ], + [ + "Path in require_once() __DIR__ . '/../bug-12203-sure-does-not-exist.php' is not a file or it does not exist.", + 6, + ], + [ + "Path in require_once() __DIR__ . '/' . \$path . '/' . \$file is not a file or it does not exist.", + 10, + ], + [ + 'Path in require_once() __DIR__ . "{$path}/{$file}" is not a file or it does not exist.', + 12, + ], + ]); + } + + public function testInFileExists(): void + { + $this->analyse([__DIR__ . '/data/include-in-file-exists.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php index 6599e0defe..c84fdf3a43 100644 --- a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php @@ -2,13 +2,13 @@ namespace PHPStan\Rules\Keywords; +use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function get_include_path; use function implode; use function realpath; use function set_include_path; -use const DIRECTORY_SEPARATOR; use const PATH_SEPARATOR; /** @@ -21,7 +21,10 @@ class RequireFileExistsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new RequireFileExistsRule($this->currentWorkingDirectory); + return new RequireFileExistsRule( + $this->currentWorkingDirectory, + self::getContainer()->getByType(ExprPrinter::class), + ); } public static function getAdditionalConfigFiles(): array @@ -130,9 +133,17 @@ public function testBug12203(): void 5, ], [ - 'Path in require_once() "' . __DIR__ . DIRECTORY_SEPARATOR . 'data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + "Path in require_once() __DIR__ . '/../bug-12203-sure-does-not-exist.php' is not a file or it does not exist.", 6, ], + [ + "Path in require_once() __DIR__ . '/' . \$path . '/' . \$file is not a file or it does not exist.", + 10, + ], + [ + 'Path in require_once() __DIR__ . "{$path}/{$file}" is not a file or it does not exist.', + 12, + ], ]); } diff --git a/tests/PHPStan/Rules/Keywords/data/bug-12203.php b/tests/PHPStan/Rules/Keywords/data/bug-12203.php index d64dcc6ebe..f7cae2aa5c 100644 --- a/tests/PHPStan/Rules/Keywords/data/bug-12203.php +++ b/tests/PHPStan/Rules/Keywords/data/bug-12203.php @@ -4,3 +4,9 @@ require_once '../bug-12203-sure-does-not-exist.php'; require_once __DIR__ . '/../bug-12203-sure-does-not-exist.php'; + +$path = '..'; +$file = 'bug-12203-sure-does-not-exist.php'; +require_once __DIR__ . '/'. $path .'/'. $file; + +require_once __DIR__ . "$path/$file";