Skip to content

Commit 2bb53ee

Browse files
committed
Extract return type inference
1 parent 62e6d46 commit 2bb53ee

File tree

3 files changed

+158
-126
lines changed

3 files changed

+158
-126
lines changed

extension.neon

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ services:
3030
additionalConfigNamespaces: %codeigniter.additionalConfigNamespaces%
3131
additionalModelNamespaces: %codeigniter.additionalModelNamespaces%
3232

33+
modelFetchedReturnTypeHelper:
34+
class: CodeIgniter\PHPStan\Type\ModelFetchedReturnTypeHelper
35+
arguments:
36+
notStringFormattedFieldsArray: %codeigniter.notStringFormattedFields%
37+
3338
servicesReturnTypeHelper:
3439
class: CodeIgniter\PHPStan\Type\ServicesReturnTypeHelper
3540
arguments:
@@ -45,8 +50,6 @@ services:
4550

4651
-
4752
class: CodeIgniter\PHPStan\Type\FakeFunctionReturnTypeExtension
48-
arguments:
49-
notStringFormattedFieldsArray: %codeigniter.notStringFormattedFields%
5053
tags:
5154
- phpstan.broker.dynamicFunctionReturnTypeExtension
5255

src/Type/FakeFunctionReturnTypeExtension.php

Lines changed: 3 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -15,61 +15,17 @@
1515

1616
use PhpParser\Node\Expr\FuncCall;
1717
use PHPStan\Analyser\Scope;
18-
use PHPStan\Reflection\ClassReflection;
1918
use PHPStan\Reflection\FunctionReflection;
20-
use PHPStan\Reflection\ReflectionProvider;
21-
use PHPStan\ShouldNotHappenException;
22-
use PHPStan\Type\BooleanType;
23-
use PHPStan\Type\Constant\ConstantArrayType;
24-
use PHPStan\Type\Constant\ConstantStringType;
2519
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
26-
use PHPStan\Type\IntegerType;
2720
use PHPStan\Type\NonAcceptingNeverType;
28-
use PHPStan\Type\ObjectType;
29-
use PHPStan\Type\ObjectWithoutClassType;
30-
use PHPStan\Type\StringType;
3121
use PHPStan\Type\Type;
32-
use stdClass;
3322

3423
final class FakeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
3524
{
36-
/**
37-
* @var array<string, class-string<Type>>
38-
*/
39-
private static array $notStringFormattedFields = [
40-
'success' => BooleanType::class,
41-
'user_id' => IntegerType::class,
42-
];
43-
44-
/**
45-
* @var array<string, class-string<Type>>
46-
*/
47-
private static array $typeInterpolations = [
48-
'bool' => BooleanType::class,
49-
'int' => IntegerType::class,
50-
];
51-
52-
/**
53-
* @var list<string>
54-
*/
55-
private array $dateFields = [];
56-
57-
/**
58-
* @param array<string, string> $notStringFormattedFieldsArray
59-
*/
6025
public function __construct(
26+
private readonly ModelFetchedReturnTypeHelper $modelFetchedReturnTypeHelper,
6127
private readonly FactoriesReturnTypeHelper $factoriesReturnTypeHelper,
62-
private readonly ReflectionProvider $reflectionProvider,
63-
array $notStringFormattedFieldsArray
64-
) {
65-
foreach ($notStringFormattedFieldsArray as $field => $type) {
66-
if (! isset(self::$typeInterpolations[$type])) {
67-
continue;
68-
}
69-
70-
self::$notStringFormattedFields[$field] = self::$typeInterpolations[$type];
71-
}
72-
}
28+
) {}
7329

7430
public function isFunctionSupported(FunctionReflection $functionReflection): bool
7531
{
@@ -98,83 +54,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
9854

9955
$classReflection = current($classReflections);
10056

101-
$returnType = $this->getNativeStringPropertyValue($classReflection, $scope, 'returnType');
102-
103-
if ($returnType === 'object') {
104-
return new ObjectType(stdClass::class);
105-
}
106-
107-
if ($returnType === 'array') {
108-
return $this->getArrayReturnType($classReflection, $scope);
109-
}
110-
111-
if ($this->reflectionProvider->hasClass($returnType)) {
112-
return new ObjectType($returnType);
113-
}
114-
115-
return new ObjectWithoutClassType();
116-
}
117-
118-
private function getArrayReturnType(ClassReflection $classReflection, Scope $scope): Type
119-
{
120-
$this->fillDateFields($classReflection, $scope);
121-
$fieldsTypes = $this->getNativePropertyType($classReflection, $scope, 'allowedFields')->getConstantArrays();
122-
123-
if ($fieldsTypes === []) {
124-
return new ConstantArrayType([], []);
125-
}
126-
127-
$fields = array_filter(array_map(
128-
static fn (Type $type) => current($type->getConstantStrings()),
129-
current($fieldsTypes)->getValueTypes()
130-
));
131-
132-
return new ConstantArrayType(
133-
$fields,
134-
array_map(function (ConstantStringType $fieldType) use ($classReflection, $scope): Type {
135-
$field = $fieldType->getValue();
136-
137-
if (array_key_exists($field, self::$notStringFormattedFields)) {
138-
$type = self::$notStringFormattedFields[$field];
139-
140-
return new $type();
141-
}
142-
143-
if (
144-
in_array($field, $this->dateFields, true)
145-
&& $this->getNativeStringPropertyValue($classReflection, $scope, 'dateFormat') === 'int'
146-
) {
147-
return new IntegerType();
148-
}
149-
150-
return new StringType();
151-
}, $fields)
152-
);
153-
}
154-
155-
private function fillDateFields(ClassReflection $classReflection, Scope $scope): void
156-
{
157-
foreach (['createdAt', 'updatedAt', 'deletedAt'] as $property) {
158-
if ($classReflection->hasNativeProperty($property)) {
159-
$this->dateFields[] = $this->getNativeStringPropertyValue($classReflection, $scope, $property);
160-
}
161-
}
162-
}
163-
164-
private function getNativePropertyType(ClassReflection $classReflection, Scope $scope, string $property): Type
165-
{
166-
if (! $classReflection->hasNativeProperty($property)) {
167-
throw new ShouldNotHappenException(sprintf('Native property %s::$%s does not exist.', $classReflection->getDisplayName(), $property));
168-
}
169-
170-
return $scope->getType($classReflection->getNativeProperty($property)->getNativeReflection()->getDefaultValueExpression());
171-
}
172-
173-
private function getNativeStringPropertyValue(ClassReflection $classReflection, Scope $scope, string $property): string
174-
{
175-
$propertyType = $this->getNativePropertyType($classReflection, $scope, $property)->getConstantStrings();
176-
assert(count($propertyType) === 1);
177-
178-
return current($propertyType)->getValue();
57+
return $this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $scope);
17958
}
18059
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Type;
15+
16+
use PHPStan\Analyser\Scope;
17+
use PHPStan\Reflection\ClassReflection;
18+
use PHPStan\Reflection\ReflectionProvider;
19+
use PHPStan\ShouldNotHappenException;
20+
use PHPStan\Type\BooleanType;
21+
use PHPStan\Type\Constant\ConstantArrayType;
22+
use PHPStan\Type\Constant\ConstantStringType;
23+
use PHPStan\Type\IntegerType;
24+
use PHPStan\Type\ObjectType;
25+
use PHPStan\Type\ObjectWithoutClassType;
26+
use PHPStan\Type\StringType;
27+
use PHPStan\Type\Type;
28+
use stdClass;
29+
30+
final class ModelFetchedReturnTypeHelper
31+
{
32+
/**
33+
* @var array<string, class-string<Type>>
34+
*/
35+
private static array $notStringFormattedFields = [
36+
'success' => BooleanType::class,
37+
'user_id' => IntegerType::class,
38+
];
39+
40+
/**
41+
* @var array<string, class-string<Type>>
42+
*/
43+
private static array $typeInterpolations = [
44+
'bool' => BooleanType::class,
45+
'int' => IntegerType::class,
46+
];
47+
48+
/**
49+
* @var list<string>
50+
*/
51+
private array $dateFields = [];
52+
53+
/**
54+
* @param array<string, string> $notStringFormattedFieldsArray
55+
*/
56+
public function __construct(
57+
private readonly ReflectionProvider $reflectionProvider,
58+
array $notStringFormattedFieldsArray
59+
) {
60+
foreach ($notStringFormattedFieldsArray as $field => $type) {
61+
if (! isset(self::$typeInterpolations[$type])) {
62+
continue;
63+
}
64+
65+
self::$notStringFormattedFields[$field] = self::$typeInterpolations[$type];
66+
}
67+
}
68+
69+
public function getFetchedReturnType(ClassReflection $classReflection, Scope $scope): Type
70+
{
71+
$returnType = $this->getNativeStringPropertyValue($classReflection, $scope, 'returnType');
72+
73+
if ($returnType === 'object') {
74+
return new ObjectType(stdClass::class);
75+
}
76+
77+
if ($returnType === 'array') {
78+
return $this->getArrayReturnType($classReflection, $scope);
79+
}
80+
81+
if ($this->reflectionProvider->hasClass($returnType)) {
82+
return new ObjectType($returnType);
83+
}
84+
85+
return new ObjectWithoutClassType();
86+
}
87+
88+
private function getArrayReturnType(ClassReflection $classReflection, Scope $scope): Type
89+
{
90+
$this->fillDateFields($classReflection, $scope);
91+
$fieldsTypes = $this->getNativePropertyType($classReflection, $scope, 'allowedFields')->getConstantArrays();
92+
93+
if ($fieldsTypes === []) {
94+
return new ConstantArrayType([], []);
95+
}
96+
97+
$fields = array_filter(array_map(
98+
static fn (Type $type) => current($type->getConstantStrings()),
99+
current($fieldsTypes)->getValueTypes()
100+
));
101+
102+
return new ConstantArrayType(
103+
$fields,
104+
array_map(function (ConstantStringType $fieldType) use ($classReflection, $scope): Type {
105+
$field = $fieldType->getValue();
106+
107+
if (array_key_exists($field, self::$notStringFormattedFields)) {
108+
$type = self::$notStringFormattedFields[$field];
109+
110+
return new $type();
111+
}
112+
113+
if (
114+
in_array($field, $this->dateFields, true)
115+
&& $this->getNativeStringPropertyValue($classReflection, $scope, 'dateFormat') === 'int'
116+
) {
117+
return new IntegerType();
118+
}
119+
120+
return new StringType();
121+
}, $fields)
122+
);
123+
}
124+
125+
private function fillDateFields(ClassReflection $classReflection, Scope $scope): void
126+
{
127+
foreach (['createdAt', 'updatedAt', 'deletedAt'] as $property) {
128+
if ($classReflection->hasNativeProperty($property)) {
129+
$this->dateFields[] = $this->getNativeStringPropertyValue($classReflection, $scope, $property);
130+
}
131+
}
132+
}
133+
134+
private function getNativePropertyType(ClassReflection $classReflection, Scope $scope, string $property): Type
135+
{
136+
if (! $classReflection->hasNativeProperty($property)) {
137+
throw new ShouldNotHappenException(sprintf('Native property %s::$%s does not exist.', $classReflection->getDisplayName(), $property));
138+
}
139+
140+
return $scope->getType($classReflection->getNativeProperty($property)->getNativeReflection()->getDefaultValueExpression());
141+
}
142+
143+
private function getNativeStringPropertyValue(ClassReflection $classReflection, Scope $scope, string $property): string
144+
{
145+
$propertyType = $this->getNativePropertyType($classReflection, $scope, $property)->getConstantStrings();
146+
assert(count($propertyType) === 1);
147+
148+
return current($propertyType)->getValue();
149+
}
150+
}

0 commit comments

Comments
 (0)