Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/diviner/atomizer/DivinerPHPAtomizer.php
12256 views
1
<?php
2
3
final class DivinerPHPAtomizer extends DivinerAtomizer {
4
5
protected function newAtom($type) {
6
return parent::newAtom($type)->setLanguage('php');
7
}
8
9
protected function executeAtomize($file_name, $file_data) {
10
$future = PhutilXHPASTBinary::getParserFuture($file_data);
11
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
12
$file_data,
13
$future->resolve());
14
15
$atoms = array();
16
$root = $tree->getRootNode();
17
18
$func_decl = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
19
foreach ($func_decl as $func) {
20
$name = $func->getChildByIndex(2);
21
22
// Don't atomize closures
23
if ($name->getTypeName() === 'n_EMPTY') {
24
continue;
25
}
26
27
$atom = $this->newAtom(DivinerAtom::TYPE_FUNCTION)
28
->setName($name->getConcreteString())
29
->setLine($func->getLineNumber())
30
->setFile($file_name);
31
32
$this->findAtomDocblock($atom, $func);
33
$this->parseParams($atom, $func);
34
$this->parseReturnType($atom, $func);
35
36
$atoms[] = $atom;
37
}
38
39
$class_types = array(
40
DivinerAtom::TYPE_CLASS => 'n_CLASS_DECLARATION',
41
DivinerAtom::TYPE_INTERFACE => 'n_INTERFACE_DECLARATION',
42
);
43
foreach ($class_types as $atom_type => $node_type) {
44
$class_decls = $root->selectDescendantsOfType($node_type);
45
46
foreach ($class_decls as $class) {
47
$name = $class->getChildByIndex(1, 'n_CLASS_NAME');
48
49
$atom = $this->newAtom($atom_type)
50
->setName($name->getConcreteString())
51
->setFile($file_name)
52
->setLine($class->getLineNumber());
53
54
// This parses `final` and `abstract`.
55
$attributes = $class->getChildByIndex(0, 'n_CLASS_ATTRIBUTES');
56
foreach ($attributes->selectDescendantsOfType('n_STRING') as $attr) {
57
$atom->setProperty($attr->getConcreteString(), true);
58
}
59
60
// If this exists, it is `n_EXTENDS_LIST`.
61
$extends = $class->getChildByIndex(2);
62
$extends_class = $extends->selectDescendantsOfType('n_CLASS_NAME');
63
foreach ($extends_class as $parent_class) {
64
$atom->addExtends(
65
$this->newRef(
66
DivinerAtom::TYPE_CLASS,
67
$parent_class->getConcreteString()));
68
}
69
70
// If this exists, it is `n_IMPLEMENTS_LIST`.
71
$implements = $class->getChildByIndex(3);
72
$iface_names = $implements->selectDescendantsOfType('n_CLASS_NAME');
73
foreach ($iface_names as $iface_name) {
74
$atom->addExtends(
75
$this->newRef(
76
DivinerAtom::TYPE_INTERFACE,
77
$iface_name->getConcreteString()));
78
}
79
80
$this->findAtomDocblock($atom, $class);
81
82
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
83
foreach ($methods as $method) {
84
$matom = $this->newAtom(DivinerAtom::TYPE_METHOD);
85
86
$this->findAtomDocblock($matom, $method);
87
88
$attribute_list = $method->getChildByIndex(0);
89
$attributes = $attribute_list->selectDescendantsOfType('n_STRING');
90
if ($attributes) {
91
foreach ($attributes as $attribute) {
92
$attr = strtolower($attribute->getConcreteString());
93
switch ($attr) {
94
case 'final':
95
case 'abstract':
96
case 'static':
97
$matom->setProperty($attr, true);
98
break;
99
case 'public':
100
case 'protected':
101
case 'private':
102
$matom->setProperty('access', $attr);
103
break;
104
}
105
}
106
} else {
107
$matom->setProperty('access', 'public');
108
}
109
110
$this->parseParams($matom, $method);
111
112
$matom->setName($method->getChildByIndex(2)->getConcreteString());
113
$matom->setLine($method->getLineNumber());
114
$matom->setFile($file_name);
115
116
$this->parseReturnType($matom, $method);
117
$atom->addChild($matom);
118
119
$atoms[] = $matom;
120
}
121
122
$atoms[] = $atom;
123
}
124
}
125
126
return $atoms;
127
}
128
129
private function parseParams(DivinerAtom $atom, AASTNode $func) {
130
$params = $func
131
->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST')
132
->selectDescendantsOfType('n_DECLARATION_PARAMETER');
133
134
$param_spec = array();
135
136
if ($atom->getDocblockRaw()) {
137
$metadata = $atom->getDocblockMeta();
138
} else {
139
$metadata = array();
140
}
141
142
$docs = idx($metadata, 'param');
143
if ($docs) {
144
$docs = (array)$docs;
145
$docs = array_filter($docs);
146
} else {
147
$docs = array();
148
}
149
150
if (count($docs)) {
151
if (count($docs) < count($params)) {
152
$atom->addWarning(
153
pht(
154
'This call takes %s parameter(s), but only %s are documented.',
155
phutil_count($params),
156
phutil_count($docs)));
157
}
158
}
159
160
foreach ($params as $param) {
161
$name = $param->getChildByIndex(1)->getConcreteString();
162
$dict = array(
163
'type' => $param->getChildByIndex(0)->getConcreteString(),
164
'default' => $param->getChildByIndex(2)->getConcreteString(),
165
);
166
167
if ($docs) {
168
$doc = array_shift($docs);
169
if ($doc) {
170
$dict += $this->parseParamDoc($atom, $doc, $name);
171
}
172
}
173
174
$param_spec[] = array(
175
'name' => $name,
176
) + $dict;
177
}
178
179
if ($docs) {
180
foreach ($docs as $doc) {
181
if ($doc) {
182
$param_spec[] = $this->parseParamDoc($atom, $doc, null);
183
}
184
}
185
}
186
187
// TODO: Find `assert_instances_of()` calls in the function body and
188
// add their type information here. See T1089.
189
190
$atom->setProperty('parameters', $param_spec);
191
}
192
193
private function findAtomDocblock(DivinerAtom $atom, XHPASTNode $node) {
194
$token = $node->getDocblockToken();
195
if ($token) {
196
$atom->setDocblockRaw($token->getValue());
197
return true;
198
} else {
199
$tokens = $node->getTokens();
200
if ($tokens) {
201
$prev = head($tokens);
202
while ($prev = $prev->getPrevToken()) {
203
if ($prev->isAnyWhitespace()) {
204
continue;
205
}
206
break;
207
}
208
209
if ($prev && $prev->isComment()) {
210
$value = $prev->getValue();
211
$matches = null;
212
if (preg_match('/@(return|param|task|author)/', $value, $matches)) {
213
$atom->addWarning(
214
pht(
215
'Atom "%s" is preceded by a comment containing `%s`, but '.
216
'the comment is not a documentation comment. Documentation '.
217
'comments must begin with `%s`, followed by a newline. Did '.
218
'you mean to use a documentation comment? (As the comment is '.
219
'not a documentation comment, it will be ignored.)',
220
$atom->getName(),
221
'@'.$matches[1],
222
'/**'));
223
}
224
}
225
}
226
227
$atom->setDocblockRaw('');
228
return false;
229
}
230
}
231
232
protected function parseParamDoc(DivinerAtom $atom, $doc, $name) {
233
$dict = array();
234
$split = preg_split('/\s+/', trim($doc), 2);
235
if (!empty($split[0])) {
236
$dict['doctype'] = $split[0];
237
}
238
239
if (!empty($split[1])) {
240
$docs = $split[1];
241
242
// If the parameter is documented like `@param int $num Blah blah ..`,
243
// get rid of the `$num` part (which Diviner considers optional). If it
244
// is present and different from the declared name, raise a warning.
245
$matches = null;
246
if (preg_match('/^(\\$\S+)\s+/', $docs, $matches)) {
247
if ($name !== null) {
248
if ($matches[1] !== $name) {
249
$atom->addWarning(
250
pht(
251
'Parameter "%s" is named "%s" in the documentation. '.
252
'The documentation may be out of date.',
253
$name,
254
$matches[1]));
255
}
256
}
257
$docs = substr($docs, strlen($matches[0]));
258
}
259
260
$dict['docs'] = $docs;
261
}
262
263
return $dict;
264
}
265
266
private function parseReturnType(DivinerAtom $atom, XHPASTNode $decl) {
267
$return_spec = array();
268
269
$metadata = $atom->getDocblockMeta();
270
$return = idx($metadata, 'return');
271
272
$type = null;
273
$docs = null;
274
275
if (!$return) {
276
$return = idx($metadata, 'returns');
277
if ($return) {
278
$atom->addWarning(
279
pht(
280
'Documentation uses `%s`, but should use `%s`.',
281
'@returns',
282
'@return'));
283
}
284
}
285
286
$return = (array)$return;
287
if (count($return) > 1) {
288
$atom->addWarning(
289
pht(
290
'Documentation specifies `%s` multiple times.',
291
'@return'));
292
}
293
$return = head($return);
294
295
if ($atom->getName() == '__construct' && $atom->getType() == 'method') {
296
$return_spec = array(
297
'doctype' => 'this',
298
'docs' => '//Implicit.//',
299
);
300
301
if ($return) {
302
$atom->addWarning(
303
pht(
304
'Method `%s` has explicitly documented `%s`. The `%s` method '.
305
'always returns `%s`. Diviner documents this implicitly.',
306
'__construct()',
307
'@return',
308
'__construct()',
309
'$this'));
310
}
311
} else if ($return) {
312
$split = preg_split('/(?<!,)\s+/', trim($return), 2);
313
if (!empty($split[0])) {
314
$type = $split[0];
315
}
316
317
if ($decl->getChildByIndex(1)->getTypeName() == 'n_REFERENCE') {
318
$type = $type.' &';
319
}
320
321
if (!empty($split[1])) {
322
$docs = $split[1];
323
}
324
325
$return_spec = array(
326
'doctype' => $type,
327
'docs' => $docs,
328
);
329
} else {
330
$return_spec = array(
331
'type' => 'wild',
332
);
333
}
334
335
$atom->setProperty('return', $return_spec);
336
}
337
338
}
339
340