diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 897a698dea..4ee51df722 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -75,7 +76,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $constantArray->isOptionalKey($i), ); } - $arrayTypes[] = $returnedArrayBuilder->getArray(); + $returnedArray = $returnedArrayBuilder->getArray(); + if ($constantArray->isList()->yes()) { + $returnedArray = AccessoryArrayListType::intersectWith($returnedArray); + } + $arrayTypes[] = $returnedArray; } $mappedArrayType = TypeCombinator::union(...$arrayTypes); diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 5471553638..336db1f971 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -934,6 +934,11 @@ public function testLists(): void ]); } + public function testConditionalListRule(): void + { + $this->analyse([__DIR__ . '/data/return-list-rule.php'], []); + } + public function testBug6856(): void { $this->analyse([__DIR__ . '/data/bug-6856.php'], []); diff --git a/tests/PHPStan/Rules/Methods/data/return-list-rule.php b/tests/PHPStan/Rules/Methods/data/return-list-rule.php new file mode 100644 index 0000000000..4827058bdf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-list-rule.php @@ -0,0 +1,87 @@ + + */ +class BinaryOpEnumValueRule implements Rule +{ + + /** @var class-string */ + private string $className; + + /** + * @param class-string $operator + */ + public function __construct(string $operator, ?string $okMessage = null) + { + $this->className = $operator; + } + + public function getNodeType(): string + { + return $this->className; + } + + /** + * @param BinaryOp $node + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + $leftType = $scope->getType($node->left); + $rightType = $scope->getType($node->right); + $isDirectCompareType = true; + + if (!$this->isEnumWithValue($leftType) || !$this->isEnumWithValue($rightType)) { + $isDirectCompareType = false; + } + + $errors = []; + $leftError = $this->processOpExpression($node->left, $leftType, $node->getOperatorSigil()); + $rightError = $this->processOpExpression($node->right, $rightType, $node->getOperatorSigil()); + + if ($leftError !== null) { + $errors[] = $leftError; + } + + if ($rightError !== null && $rightError !== $leftError) { + $errors[] = $rightError; + } + + if (!$isDirectCompareType && $errors === []) { + return []; + } + + if ($isDirectCompareType && $errors === []) { + $errors[] = sprintf( + 'Cannot compare %s to %s', + $leftType->describe(VerbosityLevel::typeOnly()), + $rightType->describe(VerbosityLevel::typeOnly()), + ); + } + + return array_map(static fn (string $message) => RuleErrorBuilder::message($message)->build(), $errors); + } + + private function processOpExpression(Expr $expression, Type $expressionType, string $sigil): ?string + { + return null; + } + + private function isEnumWithValue(Type $type): bool + { + return false; + } + +}