-
Notifications
You must be signed in to change notification settings - Fork 369
Expand file tree
/
Copy pathMoreSpecificElementTypePlugin.php
More file actions
246 lines (233 loc) · 9.87 KB
/
MoreSpecificElementTypePlugin.php
File metadata and controls
246 lines (233 loc) · 9.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\FQSEN;
use Phan\Language\UnionType;
use Phan\Library\Map;
use Phan\Library\Set;
use Phan\PluginV3;
use Phan\PluginV3\FinalizeProcessCapability;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for return types that can be made more specific.
*
* - E.g. `/** (at)return object (*)/ function () { return new ArrayObject(); }`
* could be documented as returning an ArrayObject instead.
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* MoreSpecificElementTypePlugin hooks into two events:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed in post-order
* - finalizeProcess
* This is called after the other forms of analysis are finished running.
*
* A plugin file must
*
* - Contain a class that inherits from \Phan\PluginV3
*
* - End by returning an instance of that class.
*
* It is assumed without being checked that plugins aren't
* mangling state within the passed code base or context.
*
* Note: When adding new plugins,
* add them to the corresponding section of README.md
*
* TODO: Account for methods in traits being possibly overrides
*
* TODO: This does not support intersection types
*/
class MoreSpecificElementTypePlugin extends PluginV3 implements
PostAnalyzeNodeCapability,
FinalizeProcessCapability
{
/** @var Map<FQSEN,ElementTypeInfo> maps function/method/closure FQSEN to function info and the set of union types they return */
public static $method_return_types;
/** @var Set<FQSEN> the set of function/method/closure FQSENs that don't need to be more specific. */
public static $method_blacklist;
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return MoreSpecificElementTypeVisitor::class;
}
/**
* Record that $function contains a return statement which returns an expression of type $return_type.
*
* This may be called multiple times for the same return statement (Phan recursively analyzes functions with underspecified param types by default)
*/
public static function recordType(FunctionInterface $function, UnionType $return_type): void
{
$fqsen = $function->getFQSEN();
if (self::$method_blacklist->offsetExists($fqsen)) {
return;
}
if ($return_type->isEmpty()) {
self::$method_blacklist->offsetSet($fqsen);
self::$method_return_types->offsetUnset($fqsen);
return;
}
if (self::$method_return_types->offsetExists($fqsen)) {
self::$method_return_types->offsetGet($fqsen)->types->offsetSet($return_type);
} else {
self::$method_return_types->offsetSet($fqsen, new ElementTypeInfo($function, [$return_type]));
}
}
private static function shouldWarnAboutMoreSpecificType(CodeBase $code_base, UnionType $actual_type, UnionType $declared_return_type): bool
{
if ($declared_return_type->isEmpty()) {
// There was no phpdoc type declaration, so let UnknownElementTypePlugin warn about that instead of this.
// This plugin warns about `@return mixed` but not the absence of a declaration because the former normally prevents phan from inferring something more specific.
return false;
}
if ($declared_return_type->containsNullable() && !$actual_type->containsNullable()) {
// Warn about `Subclass1|Subclass2` being the real return type of `?BaseClass`
// because the actual returned type is non-null
return true;
}
if ($declared_return_type->typeCount() === 1) {
if ($declared_return_type->getTypeSet()[0]->hasObjectWithKnownFQSEN()) {
if ($actual_type->typeCount() >= 2) {
// Don't warn about Subclass1|Subclass2 being more specific than BaseClass
return false;
}
}
}
if ($declared_return_type->isStrictSubtypeOf($code_base, $actual_type)) {
return false;
}
if (!$actual_type->isStrictSubtypeOf($code_base, $declared_return_type)) {
return false;
}
if (!$actual_type->canCastToUnionType($declared_return_type, $code_base)) {
// Don't warn here about type mismatches such as int->string or object->array, but do warn about SubClass->BaseClass.
// Phan should warn elsewhere about those mismatches
return false;
}
if ($declared_return_type->hasTopLevelArrayShapeTypeInstances()) {
return false;
}
$real_actual_type = $actual_type->getRealUnionType();
if (!$real_actual_type->isEmpty() && $declared_return_type->isStrictSubtypeOf($code_base, $real_actual_type)) {
// TODO: Provide a way to disable this heuristic.
return false;
}
return true;
}
private static function containsObjectWithKnownFQSEN(UnionType $union_type): bool
{
foreach ($union_type->getTypesRecursively() as $type) {
if ($type->hasObjectWithKnownFQSEN()) {
return true;
}
}
return false;
}
/**
* After all return statements are gathered, suggest a more specific type for the various functions.
*/
public function finalizeProcess(CodeBase $code_base): void
{
foreach (self::$method_return_types as $type_info) {
$function = $type_info->function;
$function_context = $function->getContext();
// TODO: Do a better job for Traversable<MyClass> and iterable<MyClass>
$actual_type = UnionType::merge($type_info->types->toArray())->withStaticResolvedInContext($function_context)->eraseTemplatesRecursive()->asNormalizedTypes();
$declared_return_type = $function->getOriginalReturnType()->withStaticResolvedInContext($function_context)->eraseTemplatesRecursive()->asNormalizedTypes();
if (!self::shouldWarnAboutMoreSpecificType($code_base, $actual_type, $declared_return_type)) {
continue;
}
if (self::containsObjectWithKnownFQSEN($actual_type) && !self::containsObjectWithKnownFQSEN($declared_return_type)) {
$issue_type = 'PhanPluginMoreSpecificActualReturnTypeContainsFQSEN';
$issue_message = 'Phan inferred that {FUNCTION} documented to have return type {TYPE} (without an FQSEN) returns the more specific type {TYPE} (with an FQSEN)';
} else {
$issue_type = 'PhanPluginMoreSpecificActualReturnType';
$issue_message = 'Phan inferred that {FUNCTION} documented to have return type {TYPE} returns the more specific type {TYPE}';
}
$this->emitIssue(
$code_base,
$function->getContext(),
$issue_type,
$issue_message,
[
$function->getRepresentationForIssue(),
$declared_return_type,
$actual_type->getDebugRepresentation()
]
);
}
}
}
/**
* Represents the actual return types seen during analysis
* (including recursive analysis)
*/
class ElementTypeInfo
{
/** @var FunctionInterface the function with the return values*/
public $function;
/** @var Set<UnionType> the set of observed return types */
public $types;
/**
* @param list<UnionType> $return_types
*/
public function __construct(FunctionInterface $function, array $return_types)
{
$this->function = $function;
$this->types = new Set($return_types);
}
}
MoreSpecificElementTypePlugin::$method_blacklist = new Set();
MoreSpecificElementTypePlugin::$method_return_types = new Map();
/**
* This visitor analyzes node kinds that can be the root of expressions
* containing duplicate expressions, and is called on nodes in post-order.
*/
class MoreSpecificElementTypeVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @param Node $node a node of kind ast\AST_RETURN, representing a return statement.
*/
public function visitReturn(Node $node): void
{
if (!$this->context->isInFunctionLikeScope()) {
return;
}
try {
$function = $this->context->getFunctionLikeInScope($this->code_base);
} catch (Exception) {
return;
}
if ($function->hasYield()) {
// TODO: Support analyzing yield key/value types of generators?
return;
}
if ($function instanceof Method) {
// Skip functions that are overrides or are overridden.
// They may be documenting a less specific return type to deal with the inheritance hierarchy.
if ($function->isOverride() || $function->isOverriddenByAnother()) {
return;
}
}
try {
// Fetch the list of valid classes, and warn about any undefined classes.
// (We have more specific issue types such as PhanNonClassMethodCall below, don't emit PhanTypeExpected*)
$union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']);
} catch (Exception) {
// Phan should already throw for this
return;
}
MoreSpecificElementTypePlugin::recordType($function, $union_type->withFlattenedArrayShapeOrLiteralTypeInstances());
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new MoreSpecificElementTypePlugin();