33namespace PHPStan \Rules \Keywords ;
44
55use PhpParser \Node ;
6+ use PhpParser \Node \Arg ;
7+ use PhpParser \Node \Expr ;
8+ use PhpParser \Node \Expr \FuncCall ;
69use PhpParser \Node \Expr \Include_ ;
10+ use PhpParser \Node \Name \FullyQualified ;
711use PHPStan \Analyser \Scope ;
812use PHPStan \DependencyInjection \AutowiredParameter ;
913use PHPStan \DependencyInjection \RegisteredRule ;
1014use PHPStan \File \FileHelper ;
15+ use PHPStan \Node \Printer \ExprPrinter ;
1116use PHPStan \Rules \IdentifierRuleError ;
1217use PHPStan \Rules \Rule ;
1318use PHPStan \Rules \RuleErrorBuilder ;
1419use PHPStan \ShouldNotHappenException ;
20+ use PHPStan \Type \Constant \ConstantStringType ;
1521use function array_merge ;
1622use function dirname ;
1723use function explode ;
@@ -30,6 +36,7 @@ final class RequireFileExistsRule implements Rule
3036 public function __construct (
3137 #[AutowiredParameter]
3238 private string $ currentWorkingDirectory ,
39+ private ExprPrinter $ exprPrinter ,
3340 )
3441 {
3542 }
@@ -41,15 +48,28 @@ public function getNodeType(): string
4148
4249 public function processNode (Node $ node , Scope $ scope ): array
4350 {
51+ if ($ this ->isInFileExists ($ node , $ scope )) {
52+ return [];
53+ }
54+
4455 $ errors = [];
45- $ paths = $ this ->resolveFilePaths ($ node , $ scope );
56+ $ usedMagicDirFallback = false ;
57+ $ paths = $ this ->resolveFilePaths ($ node ->expr , $ scope , $ usedMagicDirFallback );
4658
4759 foreach ($ paths as $ path ) {
60+ $ path = $ path ->getValue ();
61+
4862 if ($ this ->doesFileExist ($ path , $ scope )) {
4963 continue ;
5064 }
5165
52- $ 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 );
5373 }
5474
5575 return $ errors ;
@@ -90,7 +110,7 @@ private function doesFileExistForDirectory(string $path, string $workingDirector
90110
91111 private function getErrorMessage (Include_ $ node , string $ filePath ): IdentifierRuleError
92112 {
93- $ 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. ' ;
94114
95115 switch ($ node ->type ) {
96116 case Include_::TYPE_REQUIRE :
@@ -125,19 +145,49 @@ private function getErrorMessage(Include_ $node, string $filePath): IdentifierRu
125145 }
126146
127147 /**
128- * @return array<string >
148+ * @return list<ConstantStringType >
129149 */
130- private function resolveFilePaths (Include_ $ node , Scope $ scope ): array
150+ private function resolveFilePaths (Expr $ expr , Scope $ scope, bool & $ magicDirFallback ): array
131151 {
132- $ paths = [];
133- $ type = $ scope ->getType ($ node ->expr );
134- $ constantStrings = $ type ->getConstantStrings ();
152+ $ magicDirFallback = false ;
135153
136- foreach ( $ constantStrings as $ constantString ) {
137- $ paths [] = $ constantString -> getValue ();
154+ if (! $ expr instanceof Expr \ BinaryOp \Concat ) {
155+ return $ scope -> getType ( $ expr )-> getConstantStrings ();
138156 }
139157
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+ }
140175 return $ paths ;
141176 }
142177
178+ private function isInFileExists (Include_ $ node , Scope $ scope ): bool
179+ {
180+ foreach (['file_exists ' , 'is_file ' ] as $ funcName ) {
181+ $ expr = new FuncCall (new FullyQualified ($ funcName ), [
182+ new Arg ($ node ->expr ),
183+ ]);
184+
185+ if ($ scope ->getType ($ expr )->isTrue ()->yes ()) {
186+ return true ;
187+ }
188+ }
189+
190+ return false ;
191+ }
192+
143193}
0 commit comments