diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a8434f20f9..94d21ed70e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -780,7 +780,7 @@ parameters: - message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' identifier: phpstanApi.instanceofType - count: 2 + count: 3 path: src/Testing/TypeInferenceTestCase.php - diff --git a/src/Rules/Debug/FileAssertRule.php b/src/Rules/Debug/FileAssertRule.php index 888b00a445..bf5af7456d 100644 --- a/src/Rules/Debug/FileAssertRule.php +++ b/src/Rules/Debug/FileAssertRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -23,7 +24,10 @@ final class FileAssertRule implements Rule { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private TypeStringResolver $typeStringResolver, + ) { } @@ -51,6 +55,10 @@ public function processNode(Node $node, Scope $scope): array return $this->processAssertNativeType($node->getArgs(), $scope); } + if ($function->getName() === 'PHPStan\\Testing\\assertSuperType') { + return $this->processAssertSuperType($node->getArgs(), $scope); + } + if ($function->getName() === 'PHPStan\\Testing\\assertVariableCertainty') { return $this->processAssertVariableCertainty($node->getArgs(), $scope); } @@ -124,6 +132,40 @@ private function processAssertNativeType(array $args, Scope $scope): array ]; } + /** + * @param Node\Arg[] $args + * @return list + */ + private function processAssertSuperType(array $args, Scope $scope): array + { + if (count($args) !== 2) { + return []; + } + + $expectedTypeStrings = $scope->getType($args[0]->value)->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { + return [ + RuleErrorBuilder::message('Expected super type must be a literal string.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), + ]; + } + + $expressionType = $scope->getType($args[1]->value); + $expectedType = $this->typeStringResolver->resolve($expectedTypeStrings[0]->getValue()); + if ($expectedType->isSuperTypeOf($expressionType)->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Expected subtype of %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType->describe(VerbosityLevel::precise()))) + ->nonIgnorable() + ->identifier('phpstan.superType') + ->build(), + ]; + } + /** * @param Node\Arg[] $args * @return list diff --git a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php index 2e379420b7..acbb863ed4 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -35,6 +35,7 @@ final class CallToFunctionStatementWithoutSideEffectsRule implements Rule 'PHPStan\\debugScope', 'PHPStan\\Testing\\assertType', 'PHPStan\\Testing\\assertNativeType', + 'PHPStan\\Testing\\assertSuperType', 'PHPStan\\Testing\\assertVariableCertainty', ]; diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 0d7e0306d1..1793d7b1ec 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -147,6 +147,28 @@ public function assertFileAsserts( $actual, $failureMessage, ); + } elseif ($assertType === 'superType') { + $expected = $args[0]; + $actual = $args[1]; + $isCorrect = $args[2]; + + $failureMessage = sprintf('Expected subtype of %s, got type %s in %s on line %d.', $expected, $actual, $file, $args[3]); + + $delayedErrors = $args[4] ?? []; + if (count($delayedErrors) > 0) { + $failureMessage .= sprintf( + "\n\nThis failure might be reported because of the following misconfiguration %s:\n\n", + count($delayedErrors) === 1 ? 'issue' : 'issues', + ); + foreach ($delayedErrors as $delayedError) { + $failureMessage .= sprintf("* %s\n", $delayedError); + } + } + + $this->assertTrue( + $isCorrect, + $failureMessage, + ); } elseif ($assertType === 'variableCertainty') { $expectedCertainty = $args[0]; $actualCertainty = $args[1]; @@ -214,7 +236,7 @@ public static function gatherAssertTypes(string $file): array } $functionName = $nameNode->toString(); - if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertvariablecertainty'], true)) { + if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertsupertype', 'assertvariablecertainty'], true)) { self::fail(sprintf( 'Missing use statement for %s() in %s on line %d.', $functionName, @@ -246,6 +268,18 @@ public static function gatherAssertTypes(string $file): array $actualType = $scope->getNativeType($node->getArgs()[1]->value); $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; + } elseif ($functionName === 'PHPStan\\Testing\\assertSuperType') { + $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf( + 'Expected super type must be a literal string, %s given in %s on line %d.', + $expectedType->describe(VerbosityLevel::precise()), + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); + } + $actualType = $scope->getType($node->getArgs()[1]->value); + $assert = ['superType', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $expectedType->isSuperTypeOf($actualType)->yes(), $node->getStartLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertVariableCertainty') { $certainty = $node->getArgs()[0]->value; if (!$certainty instanceof StaticCall) { @@ -284,6 +318,7 @@ public static function gatherAssertTypes(string $file): array $assertFunctions = [ 'assertType' => 'PHPStan\\Testing\\assertType', 'assertNativeType' => 'PHPStan\\Testing\\assertNativeType', + 'assertSuperType' => 'PHPStan\\Testing\\assertSuperType', 'assertVariableCertainty' => 'PHPStan\\Testing\\assertVariableCertainty', ]; foreach ($assertFunctions as $assertFn => $fqFunctionName) { diff --git a/src/Testing/functions.php b/src/Testing/functions.php index c79cc2d99c..241d78d31f 100644 --- a/src/Testing/functions.php +++ b/src/Testing/functions.php @@ -35,6 +35,20 @@ function assertNativeType(string $type, $value) // phpcs:ignore return null; } +/** + * Asserts a super type of a value. + * + * @phpstan-pure + * @param mixed $value + * @return mixed + * + * @throws void + */ +function assertSuperType(string $superType, $value) // phpcs:ignore +{ + return null; +} + /** * @phpstan-pure * @param mixed $variable diff --git a/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php b/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php index 40224fc3f2..c58a8de72b 100644 --- a/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php +++ b/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Debug; +use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -13,7 +14,10 @@ class FileAssertRuleTest extends RuleTestCase protected function getRule(): Rule { - return new FileAssertRule(self::createReflectionProvider()); + return new FileAssertRule( + self::createReflectionProvider(), + self::getContainer()->getByType(TypeStringResolver::class), + ); } public function testRule(): void @@ -21,27 +25,39 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/file-asserts.php'], [ [ 'Expected type array, actual: array', - 19, + 20, + ], + [ + 'Expected subtype of array, actual: array', + 23, ], [ 'Expected native type false, actual: bool', - 36, + 41, ], [ 'Expected native type true, actual: bool', - 37, + 42, + ], + [ + 'Expected subtype of string, actual: false', + 47, + ], + [ + 'Expected subtype of never, actual: false', + 48, ], [ 'Expected variable $b certainty Yes, actual: No', - 45, + 56, ], [ 'Expected variable $b certainty Maybe, actual: No', - 46, + 57, ], [ "Expected offset 'firstName' certainty No, actual: Yes", - 65, + 76, ], ]); } diff --git a/tests/PHPStan/Rules/Debug/data/file-asserts.php b/tests/PHPStan/Rules/Debug/data/file-asserts.php index 10289de586..153c1b0e6c 100644 --- a/tests/PHPStan/Rules/Debug/data/file-asserts.php +++ b/tests/PHPStan/Rules/Debug/data/file-asserts.php @@ -5,6 +5,7 @@ use PHPStan\TrinaryLogic; use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertSuperType; use function PHPStan\Testing\assertVariableCertainty; class Foo @@ -17,6 +18,9 @@ public function doFoo(array $a): void { assertType('array', $a); assertType('array', $a); + + assertSuperType('array', $a); + assertSuperType('array', $a); } /** @@ -26,6 +30,7 @@ public function doBar(array $a): void { assertType('non-empty-array', $a); assertNativeType('array', $a); + assertSuperType('mixed', $a); assertType('false', $a === []); assertType('true', $a !== []); @@ -35,6 +40,12 @@ public function doBar(array $a): void assertNativeType('false', $a === []); assertNativeType('true', $a !== []); + + assertSuperType('bool', $a === []); + assertSuperType('bool', $a !== []); + assertSuperType('mixed', $a === []); + assertSuperType('string', $a === []); + assertSuperType('never', $a === []); } public function doBaz($a): void