Skip to content

Commit 14f3893

Browse files
committed
Do not treat method templates resolved to their bound as unresolved
The unresolved template check skipped every template whose resolved type equals its bound. That is the expected fallback for class templates the call does not provide, but a method template is inferred from the arguments: a template inferred as `mixed` equals its default bound and was wrongly skipped. This suppressed the already-narrowed report for @phpstan-assert calls whose asserted type was already satisfied.
1 parent acd0511 commit 14f3893

4 files changed

Lines changed: 84 additions & 9 deletions

File tree

‎src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ static function (string $parameterName) use ($argsMap, $getArgType): ?Type {
881881
static function (Type $type, callable $traverse) use ($templateTypeMap, &$containsUnresolvedTemplate) {
882882
if ($type instanceof TemplateType && $type->getScope()->getClassName() !== null) {
883883
$resolvedType = $templateTypeMap->getType($type->getName());
884-
if ($resolvedType === null || $type->getBound()->equals($resolvedType)) {
884+
if ($resolvedType === null || ($type->getScope()->getFunctionName() === null && $type->getBound()->equals($resolvedType))) {
885885
$containsUnresolvedTemplate = true;
886886
return $type;
887887
}

‎tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,38 @@ public function testAssertUnresolvedGeneric(): void
143143
$this->analyse([__DIR__ . '/data/assert-unresolved-generic.php'], []);
144144
}
145145

146+
#[RequiresPhp('>= 8.0.0')]
147+
public function testNoNamedArgumentsAssertIsAlwaysSatisfied(): void
148+
{
149+
$this->treatPhpDocTypesAsCertain = false;
150+
$this->analyse([__DIR__ . '/data/no-named-arguments-assert.php'], [
151+
[
152+
'Call to static method NoNamedArgumentsAssert\QueryBuilder::validateVariadicParameter() with list<mixed> will always evaluate to true.',
153+
14,
154+
],
155+
[
156+
'Call to static method NoNamedArgumentsAssert\QueryBuilder::validateVariadicParameter() with list<string> will always evaluate to true.',
157+
22,
158+
],
159+
]);
160+
}
161+
162+
#[RequiresPhp('>= 8.0.0')]
163+
public function testNoNamedArgumentsAssertIsAlwaysSatisfiedTreatPhpDocTypesAsCertain(): void
164+
{
165+
$this->treatPhpDocTypesAsCertain = true;
166+
$this->analyse([__DIR__ . '/data/no-named-arguments-assert.php'], [
167+
[
168+
'Call to static method NoNamedArgumentsAssert\QueryBuilder::validateVariadicParameter() with list<mixed> will always evaluate to true.',
169+
14,
170+
],
171+
[
172+
'Call to static method NoNamedArgumentsAssert\QueryBuilder::validateVariadicParameter() with list<string> will always evaluate to true.',
173+
22,
174+
],
175+
]);
176+
}
177+
146178
public static function dataReportAlwaysTrueInLastCondition(): iterable
147179
{
148180
yield [false, [
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php // lint >= 8.0
2+
declare(strict_types = 1);
3+
4+
namespace NoNamedArgumentsAssert;
5+
6+
final class QueryBuilder
7+
{
8+
9+
/**
10+
* @no-named-arguments
11+
*/
12+
public function select(mixed ...$select): void
13+
{
14+
self::validateVariadicParameter($select);
15+
}
16+
17+
/**
18+
* @no-named-arguments
19+
*/
20+
public function groupBy(string ...$groupBy): void
21+
{
22+
self::validateVariadicParameter($groupBy);
23+
}
24+
25+
/**
26+
* @template TItem
27+
* @param TItem[] $parameter
28+
* @phpstan-assert list<TItem> $parameter
29+
*/
30+
private static function validateVariadicParameter(array $parameter): void
31+
{
32+
if (array_is_list($parameter)) {
33+
return;
34+
}
35+
36+
throw new \InvalidArgumentException();
37+
}
38+
39+
}

‎turbo-ext/src/DefaultNarrowingHelper.cpp‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2027,15 +2027,19 @@ class DefaultNarrowingHelper
20272027
if (UNEXPECTED(resolvedType.isUndef())) return;
20282028
bool unresolved = resolvedType.isNull();
20292029
if (!unresolved) {
2030-
zv::Val bound = pt_type_call(Z_OBJ_P(type), PT_LC("getbound"), 0, NULL);
2031-
if (UNEXPECTED(bound.isUndef())) return;
2032-
if (UNEXPECTED(Z_TYPE_P(bound.raw()) != IS_OBJECT)) {
2033-
(void) callOnNonObject("equals", bound.raw());
2034-
return;
2030+
zv::Val functionName = pt_type_call(Z_OBJ_P(templateScope.raw()), PT_LC("getfunctionname"), 0, NULL);
2031+
if (UNEXPECTED(functionName.isUndef())) return;
2032+
if (functionName.isNull()) {
2033+
zv::Val bound = pt_type_call(Z_OBJ_P(type), PT_LC("getbound"), 0, NULL);
2034+
if (UNEXPECTED(bound.isUndef())) return;
2035+
if (UNEXPECTED(Z_TYPE_P(bound.raw()) != IS_OBJECT)) {
2036+
(void) callOnNonObject("equals", bound.raw());
2037+
return;
2038+
}
2039+
zv::Val equals = pt_type_op(Z_OBJ_P(bound.raw()), PT_OP_EQUALS, 1, resolvedType.raw());
2040+
if (UNEXPECTED(equals.isUndef())) return;
2041+
unresolved = zend_is_true(equals.raw());
20352042
}
2036-
zv::Val equals = pt_type_op(Z_OBJ_P(bound.raw()), PT_OP_EQUALS, 1, resolvedType.raw());
2037-
if (UNEXPECTED(equals.isUndef())) return;
2038-
unresolved = zend_is_true(equals.raw());
20392043
}
20402044
if (unresolved) {
20412045
zval *target = containsUnresolvedTemplate;

0 commit comments

Comments
 (0)