diff --git a/src/Rules/Keywords/RequireFileExistsRule.php b/src/Rules/Keywords/RequireFileExistsRule.php index b0d4def309..0a3e6e35a5 100644 --- a/src/Rules/Keywords/RequireFileExistsRule.php +++ b/src/Rules/Keywords/RequireFileExistsRule.php @@ -3,15 +3,21 @@ namespace PHPStan\Rules\Keywords; 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; use PHPStan\Analyser\Scope; 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; @@ -30,6 +36,7 @@ final class RequireFileExistsRule implements Rule public function __construct( #[AutowiredParameter] private string $currentWorkingDirectory, + private ExprPrinter $exprPrinter, ) { } @@ -41,15 +48,28 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + if ($this->isInFileExists($node, $scope)) { + return []; + } + $errors = []; - $paths = $this->resolveFilePaths($node, $scope); + $usedMagicDirFallback = false; + $paths = $this->resolveFilePaths($node, $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; @@ -90,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: @@ -125,19 +145,43 @@ private function getErrorMessage(Include_ $node, string $filePath): IdentifierRu } /** - * @return array + * @return list */ - private function resolveFilePaths(Include_ $node, Scope $scope): array + private function resolveFilePaths(Include_ $node, Scope $scope, bool &$magicDirFallback): array + { + $magicDirFallback = false; + $expr = $node->expr; + + 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; + } + + return $scope->getType($expr)->getConstantStrings(); + } + + private function isInFileExists(Include_ $node, Scope $scope): bool { - $paths = []; - $type = $scope->getType($node->expr); - $constantStrings = $type->getConstantStrings(); + foreach (['file_exists', 'is_file'] as $funcName) { + $expr = new FuncCall(new FullyQualified($funcName), [ + new Arg($node->expr), + ]); - foreach ($constantStrings as $constantString) { - $paths[] = $constantString->getValue(); + if ($scope->getType($expr)->isTrue()->yes()) { + return true; + } } - return $paths; + return false; } } diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php new file mode 100644 index 0000000000..b967cdbe56 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleNoConstantPathTest.php @@ -0,0 +1,44 @@ + + */ +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, + ], + ]); + } + + 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 732819b506..ee7835c51a 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,10 +133,15 @@ 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, ], ]); } + public function testInFileExists(): void + { + $this->analyse([__DIR__ . '/data/include-in-file-exists.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Keywords/data/include-in-file-exists.php b/tests/PHPStan/Rules/Keywords/data/include-in-file-exists.php new file mode 100644 index 0000000000..74e271d330 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/include-in-file-exists.php @@ -0,0 +1,9 @@ +