From ed962cf32358c79b4f6f29a1f2795289df879ec3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 11 Jul 2026 09:13:33 +0200 Subject: [PATCH] Fix 'path ... does not exist' false-positive for expr containing DIRECTORY_SEPARATOR --- src/File/FileHelper.php | 8 ++++++-- src/Rules/Keywords/RequireFileExistsRule.php | 11 +++++++++-- .../RequireFileExistsRuleNoConstantPathTest.php | 10 ++++++++++ .../Rules/Keywords/RequireFileExistsRuleTest.php | 10 ++++++++++ tests/PHPStan/Rules/Keywords/data/bug-12203.php | 3 +++ .../Rules/Keywords/data/require-file-simple-case.php | 2 ++ 6 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/File/FileHelper.php b/src/File/FileHelper.php index 84def9a8f2c..fdfd4cc0653 100644 --- a/src/File/FileHelper.php +++ b/src/File/FileHelper.php @@ -90,8 +90,7 @@ public function normalizePath(string $originalPath, string $directorySeparator = $path = $originalPath; } - $path = str_replace(['\\', '//', '///', '////'], '/', $path); - + $path = $this->normalizeSeparator($path); $pathRoot = str_starts_with($path, '/') ? $directorySeparator : ''; $pathParts = explode('/', trim($path, '/')); @@ -113,4 +112,9 @@ public function normalizePath(string $originalPath, string $directorySeparator = return self::$normalizedPathsCache[$originalPath][$directorySeparator] = ($scheme !== null ? $scheme . '://' : '') . $pathRoot . implode($directorySeparator, $normalizedPathParts); } + public function normalizeSeparator(string $path): string + { + return str_replace(['\\', '//', '///', '////'], '/', $path); + } + } diff --git a/src/Rules/Keywords/RequireFileExistsRule.php b/src/Rules/Keywords/RequireFileExistsRule.php index 9e5fa1eb571..4ea63f57159 100644 --- a/src/Rules/Keywords/RequireFileExistsRule.php +++ b/src/Rules/Keywords/RequireFileExistsRule.php @@ -52,6 +52,7 @@ public function __construct( private ExprPrinter $exprPrinter, #[AutowiredParameter(ref: '%featureToggles.magicDirInInclude%')] private bool $checkMagicDirInInclude, + private FileHelper $fileHelper, ) { } @@ -188,10 +189,16 @@ private function resolveFilePaths(Expr $expr, Scope $scope, bool &$magicDirFallb $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()); + $normalizedPath = $this->fileHelper->normalizeSeparator($left->getValue() . $rightPath->getValue()); + $paths[$normalizedPath] = $normalizedPath; } } - return $paths; + + $list = []; + foreach ($paths as $path) { + $list[] = new ConstantStringType($path); + } + return $list; } private function isInFileExists(Include_ $node, Scope $scope): bool diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php index 6d2f3625ea5..f953876bae3 100644 --- a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Keywords; +use PHPStan\File\FileHelper; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -20,6 +21,7 @@ protected function getRule(): Rule $this->currentWorkingDirectory, self::getContainer()->getByType(ExprPrinter::class), true, + self::getContainer()->getByType(FileHelper::class), ); } @@ -42,6 +44,14 @@ public function testBug12203NoConstantPath(): void 'Path in require_once() __DIR__ . "{$path}/{$file}" is not a file or it does not exist.', 12, ], + [ + 'Path in require_once() __DIR__ . DIRECTORY_SEPARATOR . $path . \'/\' . $file is not a file or it does not exist.', + 14, + ], + [ + 'Path in require_once() "../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 15, + ], ]); } diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php index c8b989d2ff3..7cad2911c20 100644 --- a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Keywords; +use PHPStan\File\FileHelper; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -25,6 +26,7 @@ protected function getRule(): Rule $this->currentWorkingDirectory, self::getContainer()->getByType(ExprPrinter::class), true, + self::getContainer()->getByType(FileHelper::class), ); } @@ -145,6 +147,14 @@ public function testBug12203(): void 'Path in require_once() __DIR__ . "{$path}/{$file}" is not a file or it does not exist.', 12, ], + [ + 'Path in require_once() __DIR__ . DIRECTORY_SEPARATOR . $path . \'/\' . $file is not a file or it does not exist.', + 14, + ], + [ + 'Path in require_once() "../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 15, + ], ]); } diff --git a/tests/PHPStan/Rules/Keywords/data/bug-12203.php b/tests/PHPStan/Rules/Keywords/data/bug-12203.php index f7cae2aa5c3..29274540176 100644 --- a/tests/PHPStan/Rules/Keywords/data/bug-12203.php +++ b/tests/PHPStan/Rules/Keywords/data/bug-12203.php @@ -10,3 +10,6 @@ require_once __DIR__ . '/'. $path .'/'. $file; require_once __DIR__ . "$path/$file"; + +require_once __DIR__ . DIRECTORY_SEPARATOR. $path .'/'. $file; +require_once '..'. \DIRECTORY_SEPARATOR .'bug-12203-sure-does-not-exist.php'; diff --git a/tests/PHPStan/Rules/Keywords/data/require-file-simple-case.php b/tests/PHPStan/Rules/Keywords/data/require-file-simple-case.php index a0d03a39c2b..5eed2837bcb 100644 --- a/tests/PHPStan/Rules/Keywords/data/require-file-simple-case.php +++ b/tests/PHPStan/Rules/Keywords/data/require-file-simple-case.php @@ -12,3 +12,5 @@ include_once $fileThatDoesNotExist; require $fileThatDoesNotExist; require_once $fileThatDoesNotExist; + +$fileThatExists = __DIR__ . \DIRECTORY_SEPARATOR . 'include-me-to-prove-you-work.txt';