Skip to content
66 changes: 55 additions & 11 deletions src/Rules/Keywords/RequireFileExistsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +36,7 @@ final class RequireFileExistsRule implements Rule
public function __construct(
#[AutowiredParameter]
private string $currentWorkingDirectory,
private ExprPrinter $exprPrinter,
)
{
}
Expand All @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -125,19 +145,43 @@ private function getErrorMessage(Include_ $node, string $filePath): IdentifierRu
}

/**
* @return array<string>
* @return list<ConstantStringType>
*/
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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Keywords;

use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<RequireFileExistsRule>
*/
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same test as in tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php but without usePathConstantsAsConstantString: true defined via neon-config

{
$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'], []);
}

}
14 changes: 11 additions & 3 deletions tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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
Expand Down Expand Up @@ -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.',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed the reported error path to the actual expr in the source code, so we don't report a error containing a absolute file path, which cannot be properly used in baselines, because it likely differs between different computers

"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'], []);
}

}
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Keywords/data/include-in-file-exists.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require __DIR__ . '/../vendor/autoload.php';
}
Comment on lines +3 to +5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in addition to the initial issue, this PR also prevents a error when a given path was checked with file_exists/is_file before require/include


if (is_file(__DIR__ . '/../vendor/autoload.php')) {
require __DIR__ . '/../vendor/autoload.php';
}
Loading