-
Notifications
You must be signed in to change notification settings - Fork 512
Rule to check if required file exists #3294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c1f3c6f
fb165ba
749414a
deefe60
be18ba6
24a9650
7c3f635
4ff25c8
1625225
496c541
03f15fa
4889f9e
0e581eb
1520ea9
960c71a
aff4b04
2814cf8
4acc555
4680c6e
67ce86b
31fa5e0
50dde41
9f353d8
bfa3908
2369559
a2f0e6e
f20f26f
9896884
54f7779
b723cf2
9035154
4c85ed7
2bed45a
76b6f4f
8ea25b8
f8db0ec
032965a
31a86c8
6abc7ec
71ee779
3df4d41
95e5888
4858ceb
67ee4d8
4e049ea
71d23e4
525a393
e861017
0743c06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Keywords; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr\Include_; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\File\FileHelper; | ||
use PHPStan\Rules\IdentifierRuleError; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\ShouldNotHappenException; | ||
use function array_merge; | ||
use function dirname; | ||
use function explode; | ||
use function get_include_path; | ||
use function is_file; | ||
use function sprintf; | ||
use const PATH_SEPARATOR; | ||
|
||
/** | ||
* @implements Rule<Include_> | ||
*/ | ||
final class RequireFileExistsRule implements Rule | ||
{ | ||
|
||
public function __construct(private string $currentWorkingDirectory) | ||
{ | ||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return Include_::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
$errors = []; | ||
$paths = $this->resolveFilePaths($node, $scope); | ||
|
||
foreach ($paths as $path) { | ||
if ($this->doesFileExist($path, $scope)) { | ||
continue; | ||
} | ||
|
||
$errors[] = $this->getErrorMessage($node, $path); | ||
} | ||
|
||
return $errors; | ||
} | ||
|
||
/** | ||
* We cannot use `stream_resolve_include_path` as it works based on the calling script. | ||
* This method simulates the behavior of `stream_resolve_include_path` but for the given scope. | ||
* The priority order is the following: | ||
* 1. The current working directory. | ||
* 2. The include path. | ||
* 3. The path of the script that is being executed. | ||
*/ | ||
private function doesFileExist(string $path, Scope $scope): bool | ||
{ | ||
$directories = array_merge( | ||
[$this->currentWorkingDirectory], | ||
explode(PATH_SEPARATOR, get_include_path()), | ||
[dirname($scope->getFile())], | ||
); | ||
|
||
foreach ($directories as $directory) { | ||
if ($this->doesFileExistForDirectory($path, $directory)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private function doesFileExistForDirectory(string $path, string $workingDirectory): bool | ||
{ | ||
$fileHelper = new FileHelper($workingDirectory); | ||
$normalisedPath = $fileHelper->normalizePath($path); | ||
$absolutePath = $fileHelper->absolutizePath($normalisedPath); | ||
|
||
return is_file($absolutePath); | ||
} | ||
|
||
private function getErrorMessage(Include_ $node, string $filePath): IdentifierRuleError | ||
{ | ||
$message = 'Path in %s() "%s" is not a file or it does not exist.'; | ||
|
||
switch ($node->type) { | ||
case Include_::TYPE_REQUIRE: | ||
$type = 'require'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One last simple change: I worry the identifier cannot be statically inferred here because of Please assign Then I'll merge this. Thank you for the effort! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in e861017. Shouldn't this also run as a quality check before merging a PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, feel free to contribute this rule 😊 |
||
$identifierType = 'require'; | ||
break; | ||
case Include_::TYPE_REQUIRE_ONCE: | ||
$type = 'require_once'; | ||
$identifierType = 'requireOnce'; | ||
break; | ||
case Include_::TYPE_INCLUDE: | ||
$type = 'include'; | ||
$identifierType = 'include'; | ||
break; | ||
case Include_::TYPE_INCLUDE_ONCE: | ||
$type = 'include_once'; | ||
$identifierType = 'includeOnce'; | ||
break; | ||
default: | ||
throw new ShouldNotHappenException('Rule should have already validated the node type.'); | ||
} | ||
|
||
$identifier = sprintf('%s.fileNotFound', $identifierType); | ||
|
||
return RuleErrorBuilder::message( | ||
sprintf( | ||
$message, | ||
$type, | ||
$filePath, | ||
), | ||
)->identifier($identifier)->build(); | ||
} | ||
|
||
/** | ||
* @return array<string> | ||
*/ | ||
private function resolveFilePaths(Include_ $node, Scope $scope): array | ||
{ | ||
$paths = []; | ||
$type = $scope->getType($node->expr); | ||
$constantStrings = $type->getConstantStrings(); | ||
|
||
foreach ($constantStrings as $constantString) { | ||
$paths[] = $constantString->getValue(); | ||
} | ||
|
||
return $paths; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Keywords; | ||
|
||
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 PATH_SEPARATOR; | ||
|
||
/** | ||
* @extends RuleTestCase<RequireFileExistsRule> | ||
*/ | ||
class RequireFileExistsRuleTest extends RuleTestCase | ||
{ | ||
|
||
private RequireFileExistsRule $rule; | ||
|
||
public function setUp(): void | ||
{ | ||
parent::setUp(); | ||
|
||
$this->rule = $this->getDefaultRule(); | ||
} | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return $this->rule; | ||
} | ||
|
||
public static function getAdditionalConfigFiles(): array | ||
{ | ||
return [ | ||
__DIR__ . '/../../Analyser/usePathConstantsAsConstantString.neon', | ||
]; | ||
} | ||
|
||
private function getDefaultRule(): RequireFileExistsRule | ||
{ | ||
return new RequireFileExistsRule(__DIR__ . '/../'); | ||
} | ||
|
||
public function testBasicCase(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/require-file-simple-case.php'], [ | ||
[ | ||
'Path in include() "a-file-that-does-not-exist.php" is not a file or it does not exist.', | ||
11, | ||
], | ||
[ | ||
'Path in include_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', | ||
12, | ||
], | ||
[ | ||
'Path in require() "a-file-that-does-not-exist.php" is not a file or it does not exist.', | ||
13, | ||
], | ||
[ | ||
'Path in require_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', | ||
14, | ||
], | ||
]); | ||
} | ||
|
||
public function testFileDoesNotExistConditionally(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/require-file-conditionally.php'], [ | ||
[ | ||
'Path in include() "a-file-that-does-not-exist.php" is not a file or it does not exist.', | ||
9, | ||
], | ||
[ | ||
'Path in include_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', | ||
10, | ||
], | ||
[ | ||
'Path in require() "a-file-that-does-not-exist.php" is not a file or it does not exist.', | ||
11, | ||
], | ||
[ | ||
'Path in require_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', | ||
12, | ||
], | ||
]); | ||
} | ||
|
||
public function testRelativePath(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/require-file-relative-path.php'], [ | ||
[ | ||
'Path in include() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', | ||
8, | ||
], | ||
[ | ||
'Path in include_once() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', | ||
9, | ||
], | ||
[ | ||
'Path in require() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', | ||
10, | ||
], | ||
[ | ||
'Path in require_once() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', | ||
11, | ||
], | ||
]); | ||
} | ||
|
||
public function testRelativePathWithIncludePath(): void | ||
{ | ||
$includePaths = [realpath(__DIR__)]; | ||
$includePaths[] = get_include_path(); | ||
|
||
set_include_path(implode(PATH_SEPARATOR, $includePaths)); | ||
|
||
try { | ||
$this->analyse([__DIR__ . '/data/require-file-relative-path.php'], []); | ||
} finally { | ||
set_include_path($includePaths[1]); | ||
} | ||
} | ||
|
||
public function testRelativePathWithSameWorkingDirectory(): void | ||
{ | ||
$this->rule = new RequireFileExistsRule(__DIR__); | ||
|
||
try { | ||
$this->analyse([__DIR__ . '/data/require-file-relative-path.php'], []); | ||
} finally { | ||
$this->rule = $this->getDefaultRule(); | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php declare(strict_types=1); | ||
|
||
$path = __DIR__ . '/include-me-to-prove-you-work.txt'; | ||
|
||
if (rand(0,1)) { | ||
$path = 'a-file-that-does-not-exist.php'; | ||
} | ||
|
||
include $path; | ||
include_once $path; | ||
require $path; | ||
require_once $path; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php declare(strict_types=1); | ||
|
||
include 'include-me-to-prove-you-work.txt'; | ||
include_once 'include-me-to-prove-you-work.txt'; | ||
require 'include-me-to-prove-you-work.txt'; | ||
require_once 'include-me-to-prove-you-work.txt'; | ||
|
||
include 'data/include-me-to-prove-you-work.txt'; | ||
include_once 'data/include-me-to-prove-you-work.txt'; | ||
require 'data/include-me-to-prove-you-work.txt'; | ||
require_once 'data/include-me-to-prove-you-work.txt'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php declare(strict_types=1); | ||
|
||
$fileThatExists = __DIR__ . '/include-me-to-prove-you-work.txt'; | ||
$fileThatDoesNotExist = 'a-file-that-does-not-exist.php'; | ||
|
||
include $fileThatExists; | ||
include_once $fileThatExists; | ||
require $fileThatExists; | ||
require_once $fileThatExists; | ||
|
||
include $fileThatDoesNotExist; | ||
include_once $fileThatDoesNotExist; | ||
require $fileThatDoesNotExist; | ||
require_once $fileThatDoesNotExist; |
Uh oh!
There was an error while loading. Please reload this page.