5
5
namespace Bellangelo \PHPStanRequireFileExists ;
6
6
7
7
use PhpParser \Node ;
8
- use PhpParser \Node \Expr \ConstFetch ;
9
8
use PhpParser \Node \Expr \Include_ ;
10
- use PhpParser \Node \Expr \BinaryOp \Concat ;
11
- use PhpParser \Node \Expr \ClassConstFetch ;
12
- use PhpParser \Node \Identifier ;
13
- use PhpParser \Node \Name ;
14
- use PhpParser \Node \Scalar \MagicConst \Dir ;
15
- use PhpParser \Node \Scalar \String_ ;
16
9
use PHPStan \Analyser \Scope ;
17
- use PHPStan \Reflection \ReflectionProvider ;
10
+ use PHPStan \File \FileHelper ;
11
+ use PHPStan \Rules \IdentifierRuleError ;
18
12
use PHPStan \Rules \Rule ;
19
13
use PHPStan \Rules \RuleErrorBuilder ;
14
+ use PHPStan \ShouldNotHappenException ;
15
+ use function array_merge ;
16
+ use function dirname ;
17
+ use function explode ;
18
+ use function get_include_path ;
19
+ use function is_file ;
20
+ use function sprintf ;
21
+ use const PATH_SEPARATOR ;
20
22
21
23
/**
22
24
* @implements Rule<Include_>
23
25
*/
24
- class RequireFileExistsRule implements Rule
26
+ final class RequireFileExistsRule implements Rule
25
27
{
26
- private ReflectionProvider $ reflectionProvider ;
28
+ private string $ currentWorkingDirectory ;
27
29
28
- public function __construct (ReflectionProvider $ reflectionProvider )
30
+ public function __construct (string $ currentWorkingDirectory )
29
31
{
30
- $ this ->reflectionProvider = $ reflectionProvider ;
32
+ $ this ->currentWorkingDirectory = $ currentWorkingDirectory ;
31
33
}
32
34
33
35
public function getNodeType (): string
@@ -37,83 +39,104 @@ public function getNodeType(): string
37
39
38
40
public function processNode (Node $ node , Scope $ scope ): array
39
41
{
40
- if ($ node instanceof Include_) {
41
- $ filePath = $ this ->resolveFilePath ($ node ->expr , $ scope );
42
- if ($ filePath !== null && !file_exists ($ filePath )) {
43
- return [
44
- RuleErrorBuilder::message (
45
- sprintf (
46
- 'Included or required file "%s" does not exist. ' ,
47
- $ filePath
48
- )
49
- )->build (),
50
- ];
42
+ $ errors = [];
43
+ $ paths = $ this ->resolveFilePaths ($ node , $ scope );
44
+
45
+ foreach ($ paths as $ path ) {
46
+ if ($ this ->doesFileExist ($ path , $ scope )) {
47
+ continue ;
51
48
}
49
+
50
+ $ errors [] = $ this ->getErrorMessage ($ node , $ path );
52
51
}
53
52
54
- return [] ;
53
+ return $ errors ;
55
54
}
56
55
57
- private function resolveFilePath (Node $ node , Scope $ scope ): ?string
56
+ /**
57
+ * We cannot use `stream_resolve_include_path` as it works based on the calling script.
58
+ * This method simulates the behavior of `stream_resolve_include_path` but for the given scope.
59
+ * The priority order is the following:
60
+ * 1. The current working directory.
61
+ * 2. The include path.
62
+ * 3. The path of the script that is being executed.
63
+ */
64
+ private function doesFileExist (string $ path , Scope $ scope ): bool
58
65
{
59
- if ( $ node instanceof String_) {
60
- return $ node -> value ;
61
- }
62
-
63
- if ( $ node instanceof Dir) {
64
- return dirname ( $ scope -> getFile ());
65
- }
66
-
67
- if ( $ node instanceof ClassConstFetch) {
68
- return $ this -> resolveClassConstant ( $ node );
66
+ $ directories = array_merge (
67
+ [ $ this -> currentWorkingDirectory ],
68
+ explode ( PATH_SEPARATOR , get_include_path ()),
69
+ [ dirname ( $ scope -> getFile ())],
70
+ );
71
+
72
+ foreach ( $ directories as $ directory ) {
73
+ if ( $ this -> doesFileExistForDirectory ( $ path , $ directory )) {
74
+ return true ;
75
+ }
69
76
}
70
77
71
- if ($ node instanceof ConstFetch) {
72
- return $ this ->resolveConstant ($ node );
73
- }
78
+ return false ;
79
+ }
74
80
75
- if ($ node instanceof Concat) {
76
- $ left = $ this ->resolveFilePath ($ node ->left , $ scope );
77
- $ right = $ this ->resolveFilePath ($ node ->right , $ scope );
78
- if ($ left !== null && $ right !== null ) {
79
- return $ left . $ right ;
80
- }
81
- }
81
+ private function doesFileExistForDirectory (string $ path , string $ workingDirectory ): bool
82
+ {
83
+ $ fileHelper = new FileHelper ($ workingDirectory );
84
+ $ normalisedPath = $ fileHelper ->normalizePath ($ path );
85
+ $ absolutePath = $ fileHelper ->absolutizePath ($ normalisedPath );
82
86
83
- return null ;
87
+ return is_file ( $ absolutePath ) ;
84
88
}
85
89
86
- private function resolveClassConstant ( ClassConstFetch $ node ): ? string
90
+ private function getErrorMessage ( Include_ $ node, string $ filePath ): IdentifierRuleError
87
91
{
88
- if ($ node ->class instanceof Name && $ node ->name instanceof Identifier) {
89
- $ className = (string ) $ node ->class ;
90
- $ constantName = $ node ->name ->toString ();
91
-
92
- if ($ this ->reflectionProvider ->hasClass ($ className )) {
93
- $ classReflection = $ this ->reflectionProvider ->getClass ($ className );
94
- if ($ classReflection ->hasConstant ($ constantName )) {
95
- $ constantReflection = $ classReflection ->getConstant ($ constantName );
96
- $ constantValue = $ constantReflection ->getValue ();
97
- if (is_string ($ constantValue )) {
98
- return $ constantValue ;
99
- }
100
- }
101
- }
92
+ $ message = 'Path in %s() "%s" is not a file or it does not exist. ' ;
93
+
94
+ switch ($ node ->type ) {
95
+ case Include_::TYPE_REQUIRE :
96
+ $ type = 'require ' ;
97
+ $ identifierType = 'require ' ;
98
+ break ;
99
+ case Include_::TYPE_REQUIRE_ONCE :
100
+ $ type = 'require_once ' ;
101
+ $ identifierType = 'requireOnce ' ;
102
+ break ;
103
+ case Include_::TYPE_INCLUDE :
104
+ $ type = 'include ' ;
105
+ $ identifierType = 'include ' ;
106
+ break ;
107
+ case Include_::TYPE_INCLUDE_ONCE :
108
+ $ type = 'include_once ' ;
109
+ $ identifierType = 'includeOnce ' ;
110
+ break ;
111
+ default :
112
+ throw new ShouldNotHappenException ('Rule should have already validated the node type. ' );
102
113
}
103
- return null ;
114
+
115
+ $ identifier = sprintf ('%s.fileNotFound ' , $ identifierType );
116
+
117
+ return RuleErrorBuilder::message (
118
+ sprintf (
119
+ $ message ,
120
+ $ type ,
121
+ $ filePath ,
122
+ ),
123
+ )->identifier ($ identifier )->build ();
104
124
}
105
125
106
- private function resolveConstant (ConstFetch $ node ): ?string
126
+ /**
127
+ * @return array<string>
128
+ */
129
+ private function resolveFilePaths (Include_ $ node , Scope $ scope ): array
107
130
{
108
- if ($ node ->name instanceof Name) {
109
- $ constantName = (string ) $ node ->name ;
110
- if (defined ($ constantName )) {
111
- $ constantValue = constant ($ constantName );
112
- if (is_string ($ constantValue )) {
113
- return $ constantValue ;
114
- }
115
- }
131
+ $ paths = [];
132
+ $ type = $ scope ->getType ($ node ->expr );
133
+ $ constantStrings = $ type ->getConstantStrings ();
134
+
135
+ foreach ($ constantStrings as $ constantString ) {
136
+ $ paths [] = $ constantString ->getValue ();
116
137
}
117
- return null ;
138
+
139
+ return $ paths ;
118
140
}
119
- }
141
+
142
+ }
0 commit comments