@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @phutil-external-symbol class PhpParser\Node
5 * @phutil-external-symbol class PhpParser\NodeTraverser
6 * @phutil-external-symbol class PhpParser\Node\FunctionLike
7 * @phutil-external-symbol class PhpParser\NodeVisitor\FindingVisitor
8 * @phutil-external-symbol class PhpParser\NodeVisitor\NameResolver
9 * @phutil-external-symbol class PhpParser\Node\Stmt\Class_
10 * @phutil-external-symbol class PhpParser\Node\Stmt\ClassLike
11 * @phutil-external-symbol class PhpParser\Node\Stmt\Enum_
12 * @phutil-external-symbol class PhpParser\Node\Stmt\Function_
13 * @phutil-external-symbol class PhpParser\Node\Stmt\Interface_
14 * @phutil-external-symbol class PhpParser\Node\Stmt\Trait_
15 * @phutil-external-symbol class PhpParser\PrettyPrinter\Standard
16 */
17final class DivinerPHPAtomizer extends DivinerAtomizer {
18
19 protected function newAtom($type) {
20 return parent::newAtom($type)->setLanguage('php');
21 }
22
23 protected function executeAtomize($file_name, $file_data) {
24 $parser = PhutilPHPParserLibrary::getParser();
25 $ast = $parser->parse($file_data);
26
27 $classlike_finder = new PhpParser\NodeVisitor\FindingVisitor(
28 function ($node) {
29 return $node instanceof PhpParser\Node\Stmt\ClassLike;
30 });
31 $function_finder = new PhpParser\NodeVisitor\FindingVisitor(
32 function ($node) {
33 return $node instanceof PhpParser\Node\Stmt\Function_;
34 });
35
36 $namespace_resolver = new PhpParser\NodeVisitor\NameResolver();
37 $traverser = new PhpParser\NodeTraverser();
38 $traverser->addVisitor($namespace_resolver);
39 $traverser->addVisitor($classlike_finder);
40 $traverser->addVisitor($function_finder);
41 $traverser->traverse($ast);
42
43 $atoms = array();
44
45 foreach ($function_finder->getFoundNodes() as $func) {
46 $atom = $this->newAtom(DivinerAtom::TYPE_FUNCTION)
47 ->setName($func->namespacedName->toString())
48 ->setLine($func->getStartLine())
49 ->setFile($file_name);
50
51 $this->findAtomDocblock($atom, $func);
52 $this->parseParams($atom, $func);
53 $this->parseReturnType($atom, $func);
54
55 $atoms[] = $atom;
56 }
57
58 $class_types = array(
59 PhpParser\Node\Stmt\Class_::class => DivinerAtom::TYPE_CLASS,
60 PhpParser\Node\Stmt\Interface_::class => DivinerAtom::TYPE_INTERFACE,
61 PhpParser\Node\Stmt\Trait_::class => DivinerAtom::TYPE_TRAIT,
62 PhpParser\Node\Stmt\Enum_::class => DivinerAtom::TYPE_ENUM,
63 );
64
65 foreach ($classlike_finder->getFoundNodes() as $class) {
66 $atom_type = $class_types[get_class($class)];
67
68 // Don't analyze anonymous classes.
69 if (!$class->name) {
70 continue;
71 }
72
73 $atom = $this->newAtom($atom_type)
74 ->setName($class->namespacedName->toString())
75 ->setFile($file_name)
76 ->setLine($class->getStartLine());
77
78 if ($class instanceof PhpParser\Node\Stmt\Class_) {
79 if ($class->isAbstract()) {
80 $atom->setProperty('abstract', true);
81 } else if ($class->isFinal()) {
82 $atom->setProperty('final', true);
83 } else if ($class->isReadonly()) {
84 $atom->setProperty('readonly', true);
85 }
86
87 if ($class->extends) {
88 $atom->addExtends(
89 $this->newRef(
90 DivinerAtom::TYPE_CLASS,
91 $class->extends->toString()));
92 }
93
94 foreach ($class->implements as $implement) {
95 $atom->addExtends(
96 $this->newRef(
97 DivinerAtom::TYPE_INTERFACE,
98 $implement->toString()));
99 }
100 } else if ($class instanceof PhpParser\Node\Stmt\Interface_) {
101 foreach ($class->extends as $extend) {
102 $atom->addExtends(
103 $this->newRef(
104 DivinerAtom::TYPE_INTERFACE,
105 $extend->toString()));
106 }
107 } else if ($class instanceof PhpParser\Node\Stmt\Enum_) {
108 foreach ($class->implements as $implement) {
109 $atom->addExtends(
110 $this->newRef(
111 DivinerAtom::TYPE_INTERFACE,
112 $implement->toString()));
113 }
114 }
115
116 $this->findAtomDocblock($atom, $class);
117
118 foreach ($class->getMethods() as $method) {
119 $matom = $this->newAtom(DivinerAtom::TYPE_METHOD)
120 ->setName($method->name->toString())
121 ->setLine($method->getStartLine())
122 ->setFile($file_name);
123
124 $this->findAtomDocblock($matom, $method);
125
126 if ($method->isFinal()) {
127 $matom->setProperty('final', true);
128 }
129
130 if ($method->isAbstract()) {
131 $matom->setProperty('abstract', true);
132 }
133
134 if ($method->isStatic()) {
135 $matom->setProperty('static', true);
136 }
137
138 if ($method->isPrivate()) {
139 $matom->setProperty('access', 'private');
140 } else if ($method->isProtected()) {
141 $matom->setProperty('access', 'protected');
142 } else {
143 $matom->setProperty('access', 'public');
144 }
145
146 $this->parseParams($matom, $method);
147
148 $this->parseReturnType($matom, $method);
149 $atom->addChild($matom);
150
151 $atoms[] = $matom;
152 }
153
154 $atoms[] = $atom;
155 }
156
157 return $atoms;
158 }
159
160 private function parseParams(
161 DivinerAtom $atom,
162 PhpParser\Node\FunctionLike $func) {
163
164 $params = $func->getParams();
165
166 $param_spec = array();
167
168 if ($atom->getDocblockRaw()) {
169 $metadata = $atom->getDocblockMeta();
170 } else {
171 $metadata = array();
172 }
173
174 $docs = idx($metadata, 'param');
175 if ($docs) {
176 $docs = (array)$docs;
177 $docs = array_filter($docs);
178 } else {
179 $docs = array();
180 }
181
182 if (count($docs)) {
183 if (count($docs) < count($params)) {
184 $atom->addWarning(
185 pht(
186 'This call takes %s parameter(s), but only %s are documented.',
187 phutil_count($params),
188 phutil_count($docs)));
189 }
190 }
191
192 foreach ($params as $param) {
193 $name = '$'.$param->var->name;
194 $dict = array(
195 'type' => $this->stringify($param->type),
196 'default' => $this->stringify($param->default),
197 );
198
199 if ($docs) {
200 $doc = array_shift($docs);
201 if ($doc) {
202 $dict += $this->parseParamDoc($atom, $doc, $name);
203 }
204 }
205
206 $param_spec[] = array(
207 'name' => $name,
208 ) + $dict;
209 }
210
211 if ($docs) {
212 foreach ($docs as $doc) {
213 if ($doc) {
214 $param_spec[] = $this->parseParamDoc($atom, $doc, null);
215 }
216 }
217 }
218
219 // TODO: Find `assert_instances_of()` calls in the function body and
220 // add their type information here. See T1089.
221
222 $atom->setProperty('parameters', $param_spec);
223 }
224
225 private function findAtomDocblock(DivinerAtom $atom, PhpParser\Node $node) {
226 $doc_comment = $node->getDocComment();
227
228 if ($doc_comment) {
229 $atom->setDocblockRaw($doc_comment->getText());
230 } else {
231 $comments = $node->getComments();
232
233 foreach ($comments as $comment) {
234 $value = $comment->getText();
235 $matches = null;
236 if (preg_match('/@(return|param|task|author)/', $value, $matches)) {
237 $atom->addWarning(
238 pht(
239 'Atom "%s" is preceded by a comment containing `%s`, but '.
240 'the comment is not a documentation comment. Documentation '.
241 'comments must begin with `%s`, followed by a newline. Did '.
242 'you mean to use a documentation comment? (As the comment is '.
243 'not a documentation comment, it will be ignored.)',
244 $atom->getName(),
245 '@'.$matches[1],
246 '/**'));
247 }
248 }
249
250 $atom->setDocblockRaw('');
251 }
252 }
253
254 protected function parseParamDoc(DivinerAtom $atom, $doc, $name) {
255 $dict = array();
256 $split = preg_split('/(?<!,)\s+/', trim($doc), 2);
257 if (!empty($split[0])) {
258 $dict['doctype'] = $split[0];
259 }
260
261 if (!empty($split[1])) {
262 $docs = $split[1];
263
264 // If the parameter is documented like `@param int $num Blah blah ..`,
265 // get rid of the `$num` part (which Diviner considers optional). If it
266 // is present and different from the declared name, raise a warning.
267 $matches = null;
268 if (preg_match('/^(\\$\S+)\s+/', $docs, $matches)) {
269 if ($name !== null) {
270 if ($matches[1] !== $name) {
271 $atom->addWarning(
272 pht(
273 'Parameter "%s" is named "%s" in the documentation. '.
274 'The documentation may be out of date.',
275 $name,
276 $matches[1]));
277 }
278 }
279 $docs = substr($docs, strlen($matches[0]));
280 }
281
282 $dict['docs'] = $docs;
283 }
284
285 return $dict;
286 }
287
288 private function parseReturnType(
289 DivinerAtom $atom,
290 PhpParser\Node\FunctionLike $decl) {
291
292 $return_spec = array();
293
294 $metadata = $atom->getDocblockMeta();
295 $return = idx($metadata, 'return');
296
297 $type = null;
298 $docs = null;
299
300 if (!$return) {
301 $return = idx($metadata, 'returns');
302 if ($return) {
303 $atom->addWarning(
304 pht(
305 'Documentation uses `%s`, but should use `%s`.',
306 '@returns',
307 '@return'));
308 }
309 }
310
311 $return = (array)$return;
312 if (count($return) > 1) {
313 $atom->addWarning(
314 pht(
315 'Documentation specifies `%s` multiple times.',
316 '@return'));
317 }
318 $return = head($return);
319
320 if ($atom->getName() == '__construct' && $atom->getType() == 'method') {
321 $return_spec = array(
322 'doctype' => 'this',
323 'docs' => '//Implicit.//',
324 );
325
326 if ($return) {
327 $atom->addWarning(
328 pht(
329 'Method `%s` has explicitly documented `%s`. The `%s` method '.
330 'always returns `%s`. Diviner documents this implicitly.',
331 '__construct()',
332 '@return',
333 '__construct()',
334 '$this'));
335 }
336 } else if ($return) {
337 $split = preg_split('/(?<!,)\s+/', trim($return), 2);
338 if (!empty($split[0])) {
339 $type = $split[0];
340 }
341
342 if ($decl->returnsByRef()) {
343 $type = $type.' &';
344 }
345
346 if (!empty($split[1])) {
347 $docs = $split[1];
348 }
349
350 $return_spec = array(
351 'doctype' => $type,
352 'docs' => $docs,
353 );
354 } else {
355 $return_spec = array(
356 'type' => 'wild',
357 );
358 }
359
360 $atom->setProperty('return', $return_spec);
361 }
362
363 private function stringify(?PhpParser\Node $node) {
364 if (!$node) {
365 return '';
366 }
367
368 return id(new PhpParser\PrettyPrinter\Standard())
369 ->prettyPrint(array($node));
370 }
371
372}