diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index d28a3225de..fa39b3cd59 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -936,7 +936,9 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - return $specifiedTypes; + return $specifiedTypes + ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->setRootExpr($specifiedTypes->getRootExpr()); } } } diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 77f5969ae7..1f661f5a98 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -328,7 +328,9 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - return $specifiedTypes; + return $specifiedTypes + ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->setRootExpr($specifiedTypes->getRootExpr()); } } } diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index e24683ac8f..872fac5c79 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -429,7 +429,9 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - return $specifiedTypes; + return $specifiedTypes + ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->setRootExpr($specifiedTypes->getRootExpr()); } } } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 55138f3de6..f5599c6394 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -350,6 +350,13 @@ private function getSpecifiedType( } } foreach ($sureTypes as $sureType) { + if ($sureType[0] === $node) { + // The check's own truthiness carries no information about + // whether the check is redundant; the informative narrowing + // lives in the argument entries. + continue; + } + if (self::isSpecified($typeSpecifierScope, $node, $sureType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; @@ -384,6 +391,13 @@ private function getSpecifiedType( } foreach ($sureNotTypes as $sureNotType) { + if ($sureNotType[0] === $node) { + // The check's own truthiness carries no information about + // whether the check is redundant; the informative narrowing + // lives in the argument entries. + continue; + } + if (self::isSpecified($typeSpecifierScope, $node, $sureNotType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; diff --git a/src/Rules/Keywords/RequireFileExistsRule.php b/src/Rules/Keywords/RequireFileExistsRule.php index 674fc815c1..9e5fa1eb57 100644 --- a/src/Rules/Keywords/RequireFileExistsRule.php +++ b/src/Rules/Keywords/RequireFileExistsRule.php @@ -33,6 +33,19 @@ final class RequireFileExistsRule implements Rule { + /** + * Functions that, when they return true, guarantee the path exists on the + * filesystem, so guarding a require/include with them suppresses the error. + */ + private const FILE_EXISTENCE_FUNCTIONS = [ + 'file_exists', + 'is_file', + 'is_readable', + 'is_writable', + 'is_writeable', + 'is_executable', + ]; + public function __construct( #[AutowiredParameter] private string $currentWorkingDirectory, @@ -183,7 +196,7 @@ private function resolveFilePaths(Expr $expr, Scope $scope, bool &$magicDirFallb private function isInFileExists(Include_ $node, Scope $scope): bool { - foreach (['file_exists', 'is_file'] as $funcName) { + foreach (self::FILE_EXISTENCE_FUNCTIONS as $funcName) { $expr = new FuncCall(new FullyQualified($funcName), [ new Arg($node->expr), ]); diff --git a/tests/PHPStan/Analyser/nsrt/bug-14829.php b/tests/PHPStan/Analyser/nsrt/bug-14829.php new file mode 100644 index 0000000000..99fcc8c4f4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14829.php @@ -0,0 +1,52 @@ +isReadable($path)) { + assertType('true', $c->isReadable($path)); + assertType('non-empty-string', $path); + } +} + +function testStaticMethod(string $path): void +{ + if (Checker::staticIsReadable($path)) { + assertType('true', Checker::staticIsReadable($path)); + assertType('non-empty-string', $path); + } +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index caaaf53050..6514f8619f 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -452,6 +452,17 @@ public function testBug8555(): void $this->analyse([__DIR__ . '/data/bug-8555.php'], []); } + public function testSelfContradiction(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/self-contradiction.php'], [ + [ + 'Result of && is always false.', + 25, + ], + ]); + } + #[RequiresPhp('>= 8.1.0')] public function testBug14807(): void { diff --git a/tests/PHPStan/Rules/Comparison/data/self-contradiction.php b/tests/PHPStan/Rules/Comparison/data/self-contradiction.php new file mode 100644 index 0000000000..47a56712ab --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/self-contradiction.php @@ -0,0 +1,35 @@ +left) && !self::isSubjectNode($comparison->left)) { + return ['subject' => $comparison->right, 'value' => $comparison->left]; + } + + if (!self::isSubjectNode($comparison->left) && self::isSubjectNode($comparison->right)) { + return ['subject' => $comparison->left, 'value' => $comparison->right]; + } + + return null; + } +} diff --git a/tests/PHPStan/Rules/Keywords/data/include-in-file-exists.php b/tests/PHPStan/Rules/Keywords/data/include-in-file-exists.php index f3b1e22633..a650a42e63 100644 --- a/tests/PHPStan/Rules/Keywords/data/include-in-file-exists.php +++ b/tests/PHPStan/Rules/Keywords/data/include-in-file-exists.php @@ -8,6 +8,22 @@ require __DIR__ . '/../vendor/autoload.php'; } +if (is_readable(__DIR__ . '/../vendor/autoload.php')) { + require __DIR__ . '/../vendor/autoload.php'; +} + +if (is_writable(__DIR__ . '/../vendor/autoload.php')) { + require __DIR__ . '/../vendor/autoload.php'; +} + +if (is_writeable(__DIR__ . '/../vendor/autoload.php')) { + require __DIR__ . '/../vendor/autoload.php'; +} + +if (is_executable(__DIR__ . '/../vendor/autoload.php')) { + require __DIR__ . '/../vendor/autoload.php'; +} + foreach ([__DIR__ . '/../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/vendor/autoload.php'] as $file) { if (file_exists($file)) { require $file;