Skip to content

Commit c0f6116

Browse files
vojtech-dobesondrejmirtes
authored andcommitted
Add assertSuperType testing utility
1 parent d817de5 commit c0f6116

File tree

7 files changed

+129
-10
lines changed

7 files changed

+129
-10
lines changed

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ parameters:
780780
-
781781
message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#'
782782
identifier: phpstanApi.instanceofType
783-
count: 2
783+
count: 3
784784
path: src/Testing/TypeInferenceTestCase.php
785785

786786
-

src/Rules/Debug/FileAssertRule.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpParser\Node\Expr\StaticCall;
77
use PHPStan\Analyser\Scope;
88
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\PhpDoc\TypeStringResolver;
910
use PHPStan\Reflection\ReflectionProvider;
1011
use PHPStan\Rules\IdentifierRuleError;
1112
use PHPStan\Rules\Rule;
@@ -23,7 +24,10 @@
2324
final class FileAssertRule implements Rule
2425
{
2526

26-
public function __construct(private ReflectionProvider $reflectionProvider)
27+
public function __construct(
28+
private ReflectionProvider $reflectionProvider,
29+
private TypeStringResolver $typeStringResolver,
30+
)
2731
{
2832
}
2933

@@ -51,6 +55,10 @@ public function processNode(Node $node, Scope $scope): array
5155
return $this->processAssertNativeType($node->getArgs(), $scope);
5256
}
5357

58+
if ($function->getName() === 'PHPStan\\Testing\\assertSuperType') {
59+
return $this->processAssertSuperType($node->getArgs(), $scope);
60+
}
61+
5462
if ($function->getName() === 'PHPStan\\Testing\\assertVariableCertainty') {
5563
return $this->processAssertVariableCertainty($node->getArgs(), $scope);
5664
}
@@ -124,6 +132,40 @@ private function processAssertNativeType(array $args, Scope $scope): array
124132
];
125133
}
126134

135+
/**
136+
* @param Node\Arg[] $args
137+
* @return list<IdentifierRuleError>
138+
*/
139+
private function processAssertSuperType(array $args, Scope $scope): array
140+
{
141+
if (count($args) !== 2) {
142+
return [];
143+
}
144+
145+
$expectedTypeStrings = $scope->getType($args[0]->value)->getConstantStrings();
146+
if (count($expectedTypeStrings) !== 1) {
147+
return [
148+
RuleErrorBuilder::message('Expected super type must be a literal string.')
149+
->nonIgnorable()
150+
->identifier('phpstan.unknownExpectation')
151+
->build(),
152+
];
153+
}
154+
155+
$expressionType = $scope->getType($args[1]->value);
156+
$expectedType = $this->typeStringResolver->resolve($expectedTypeStrings[0]->getValue());
157+
if ($expectedType->isSuperTypeOf($expressionType)->yes()) {
158+
return [];
159+
}
160+
161+
return [
162+
RuleErrorBuilder::message(sprintf('Expected subtype of %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType->describe(VerbosityLevel::precise())))
163+
->nonIgnorable()
164+
->identifier('phpstan.superType')
165+
->build(),
166+
];
167+
}
168+
127169
/**
128170
* @param Node\Arg[] $args
129171
* @return list<IdentifierRuleError>

src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ final class CallToFunctionStatementWithoutSideEffectsRule implements Rule
3535
'PHPStan\\debugScope',
3636
'PHPStan\\Testing\\assertType',
3737
'PHPStan\\Testing\\assertNativeType',
38+
'PHPStan\\Testing\\assertSuperType',
3839
'PHPStan\\Testing\\assertVariableCertainty',
3940
];
4041

src/Testing/TypeInferenceTestCase.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,28 @@ public function assertFileAsserts(
147147
$actual,
148148
$failureMessage,
149149
);
150+
} elseif ($assertType === 'superType') {
151+
$expected = $args[0];
152+
$actual = $args[1];
153+
$isCorrect = $args[2];
154+
155+
$failureMessage = sprintf('Expected subtype of %s, got type %s in %s on line %d.', $expected, $actual, $file, $args[3]);
156+
157+
$delayedErrors = $args[4] ?? [];
158+
if (count($delayedErrors) > 0) {
159+
$failureMessage .= sprintf(
160+
"\n\nThis failure might be reported because of the following misconfiguration %s:\n\n",
161+
count($delayedErrors) === 1 ? 'issue' : 'issues',
162+
);
163+
foreach ($delayedErrors as $delayedError) {
164+
$failureMessage .= sprintf("* %s\n", $delayedError);
165+
}
166+
}
167+
168+
$this->assertTrue(
169+
$isCorrect,
170+
$failureMessage,
171+
);
150172
} elseif ($assertType === 'variableCertainty') {
151173
$expectedCertainty = $args[0];
152174
$actualCertainty = $args[1];
@@ -214,7 +236,7 @@ public static function gatherAssertTypes(string $file): array
214236
}
215237

216238
$functionName = $nameNode->toString();
217-
if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertvariablecertainty'], true)) {
239+
if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertsupertype', 'assertvariablecertainty'], true)) {
218240
self::fail(sprintf(
219241
'Missing use statement for %s() in %s on line %d.',
220242
$functionName,
@@ -246,6 +268,18 @@ public static function gatherAssertTypes(string $file): array
246268

247269
$actualType = $scope->getNativeType($node->getArgs()[1]->value);
248270
$assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()];
271+
} elseif ($functionName === 'PHPStan\\Testing\\assertSuperType') {
272+
$expectedType = $scope->getType($node->getArgs()[0]->value);
273+
if (!$expectedType instanceof ConstantScalarType) {
274+
self::fail(sprintf(
275+
'Expected super type must be a literal string, %s given in %s on line %d.',
276+
$expectedType->describe(VerbosityLevel::precise()),
277+
$relativePathHelper->getRelativePath($file),
278+
$node->getStartLine(),
279+
));
280+
}
281+
$actualType = $scope->getType($node->getArgs()[1]->value);
282+
$assert = ['superType', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $expectedType->isSuperTypeOf($actualType)->yes(), $node->getStartLine()];
249283
} elseif ($functionName === 'PHPStan\\Testing\\assertVariableCertainty') {
250284
$certainty = $node->getArgs()[0]->value;
251285
if (!$certainty instanceof StaticCall) {
@@ -284,6 +318,7 @@ public static function gatherAssertTypes(string $file): array
284318
$assertFunctions = [
285319
'assertType' => 'PHPStan\\Testing\\assertType',
286320
'assertNativeType' => 'PHPStan\\Testing\\assertNativeType',
321+
'assertSuperType' => 'PHPStan\\Testing\\assertSuperType',
287322
'assertVariableCertainty' => 'PHPStan\\Testing\\assertVariableCertainty',
288323
];
289324
foreach ($assertFunctions as $assertFn => $fqFunctionName) {

src/Testing/functions.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ function assertNativeType(string $type, $value) // phpcs:ignore
3535
return null;
3636
}
3737

38+
/**
39+
* Asserts a super type of a value.
40+
*
41+
* @phpstan-pure
42+
* @param mixed $value
43+
* @return mixed
44+
*
45+
* @throws void
46+
*/
47+
function assertSuperType(string $superType, $value) // phpcs:ignore
48+
{
49+
return null;
50+
}
51+
3852
/**
3953
* @phpstan-pure
4054
* @param mixed $variable

tests/PHPStan/Rules/Debug/FileAssertRuleTest.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Rules\Debug;
44

5+
use PHPStan\PhpDoc\TypeStringResolver;
56
use PHPStan\Rules\Rule;
67
use PHPStan\Testing\RuleTestCase;
78

@@ -13,35 +14,50 @@ class FileAssertRuleTest extends RuleTestCase
1314

1415
protected function getRule(): Rule
1516
{
16-
return new FileAssertRule(self::createReflectionProvider());
17+
return new FileAssertRule(
18+
self::createReflectionProvider(),
19+
self::getContainer()->getByType(TypeStringResolver::class),
20+
);
1721
}
1822

1923
public function testRule(): void
2024
{
2125
$this->analyse([__DIR__ . '/data/file-asserts.php'], [
2226
[
2327
'Expected type array<string>, actual: array<int>',
24-
19,
28+
20,
29+
],
30+
[
31+
'Expected subtype of array<string>, actual: array<int>',
32+
23,
2533
],
2634
[
2735
'Expected native type false, actual: bool',
28-
36,
36+
41,
2937
],
3038
[
3139
'Expected native type true, actual: bool',
32-
37,
40+
42,
41+
],
42+
[
43+
'Expected subtype of string, actual: false',
44+
47,
45+
],
46+
[
47+
'Expected subtype of never, actual: false',
48+
48,
3349
],
3450
[
3551
'Expected variable $b certainty Yes, actual: No',
36-
45,
52+
56,
3753
],
3854
[
3955
'Expected variable $b certainty Maybe, actual: No',
40-
46,
56+
57,
4157
],
4258
[
4359
"Expected offset 'firstName' certainty No, actual: Yes",
44-
65,
60+
76,
4561
],
4662
]);
4763
}

tests/PHPStan/Rules/Debug/data/file-asserts.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\TrinaryLogic;
66
use function PHPStan\Testing\assertNativeType;
77
use function PHPStan\Testing\assertType;
8+
use function PHPStan\Testing\assertSuperType;
89
use function PHPStan\Testing\assertVariableCertainty;
910

1011
class Foo
@@ -17,6 +18,9 @@ public function doFoo(array $a): void
1718
{
1819
assertType('array<int>', $a);
1920
assertType('array<string>', $a);
21+
22+
assertSuperType('array<int|string>', $a);
23+
assertSuperType('array<string>', $a);
2024
}
2125

2226
/**
@@ -26,6 +30,7 @@ public function doBar(array $a): void
2630
{
2731
assertType('non-empty-array<int>', $a);
2832
assertNativeType('array', $a);
33+
assertSuperType('mixed', $a);
2934

3035
assertType('false', $a === []);
3136
assertType('true', $a !== []);
@@ -35,6 +40,12 @@ public function doBar(array $a): void
3540

3641
assertNativeType('false', $a === []);
3742
assertNativeType('true', $a !== []);
43+
44+
assertSuperType('bool', $a === []);
45+
assertSuperType('bool', $a !== []);
46+
assertSuperType('mixed', $a === []);
47+
assertSuperType('string', $a === []);
48+
assertSuperType('never', $a === []);
3849
}
3950

4051
public function doBaz($a): void

0 commit comments

Comments
 (0)