Skip to content

Commit bbbf310

Browse files
committed
Fix "Differing behaviors when requiring or including relatively vs using __DIR__"
1 parent 0b41069 commit bbbf310

4 files changed

Lines changed: 110 additions & 13 deletions

File tree

src/Rules/Keywords/RequireFileExistsRule.php

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44

55
use PhpParser\Node;
66
use PhpParser\Node\Arg;
7+
use PhpParser\Node\Expr;
78
use PhpParser\Node\Expr\FuncCall;
89
use PhpParser\Node\Expr\Include_;
910
use PhpParser\Node\Name\FullyQualified;
1011
use PHPStan\Analyser\Scope;
1112
use PHPStan\DependencyInjection\AutowiredParameter;
1213
use PHPStan\DependencyInjection\RegisteredRule;
1314
use PHPStan\File\FileHelper;
15+
use PHPStan\Node\Printer\ExprPrinter;
1416
use PHPStan\Rules\IdentifierRuleError;
1517
use PHPStan\Rules\Rule;
1618
use PHPStan\Rules\RuleErrorBuilder;
1719
use PHPStan\ShouldNotHappenException;
20+
use PHPStan\Type\Constant\ConstantStringType;
1821
use function array_merge;
1922
use function dirname;
2023
use function explode;
@@ -33,6 +36,7 @@ final class RequireFileExistsRule implements Rule
3336
public function __construct(
3437
#[AutowiredParameter]
3538
private string $currentWorkingDirectory,
39+
private ExprPrinter $exprPrinter,
3640
)
3741
{
3842
}
@@ -49,14 +53,23 @@ public function processNode(Node $node, Scope $scope): array
4953
}
5054

5155
$errors = [];
52-
$paths = $this->resolveFilePaths($node, $scope);
56+
$usedMagicDirFallback = false;
57+
$paths = $this->resolveFilePaths($node->expr, $scope, $usedMagicDirFallback);
5358

5459
foreach ($paths as $path) {
60+
$path = $path->getValue();
61+
5562
if ($this->doesFileExist($path, $scope)) {
5663
continue;
5764
}
5865

59-
$errors[] = $this->getErrorMessage($node, $path);
66+
if ($usedMagicDirFallback) {
67+
$pathExpr = $this->exprPrinter->printExpr($node->expr);
68+
} else {
69+
$pathExpr = '"' . $path . '"';
70+
}
71+
72+
$errors[] = $this->getErrorMessage($node, $pathExpr);
6073
}
6174

6275
return $errors;
@@ -97,7 +110,7 @@ private function doesFileExistForDirectory(string $path, string $workingDirector
97110

98111
private function getErrorMessage(Include_ $node, string $filePath): IdentifierRuleError
99112
{
100-
$message = 'Path in %s() "%s" is not a file or it does not exist.';
113+
$message = 'Path in %s() %s is not a file or it does not exist.';
101114

102115
switch ($node->type) {
103116
case Include_::TYPE_REQUIRE:
@@ -132,18 +145,33 @@ private function getErrorMessage(Include_ $node, string $filePath): IdentifierRu
132145
}
133146

134147
/**
135-
* @return array<string>
148+
* @return list<ConstantStringType>
136149
*/
137-
private function resolveFilePaths(Include_ $node, Scope $scope): array
150+
private function resolveFilePaths(Expr $expr, Scope $scope, bool &$magicDirFallback): array
138151
{
139-
$paths = [];
140-
$type = $scope->getType($node->expr);
141-
$constantStrings = $type->getConstantStrings();
152+
$magicDirFallback = false;
142153

143-
foreach ($constantStrings as $constantString) {
144-
$paths[] = $constantString->getValue();
154+
if (!$expr instanceof Expr\BinaryOp\Concat) {
155+
return $scope->getType($expr)->getConstantStrings();
145156
}
146157

158+
if ($expr->left instanceof Node\Scalar\MagicConst\Dir) {
159+
$magicDirFallback = true;
160+
161+
$paths = [];
162+
foreach ($scope->getType($expr->right)->getConstantStrings() as $constantString) {
163+
$paths[] = new ConstantStringType(dirname($scope->getFile()) . $constantString->getValue());
164+
}
165+
return $paths;
166+
}
167+
168+
$paths = [];
169+
$rightPaths = $this->resolveFilePaths($expr->right, $scope, $magicDirFallback);
170+
foreach ($this->resolveFilePaths($expr->left, $scope, $magicDirFallback) as $left) {
171+
foreach ($rightPaths as $rightPath) {
172+
$paths[] = new ConstantStringType($left->getValue() . $rightPath->getValue());
173+
}
174+
}
147175
return $paths;
148176
}
149177

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Keywords;
4+
5+
use PHPStan\Node\Printer\ExprPrinter;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<RequireFileExistsRule>
11+
*/
12+
class RequireFileExistsRuleNoConstantPathTest extends RuleTestCase
13+
{
14+
15+
private string $currentWorkingDirectory = __DIR__ . '/../';
16+
17+
protected function getRule(): Rule
18+
{
19+
return new RequireFileExistsRule(
20+
$this->currentWorkingDirectory,
21+
self::getContainer()->getByType(ExprPrinter::class),
22+
);
23+
}
24+
25+
public function testBug12203NoConstantPath(): void
26+
{
27+
$this->analyse([__DIR__ . '/data/bug-12203.php'], [
28+
[
29+
'Path in require_once() "../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.',
30+
5,
31+
],
32+
[
33+
"Path in require_once() __DIR__ . '/../bug-12203-sure-does-not-exist.php' is not a file or it does not exist.",
34+
6,
35+
],
36+
[
37+
"Path in require_once() __DIR__ . '/' . \$path . '/' . \$file is not a file or it does not exist.",
38+
10,
39+
],
40+
[
41+
'Path in require_once() __DIR__ . "{$path}/{$file}" is not a file or it does not exist.',
42+
12,
43+
],
44+
]);
45+
}
46+
47+
public function testInFileExists(): void
48+
{
49+
$this->analyse([__DIR__ . '/data/include-in-file-exists.php'], []);
50+
}
51+
52+
}

tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
namespace PHPStan\Rules\Keywords;
44

5+
use PHPStan\Node\Printer\ExprPrinter;
56
use PHPStan\Rules\Rule;
67
use PHPStan\Testing\RuleTestCase;
78
use function get_include_path;
89
use function implode;
910
use function realpath;
1011
use function set_include_path;
11-
use const DIRECTORY_SEPARATOR;
1212
use const PATH_SEPARATOR;
1313

1414
/**
@@ -21,7 +21,10 @@ class RequireFileExistsRuleTest extends RuleTestCase
2121

2222
protected function getRule(): Rule
2323
{
24-
return new RequireFileExistsRule($this->currentWorkingDirectory);
24+
return new RequireFileExistsRule(
25+
$this->currentWorkingDirectory,
26+
self::getContainer()->getByType(ExprPrinter::class),
27+
);
2528
}
2629

2730
public static function getAdditionalConfigFiles(): array
@@ -130,9 +133,17 @@ public function testBug12203(): void
130133
5,
131134
],
132135
[
133-
'Path in require_once() "' . __DIR__ . DIRECTORY_SEPARATOR . 'data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.',
136+
"Path in require_once() __DIR__ . '/../bug-12203-sure-does-not-exist.php' is not a file or it does not exist.",
134137
6,
135138
],
139+
[
140+
"Path in require_once() __DIR__ . '/' . \$path . '/' . \$file is not a file or it does not exist.",
141+
10,
142+
],
143+
[
144+
'Path in require_once() __DIR__ . "{$path}/{$file}" is not a file or it does not exist.',
145+
12,
146+
],
136147
]);
137148
}
138149

tests/PHPStan/Rules/Keywords/data/bug-12203.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@
44

55
require_once '../bug-12203-sure-does-not-exist.php';
66
require_once __DIR__ . '/../bug-12203-sure-does-not-exist.php';
7+
8+
$path = '..';
9+
$file = 'bug-12203-sure-does-not-exist.php';
10+
require_once __DIR__ . '/'. $path .'/'. $file;
11+
12+
require_once __DIR__ . "$path/$file";

0 commit comments

Comments
 (0)