fix PHP-7.4 compat
[mdref/mdref] / mdref / Structure.php
1 <?php
2
3 namespace mdref;
4
5 /**
6 * Structure of an entry
7 */
8 class Structure {
9 const OF_OTHER = "other";
10 const OF_NAMESPACE = "ns";
11 const OF_CLASS = "class";
12 const OF_FUNC = "func";
13
14 private $type;
15 private $struct;
16 private $entry;
17
18 function __construct(Entry $entry) {
19 $this->entry = $entry;
20
21 if ($entry->isRoot() || $entry->isNsClass()) {
22 if ($entry->isRoot()) {
23 $this->type = self::OF_NAMESPACE;
24 $this->getStructureOfRoot();
25 } elseif (!strncmp($entry->getTitle(), "namespace", strlen("namespace"))) {
26 $this->type = self::OF_NAMESPACE;
27 $this->getStructureOfNs();
28 } else {
29 $this->type = self::OF_CLASS;
30 $this->getStructureOfClass();
31 }
32 } elseif ($entry->isFunction()) {
33 $this->type = self::OF_FUNC;
34 $this->getStructureOfFunc();
35 } else {
36 $this->type = self::OF_OTHER;
37 }
38 }
39
40 static function of(Entry $entry) : StructureOf {
41 return (new static($entry))->getStruct();
42 }
43
44 function getStruct() : StructureOf {
45 return $this->struct;
46 }
47
48 function format() {
49 $this->struct->format();
50 }
51
52 private function getStructureOfFunc() : StructureOfFunc {
53 return $this->struct = new StructureOfFunc([
54 "ns" => $this->entry->getParent()->getNsName(),
55 "name" => $this->entry->getEntryName(),
56 "desc" => $this->entry->getFullDescription(),
57 "returns" => $this->getReturns(),
58 "params" => $this->getParams(),
59 "throws" => $this->getThrows()
60 ]);
61 }
62
63 private function getStructureOfClass() : StructureOfClass {
64 return $this->struct = new StructureOfClass([
65 "ns" => $this->entry->getParent()->getNsName(),
66 "name" => $this->prepareClassName(),
67 "desc" => $this->entry->getFullDescription(),
68 "consts" => $this->getConstants(),
69 "props" => $this->getProperties(),
70 "funcs" => $this->getFunctions(),
71 "classes" => $this->getClasses(),
72 ]);
73 }
74
75 private function getStructureOfNs() : StructureOfNs {
76 return $this->struct = new StructureOfNs([
77 "name" => $this->entry->getNsName(),
78 "desc" => $this->entry->getFullDescription(),
79 "consts" => $this->getConstants(),
80 "classes" => $this->getClasses(),
81 ]);
82 }
83
84 private function getStructureOfRoot() : StructureOfRoot {
85 return $this->struct = new StructureOfRoot([
86 "name" => $this->entry->getName(),
87 "desc" => $this->entry->getFile()->readIntro(),
88 "consts" => $this->getConstants(),
89 "classes" => $this->getClasses()
90 ]);
91 }
92
93 private function getSection(string $section) : string {
94 return $this->entry->getFile()->readSection($section);
95 }
96
97 private function prepareClassName() {
98 return preg_replace_callback_array([
99 '/(?P<type>class|interface|trait)\s+([\\\\\w]+\\\)?(?P<name>\w+)\s*/' => function($match) {
100 return $match["type"] . " " . $match["name"] . " ";
101 },
102 '/(?P<op>extends|implements)\s+(?P<names>[\\w]+(?:(?:,\s*[\\\\\w]+)*))/' => function ($match) {
103 return $match["op"] . " " . preg_replace('/\b(?<!\\\)(\w)/', '\\\\\\1', $match["names"]);
104 }
105 ], $this->entry->getTitle());
106 }
107
108 private function splitList(string $pattern, string $text) : array {
109 $text = trim($text);
110 if (strlen($text) && !preg_match("/^None/", $text)) {
111 if (preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) {
112 return $matches;
113 }
114 }
115 return [];
116 }
117
118 private function getConstantValue(string $name) {
119 $ns = $this->entry->getNsName();
120 if (defined("\\$ns::$name")) {
121 return constant("\\$ns::$name");
122 }
123 if (defined("\\$ns\\$name")) {
124 return constant("\\$ns\\$name");
125 }
126 return null;
127 }
128
129 private function getConstants() : array {
130 static $pattern = '/
131 \*\s+
132 (?<name>\w+)
133 (?:\s*=\s*(?P<value>.+))?
134 (?P<desc>(?:\s*\n\s*[^\*\n#].*)*)
135 /x';
136
137 $structs = [];
138 $consts = $this->splitList($pattern, $this->getSection("Constants"));
139 foreach ($consts as $const) {
140 if (!isset($const["value"]) || !strlen($const["value"])) {
141 $const["value"] = $this->getConstantValue($const["name"]);
142 }
143 $structs[$const["name"]] = new StructureOfConst($const);
144 }
145 return $structs;
146 }
147
148 private function getProperties() : array {
149 static $pattern = '/
150 \*\s+
151 (?P<modifiers>\w+\s+)*
152 (?:\((?P<usages>(?:(?:\w+)\s*)*)\))*\s*
153 (?P<type>[\\\\\w]+)\s+
154 (?<name>\$\w+)
155 (?:\s*=\s*(?P<defval>.+))?
156 (?P<desc>(?:\s*\n\s*[^\*].*)*)
157 /x';
158
159 $structs = [];
160 $props = $this->splitList($pattern, $this->getSection("Properties"));
161 foreach ($props as $prop) {
162 $structs[$prop["name"]] = new StructureOfVar($prop);
163 }
164 return $structs;
165 }
166
167 private function getFunctions() : array {
168 $structs = [];
169 foreach ($this->entry as $sub) {
170 if ($sub->isFunction()) {
171 $structs[$sub->getEntryName()] = static::of($sub);
172 }
173 }
174 return $structs;
175 }
176
177 private function getClasses() : array {
178 $structs = [];
179 foreach ($this->entry as $sub) {
180 if ($sub->isNsClass()) {
181 $structs[$sub->getEntryName()] = static::of($sub);
182 }
183 }
184 return $structs;
185 }
186
187 private function getParams() : array {
188 static $pattern = '/
189 \*\s+
190 (?P<modifiers>\w+\s+)*
191 (?P<type>[\\\\\w_]+)\s+
192 (?P<ref>&)?(?P<name>\$[\w_]+)
193 (?:\s*=\s*(?P<defval>.+))?
194 (?P<desc>(?:\s*[^*]*\n(?!\n)\s*[^\*].*)*)
195 /x';
196
197 $structs = [];
198 $params = $this->splitList($pattern, $this->getSection("Params"));
199 foreach ($params as $param) {
200 $structs[$param["name"]] = new StructureOfVar($param);
201 }
202 return $structs;
203 }
204
205 private function getReturns() : array {
206 static $pattern = '/
207 \*\s+
208 (?<type>[\\\\\w_]+)
209 \s*,?\s*
210 (?P<desc>(?:.|\n(?!\s*\*))*)
211 /x';
212
213 $returns = $this->splitList($pattern, $this->getSection("Returns"));
214 $retvals = [];
215 foreach ($returns as list(, $type, $desc)) {
216 $retvals[] = [$type, $desc];
217 }
218 return $retvals;
219 return [implode("|", array_unique(array_column($returns, "type"))), $retdesc];
220 }
221
222 private function getThrows() : array {
223 static $pattern = '/
224 \*\s+
225 (?P<exception>[\\\\\w]+)\s*
226 /x';
227
228 $throws = $this->splitList($pattern, $this->getSection("Throws"));
229 return array_column($throws, "exception");
230 }
231 }
232
233 abstract class StructureOf {
234 function __construct(array $props = []) {
235 foreach ($props as $key => $val) {
236 if (is_int($key)) {
237 continue;
238 }
239 if (!property_exists(static::class, $key)) {
240 throw new \UnexpectedValueException(
241 sprintf("Property %s::\$%s does not exist", static::class, $key)
242 );
243 }
244 if ($key === "desc" || $key === "modifiers" || $key === "defval") {
245 $val = trim($val);
246 }
247 $this->$key = $val;
248 }
249 }
250
251 // abstract function format();
252
253 function formatDesc($level, array $tags = []) {
254 $indent = str_repeat("\t", $level);
255 $desc = trim($this->desc);
256 if (false !== stristr($desc, "deprecated in")) {
257 $tags[] = "deprecated";
258 }
259 if ($tags) {
260 $desc .= "\n\n@" . implode("\n@", $tags);
261 }
262 $desc = preg_replace('/[\t ]*\n/',"\n$indent * ", $desc);
263 printf("%s/**\n%s * %s\n%s */\n", $indent, $indent, $desc, $indent);
264 }
265
266 function saneTypes(array $types) {
267 $sane = [];
268 foreach ($types as $type) {
269 if (strlen($s = $this->saneType($type, false))) {
270 $sane[] = $s;
271 }
272 }
273 return $sane;
274 }
275
276 function saneType($type, $strict = true) {
277 switch (strtolower($type)) {
278 case "object":
279 case "resource":
280 case "stream":
281 case "mixed":
282 case "true":
283 case "false":
284 case "null":
285 if ($strict) {
286 break;
287 }
288 /* fallthrough */
289 case "bool":
290 case "int":
291 case "float":
292 case "string":
293 case "array":
294 case "callable":
295 return $type;
296 break;
297 default:
298 return ($type[0] === "\\" ? "":"\\") . $type;
299 break;
300 }
301 }
302 }
303
304 class StructureOfRoot extends StructureOf {
305 public $name;
306 public $desc;
307 public $consts;
308 public $classes;
309
310 function format() {
311 $this->formatDesc(0);
312
313 foreach ($this->consts as $const) {
314 $const->format(0);
315 printf(";\n");
316 }
317
318 printf("namespace %s;\nuse %s;\n", $this->name, $this->name);
319 StructureOfNs::$last = $this->name;
320
321 foreach ($this->getClasses() as $class) {
322 $class->format();
323 }
324 }
325
326 function getClasses() {
327 yield from $this->classes;
328 foreach ($this->classes as $class) {
329 yield from $class->getClasses();
330 }
331 }
332 }
333 class StructureOfNs extends StructureOfRoot {
334 public $funcs;
335
336 public static $last;
337
338 function format() {
339 print $this->formatDesc(0);
340
341 if (strlen($this->name) && $this->name !== StructureOfNs::$last) {
342 StructureOfNs::$last = $this->name;
343 printf("namespace %s;\n", $this->name);
344 }
345 foreach ($this->consts as $const) {
346 $const->format(0);
347 printf(";\n");
348 }
349 }
350 }
351
352 class StructureOfClass extends StructureOfNs
353 {
354 public $ns;
355 public $props;
356
357 function format() {
358 if ($this->ns !== StructureOfNs::$last) {
359 printf("namespace %s;\n", $this->ns);
360 StructureOfNs::$last = $this->ns;
361 }
362
363 print $this->formatDesc(0);
364 printf("%s {\n", $this->name);
365
366 foreach ($this->consts as $const) {
367 $const->format(1);
368 printf(";\n");
369 }
370
371 foreach ($this->props as $prop) {
372 $prop->formatAsProp(1);
373 printf(";\n");
374 }
375
376 foreach ($this->funcs as $func) {
377 $func->format(1);
378 if (strncmp($this->name, "interface", strlen("interface"))) {
379 printf(" {}\n");
380 } else {
381 printf(";\n");
382 }
383 }
384
385 printf("}\n");
386 }
387 }
388
389 class StructureOfFunc extends StructureOf {
390 public $ns;
391 public $class;
392 public $name;
393 public $desc;
394 public $params;
395 public $returns;
396 public $throws;
397
398 function omitParamTypes() {
399 switch ($this->name) {
400 // ArrayAccess
401 case "offsetGet":
402 case "offsetSet":
403 case "offsetExists":
404 case "offsetUnset":
405 // Serializable
406 case "serialize":
407 case "unserialize":
408 return true;
409 }
410 return false;
411 }
412
413 function format(int $level) {
414 $tags = [];
415 foreach ($this->params as $param) {
416 $type = $this->saneType($param->type, false);
417 $tags[] = "param {$type} {$param->name} {$param->desc}";
418 }
419 foreach ($this->throws as $throws) {
420 $tags[] = "throws " . $this->saneType($throws);
421 }
422 if ($this->name !== "__construct" && $this->returns) {
423
424 if (count($this->returns) > 1) {
425 $type = implode("|", $this->saneTypes(array_column($this->returns, 0)));
426 $desc = "";
427 foreach ($this->returns as list($typ, $ret)) {
428 if (strlen($desc)) {
429 $desc .= "\n\t\t or ";
430 }
431 $desc .= $this->saneType($typ, false) . " " . $ret;
432 }
433 } else {
434 $type = $this->saneType($this->returns[0][0], false);
435 $desc = $this->returns[0][1];
436 }
437 $tags[] = "return $type $desc";
438 }
439 $this->formatDesc(1, $tags);
440 printf("\tfunction %s(", $this->name);
441 $comma = "";
442 $omit = $this->omitParamTypes();
443 foreach ($this->params as $param) {
444 print $comma;
445 $param->formatAsParam($level, !$omit);
446 $comma = ", ";
447 }
448 printf(")");
449 }
450 }
451
452 class StructureOfConst extends StructureOf {
453 public $name;
454 public $desc;
455 public $value;
456
457 function format(int $level) {
458 $indent = str_repeat("\t", $level);
459 $this->formatDesc($level);
460 printf("%sconst %s = ", $indent, $this->name);
461 var_export($this->value);
462 }
463 }
464
465 class StructureOfVar extends StructureOf {
466 public $name;
467 public $type;
468 public $desc;
469 public $modifiers;
470 public $usages;
471 public $defval;
472 public $ref;
473
474 function formatDefval() {
475 if (strlen($this->defval)) {
476 if (false && defined($this->defval)) {
477 printf(" = ");
478 var_export(constant($this->defval));
479 } else if (strlen($this->defval)) {
480 if (false !== strchr($this->defval, "\\") && $this->defval[0] != "\\") {
481 $this->defval = "\\" . $this->defval;
482 }
483 printf(" = %s", $this->defval);
484 }
485 } elseif ($this->modifiers) {
486 if (stristr($this->modifiers, "optional") !== false) {
487 printf(" = NULL");
488 }
489 }
490 }
491 function formatAsProp($level) {
492 $indent = str_repeat("\t", $level);
493 $this->formatDesc($level,
494 preg_split('/\s+/', $this->modifiers ." " . $this->usages, -1, PREG_SPLIT_NO_EMPTY)
495 + [-1 => "var " . $this->saneType($this->type)]
496 );
497 printf("%s%s %s", $indent, $this->modifiers, $this->name);
498 $this->formatDefval();
499 }
500
501 function formatAsParam($level, $with_type = true) {
502 if ($with_type && strlen($type = $this->saneType($this->type))) {
503 printf("%s ", $type);
504 }
505 printf("%s%s", $this->ref, $this->name);
506 $this->formatDefval();
507 }
508 }