Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions src/Rules/Keywords/RequireFileExistsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@

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 @@ -33,6 +36,7 @@ final class RequireFileExistsRule implements Rule
public function __construct(
#[AutowiredParameter]
private string $currentWorkingDirectory,
private ExprPrinter $exprPrinter,
)
{
}
Expand All @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -132,18 +145,33 @@ 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(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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?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,
],
[
"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'], []);
}

}
17 changes: 14 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,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.',

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

Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Keywords/data/bug-12203.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading