Skip to content

Commit b763bd9

Browse files
committed
Introduce Type::acceptsWithReason()
1 parent dc8236e commit b763bd9

File tree

67 files changed

+809
-247
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+809
-247
lines changed

Diff for: src/Rules/Arrays/AppendedArrayItemTypeRule.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,15 @@ public function processNode(Node $node, Scope $scope): array
7373
}
7474

7575
$itemType = $assignedToType->getItemType();
76-
if (!$this->ruleLevelHelper->accepts($itemType, $assignedValueType, $scope->isDeclareStrictTypes())) {
76+
$accepts = $this->ruleLevelHelper->acceptsWithReason($itemType, $assignedValueType, $scope->isDeclareStrictTypes());
77+
if (!$accepts->result) {
7778
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($itemType, $assignedValueType);
7879
return [
7980
RuleErrorBuilder::message(sprintf(
8081
'Array (%s) does not accept %s.',
8182
$assignedToType->describe($verbosityLevel),
8283
$assignedValueType->describe($verbosityLevel),
83-
))->build(),
84+
))->acceptsReasonsTip($accepts->reasons)->build(),
8485
];
8586
}
8687

Diff for: src/Rules/FunctionCallParametersCheck.php

+17-15
Original file line numberDiff line numberDiff line change
@@ -252,21 +252,23 @@ public function check(
252252
if ($this->checkArgumentTypes) {
253253
$parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType());
254254

255-
if (!$parameter->passedByReference()->createsNewVariable()
256-
&& !$this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $scope->isDeclareStrictTypes())
257-
) {
258-
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType);
259-
$parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName());
260-
$errors[] = RuleErrorBuilder::message(sprintf(
261-
$messages[6],
262-
$argumentName === null ? sprintf(
263-
'#%d %s',
264-
$i + 1,
265-
$parameterDescription,
266-
) : $parameterDescription,
267-
$parameterType->describe($verbosityLevel),
268-
$argumentValueType->describe($verbosityLevel),
269-
))->line($argumentLine)->build();
255+
if (!$parameter->passedByReference()->createsNewVariable()) {
256+
$accepts = $this->ruleLevelHelper->acceptsWithReason($parameterType, $argumentValueType, $scope->isDeclareStrictTypes());
257+
258+
if (!$accepts->result) {
259+
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType);
260+
$parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName());
261+
$errors[] = RuleErrorBuilder::message(sprintf(
262+
$messages[6],
263+
$argumentName === null ? sprintf(
264+
'#%d %s',
265+
$i + 1,
266+
$parameterDescription,
267+
) : $parameterDescription,
268+
$parameterType->describe($verbosityLevel),
269+
$argumentValueType->describe($verbosityLevel),
270+
))->line($argumentLine)->acceptsReasonsTip($accepts->reasons)->build();
271+
}
270272
}
271273

272274
if ($this->checkUnresolvableParameterTypes

Diff for: src/Rules/FunctionReturnTypeCheck.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,14 @@ public function checkReturnType(
9393
];
9494
}
9595

96-
if (!$this->ruleLevelHelper->accepts($returnType, $returnValueType, $scope->isDeclareStrictTypes())) {
96+
$accepts = $this->ruleLevelHelper->acceptsWithReason($returnType, $returnValueType, $scope->isDeclareStrictTypes());
97+
if (!$accepts->result) {
9798
return [
9899
RuleErrorBuilder::message(sprintf(
99100
$typeMismatchMessage,
100101
$returnType->describe($verbosityLevel),
101102
$returnValueType->describe($verbosityLevel),
102-
))->line($returnNode->getLine())->build(),
103+
))->line($returnNode->getLine())->acceptsReasonsTip($accepts->reasons)->build(),
103104
];
104105
}
105106

Diff for: src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public function processNode(Node $node, Scope $scope): array
4444
$parameterType = $parameters[$paramI]->getType();
4545
$parameterType = TemplateTypeHelper::resolveToBounds($parameterType);
4646

47-
if ($parameterType->accepts($defaultValueType, true)->yes()) {
47+
$accepts = $parameterType->acceptsWithReason($defaultValueType, true);
48+
if ($accepts->yes()) {
4849
continue;
4950
}
5051

@@ -56,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array
5657
$param->var->name,
5758
$defaultValueType->describe($verbosityLevel),
5859
$parameterType->describe($verbosityLevel),
59-
))->line($param->getLine())->build();
60+
))->line($param->getLine())->acceptsReasonsTip($accepts->reasons)->build();
6061
}
6162

6263
return $errors;

Diff for: src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public function processNode(Node $node, Scope $scope): array
4444
$parameterType = $parameters[$paramI]->getType();
4545
$parameterType = TemplateTypeHelper::resolveToBounds($parameterType);
4646

47-
if ($parameterType->accepts($defaultValueType, true)->yes()) {
47+
$accepts = $parameterType->acceptsWithReason($defaultValueType, true);
48+
if ($accepts->yes()) {
4849
continue;
4950
}
5051

@@ -56,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array
5657
$param->var->name,
5758
$defaultValueType->describe($verbosityLevel),
5859
$parameterType->describe($verbosityLevel),
59-
))->line($param->getLine())->build();
60+
))->line($param->getLine())->acceptsReasonsTip($accepts->reasons)->build();
6061
}
6162

6263
return $errors;

Diff for: src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public function processNode(Node $node, Scope $scope): array
4646
$parameterType = $parameters->getParameters()[$paramI]->getType();
4747
$parameterType = TemplateTypeHelper::resolveToBounds($parameterType);
4848

49-
if ($parameterType->accepts($defaultValueType, true)->yes()) {
49+
$accepts = $parameterType->acceptsWithReason($defaultValueType, true);
50+
if ($accepts->yes()) {
5051
continue;
5152
}
5253

@@ -59,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array
5960
$defaultValueType->describe($verbosityLevel),
6061
$function->getName(),
6162
$parameterType->describe($verbosityLevel),
62-
))->line($param->getLine())->build();
63+
))->line($param->getLine())->acceptsReasonsTip($accepts->reasons)->build();
6364
}
6465

6566
return $errors;

Diff for: src/Rules/Generators/YieldFromTypeRule.php

+7-4
Original file line numberDiff line numberDiff line change
@@ -74,21 +74,24 @@ public function processNode(Node $node, Scope $scope): array
7474
}
7575

7676
$messages = [];
77-
if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes())) {
77+
$acceptsKey = $this->ruleLevelHelper->acceptsWithReason($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes());
78+
if (!$acceptsKey->result) {
7879
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType(), $exprType->getIterableKeyType());
7980
$messages[] = RuleErrorBuilder::message(sprintf(
8081
'Generator expects key type %s, %s given.',
8182
$returnType->getIterableKeyType()->describe($verbosityLevel),
8283
$exprType->getIterableKeyType()->describe($verbosityLevel),
83-
))->line($node->expr->getLine())->build();
84+
))->line($node->expr->getLine())->acceptsReasonsTip($acceptsKey->reasons)->build();
8485
}
85-
if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $exprType->getIterableValueType(), $scope->isDeclareStrictTypes())) {
86+
87+
$acceptsValue = $this->ruleLevelHelper->acceptsWithReason($returnType->getIterableValueType(), $exprType->getIterableValueType(), $scope->isDeclareStrictTypes());
88+
if (!$acceptsValue->result) {
8689
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType(), $exprType->getIterableValueType());
8790
$messages[] = RuleErrorBuilder::message(sprintf(
8891
'Generator expects value type %s, %s given.',
8992
$returnType->getIterableValueType()->describe($verbosityLevel),
9093
$exprType->getIterableValueType()->describe($verbosityLevel),
91-
))->line($node->expr->getLine())->build();
94+
))->line($node->expr->getLine())->acceptsReasonsTip($acceptsValue->reasons)->build();
9295
}
9396

9497
$scopeFunction = $scope->getFunction();

Diff for: src/Rules/Generators/YieldTypeRule.php

+6-4
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,14 @@ public function processNode(Node $node, Scope $scope): array
5454
}
5555

5656
$messages = [];
57-
if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $keyType, $scope->isDeclareStrictTypes())) {
57+
$acceptsKey = $this->ruleLevelHelper->acceptsWithReason($returnType->getIterableKeyType(), $keyType, $scope->isDeclareStrictTypes());
58+
if (!$acceptsKey->result) {
5859
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType(), $keyType);
5960
$messages[] = RuleErrorBuilder::message(sprintf(
6061
'Generator expects key type %s, %s given.',
6162
$returnType->getIterableKeyType()->describe($verbosityLevel),
6263
$keyType->describe($verbosityLevel),
63-
))->build();
64+
))->acceptsReasonsTip($acceptsKey->reasons)->build();
6465
}
6566

6667
if ($node->value === null) {
@@ -69,13 +70,14 @@ public function processNode(Node $node, Scope $scope): array
6970
$valueType = $scope->getType($node->value);
7071
}
7172

72-
if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes())) {
73+
$acceptsValue = $this->ruleLevelHelper->acceptsWithReason($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes());
74+
if (!$acceptsValue->result) {
7375
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType(), $valueType);
7476
$messages[] = RuleErrorBuilder::message(sprintf(
7577
'Generator expects value type %s, %s given.',
7678
$returnType->getIterableValueType()->describe($verbosityLevel),
7779
$valueType->describe($verbosityLevel),
78-
))->build();
80+
))->acceptsReasonsTip($acceptsValue->reasons)->build();
7981
}
8082
if (!$scope->isInFirstLevelStatement() && $scope->getType($node)->isVoid()->yes()) {
8183
$messages[] = RuleErrorBuilder::message('Result of yield (void) is used.')->build();

Diff for: src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public function processNode(Node $node, Scope $scope): array
4646
$parameterType = $parameters->getParameters()[$paramI]->getType();
4747
$parameterType = TemplateTypeHelper::resolveToBounds($parameterType);
4848

49-
if ($parameterType->accepts($defaultValueType, true)->yes()) {
49+
$accepts = $parameterType->acceptsWithReason($defaultValueType, true);
50+
if ($accepts->yes()) {
5051
continue;
5152
}
5253

@@ -60,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array
6061
$method->getDeclaringClass()->getDisplayName(),
6162
$method->getName(),
6263
$parameterType->describe($verbosityLevel),
63-
))->line($param->getLine())->build();
64+
))->line($param->getLine())->acceptsReasonsTip($accepts->reasons)->build();
6465
}
6566

6667
return $errors;

Diff for: src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public function processNode(Node $node, Scope $scope): array
4848
}
4949
}
5050
$defaultValueType = $scope->getType($default);
51-
if ($this->ruleLevelHelper->accepts($propertyType, $defaultValueType, true)) {
51+
$accepts = $this->ruleLevelHelper->acceptsWithReason($propertyType, $defaultValueType, true);
52+
if ($accepts->result) {
5253
return [];
5354
}
5455

@@ -62,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array
6263
$node->getName(),
6364
$propertyType->describe($verbosityLevel),
6465
$defaultValueType->describe($verbosityLevel),
65-
))->build(),
66+
))->acceptsReasonsTip($accepts->reasons)->build(),
6667
];
6768
}
6869

Diff for: src/Rules/Properties/TypesAssignedToPropertiesRule.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ private function processSingleProperty(
5959
$scope = $propertyReflection->getScope();
6060
$assignedValueType = $scope->getType($assignedExpr);
6161

62-
if (!$this->ruleLevelHelper->accepts($propertyType, $assignedValueType, $scope->isDeclareStrictTypes())) {
62+
$accepts = $this->ruleLevelHelper->acceptsWithReason($propertyType, $assignedValueType, $scope->isDeclareStrictTypes());
63+
if (!$accepts->result) {
6364
$propertyDescription = $this->propertyDescriptor->describePropertyByName($propertyReflection, $propertyReflection->getName());
6465
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedValueType);
6566

@@ -69,7 +70,7 @@ private function processSingleProperty(
6970
$propertyDescription,
7071
$propertyType->describe($verbosityLevel),
7172
$assignedValueType->describe($verbosityLevel),
72-
))->build(),
73+
))->acceptsReasonsTip($accepts->reasons)->build(),
7374
];
7475
}
7576

Diff for: src/Rules/RuleErrorBuilder.php

+19
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
namespace PHPStan\Rules;
44

55
use PHPStan\ShouldNotHappenException;
6+
use function array_map;
67
use function class_exists;
8+
use function count;
9+
use function implode;
710
use function sprintf;
811

912
/** @api */
@@ -114,6 +117,22 @@ public function discoveringSymbolsTip(): self
114117
return $this->tip('Learn more at https://phpstan.org/user-guide/discovering-symbols');
115118
}
116119

120+
/**
121+
* @param list<string> $reasons
122+
*/
123+
public function acceptsReasonsTip(array $reasons): self
124+
{
125+
if (count($reasons) === 0) {
126+
return $this;
127+
}
128+
129+
if (count($reasons) === 1) {
130+
return $this->tip($reasons[0]);
131+
}
132+
133+
return $this->tip(implode("\n", array_map(static fn (string $reason) => sprintf('* %s', $reason), $reasons)));
134+
}
135+
117136
public function identifier(string $identifier): self
118137
{
119138
$this->properties['identifier'] = $identifier;

Diff for: src/Rules/RuleLevelHelper.php

+48-16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\Type\TypeTraverser;
2020
use PHPStan\Type\UnionType;
2121
use PHPStan\Type\VerbosityLevel;
22+
use function array_merge;
2223
use function count;
2324
use function sprintf;
2425
use function strpos;
@@ -47,6 +48,11 @@ public function isThis(Expr $expression): bool
4748

4849
/** @api */
4950
public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): bool
51+
{
52+
return $this->acceptsWithReason($acceptingType, $acceptedType, $strictTypes)->result;
53+
}
54+
55+
public function acceptsWithReason(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult
5056
{
5157
$checkForUnion = $this->checkUnionTypes;
5258

@@ -112,18 +118,22 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp
112118
$acceptedType = TypeCombinator::removeNull($acceptedType);
113119
}
114120

115-
$accepts = $acceptingType->accepts($acceptedType, $strictTypes);
121+
$accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes);
116122
if ($accepts->yes()) {
117-
return true;
123+
return new RuleLevelHelperAcceptsResult(true, $accepts->reasons);
118124
}
119125
if ($acceptingType instanceof UnionType) {
126+
$reasons = [];
120127
foreach ($acceptingType->getTypes() as $innerType) {
121-
if (self::accepts($innerType, $acceptedType, $strictTypes)) {
122-
return true;
128+
$accepts = self::acceptsWithReason($innerType, $acceptedType, $strictTypes);
129+
if ($accepts->result) {
130+
return $accepts;
123131
}
132+
133+
$reasons = array_merge($reasons, $accepts->reasons);
124134
}
125135

126-
return false;
136+
return new RuleLevelHelperAcceptsResult(false, $reasons);
127137
}
128138

129139
if (
@@ -135,25 +145,47 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp
135145
)
136146
&& $acceptingType->isConstantArray()->no()
137147
) {
138-
return (
139-
!$acceptingType->isIterableAtLeastOnce()->yes()
140-
|| $acceptedType->isIterableAtLeastOnce()->yes()
141-
) && (
142-
!$this->checkListType
143-
|| !$acceptingType->isList()->yes()
144-
|| $acceptedType->isList()->yes()
145-
) && self::accepts(
148+
if ($acceptingType->isIterableAtLeastOnce()->yes() && !$acceptedType->isIterableAtLeastOnce()->yes()) {
149+
$verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType);
150+
return new RuleLevelHelperAcceptsResult(false, [
151+
sprintf(
152+
'%s %s empty.',
153+
$acceptedType->describe($verbosity),
154+
$acceptedType->isIterableAtLeastOnce()->no() ? 'is' : 'might be',
155+
),
156+
]);
157+
}
158+
159+
if (
160+
$this->checkListType
161+
&& $acceptingType->isList()->yes()
162+
&& !$acceptedType->isList()->yes()
163+
) {
164+
$verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType);
165+
return new RuleLevelHelperAcceptsResult(false, [
166+
sprintf(
167+
'%s %s a list.',
168+
$acceptedType->describe($verbosity),
169+
$acceptedType->isList()->no() ? 'is not' : 'might not be',
170+
),
171+
]);
172+
}
173+
174+
return self::acceptsWithReason(
146175
$acceptingType->getIterableKeyType(),
147176
$acceptedType->getIterableKeyType(),
148177
$strictTypes,
149-
) && self::accepts(
178+
)->and(self::acceptsWithReason(
150179
$acceptingType->getIterableValueType(),
151180
$acceptedType->getIterableValueType(),
152181
$strictTypes,
153-
);
182+
));
154183
}
155184

156-
return $checkForUnion ? $accepts->yes() : !$accepts->no();
185+
return new RuleLevelHelperAcceptsResult(
186+
$checkForUnion ? $accepts->yes() : !$accepts->no(),
187+
$accepts->reasons,
188+
);
157189
}
158190

159191
/**

0 commit comments

Comments
 (0)