1: <?php declare(strict_types = 1);
2:
3: namespace ApiGen;
4:
5: use ApiGen\Index\FileIndex;
6: use ApiGen\Index\Index;
7: use ApiGen\Index\NamespaceIndex;
8: use ApiGen\Info\ClassInfo;
9: use ApiGen\Info\ClassLikeInfo;
10: use ApiGen\Info\EnumInfo;
11: use ApiGen\Info\FunctionInfo;
12: use ApiGen\Info\InterfaceInfo;
13: use ApiGen\Info\MissingInfo;
14: use ApiGen\Info\NameInfo;
15: use ApiGen\Info\TraitInfo;
16:
17: use function array_filter;
18: use function array_keys;
19: use function array_map;
20: use function count;
21: use function implode;
22: use function ksort;
23:
24:
25: class Indexer
26: {
27: public function indexFile(Index $index, ?string $file, bool $primary): void
28: {
29: if ($file === null) {
30: $file = '';
31: }
32:
33: if (isset($index->files[$file])) {
34: $index->files[$file]->primary = $index->files[$file]->primary || $primary;
35: return;
36: }
37:
38: $index->files[$file] = new FileIndex($file, $primary);
39: }
40:
41:
42: public function indexNamespace(Index $index, string $namespace, string $namespaceLower, bool $primary, bool $deprecated): void
43: {
44: if (isset($index->namespace[$namespaceLower])) {
45: if ($primary) {
46: for (
47: $info = $index->namespace[$namespaceLower];
48: $info !== null && !$info->primary;
49: $info = $info->name->fullLower === '' ? null : $index->namespace[$info->name->namespaceLower]
50: ) {
51: $info->primary = true;
52: }
53: }
54:
55: if (!$deprecated) {
56: for (
57: $info = $index->namespace[$namespaceLower];
58: $info !== null && $info->deprecated;
59: $info = $info->name->fullLower === '' ? null : $index->namespace[$info->name->namespaceLower]
60: ) {
61: $info->deprecated = false;
62: }
63: }
64:
65: return;
66: }
67:
68: $info = new NamespaceIndex(new NameInfo($namespace, $namespaceLower), $primary, $deprecated);
69:
70: if ($namespaceLower !== '') {
71: $primary = $primary && $info->name->namespaceLower !== '';
72: $deprecated = $deprecated && $info->name->namespaceLower !== '';
73: $this->indexNamespace($index, $info->name->namespace, $info->name->namespaceLower, $primary, $deprecated);
74: }
75:
76: $index->namespace[$namespaceLower] = $info;
77: $index->namespace[$info->name->namespaceLower]->children[$info->name->shortLower] = $info;
78: }
79:
80:
81: public function indexClassLike(Index $index, ClassLikeInfo $info): void
82: {
83: $index->classLike[$info->name->fullLower] = $info;
84:
85: foreach ($info->constants as $constantName => $_) {
86: $index->constants[$constantName][] = $info;
87: }
88:
89: foreach ($info->properties as $propertyName => $_) {
90: $index->properties[$propertyName][] = $info;
91: }
92:
93: foreach ($info->methods as $methodLowerName => $_) {
94: $index->methods[$methodLowerName][] = $info;
95: }
96:
97: if ($info instanceof ClassInfo) {
98: $this->indexClass($info, $index);
99:
100: } elseif ($info instanceof InterfaceInfo) {
101: $this->indexInterface($info, $index);
102:
103: } elseif ($info instanceof TraitInfo) {
104: $this->indexTrait($info, $index);
105:
106: } elseif ($info instanceof EnumInfo) {
107: $this->indexEnum($info, $index);
108:
109: } elseif ($info instanceof MissingInfo) {
110: $this->indexMissing($info, $index);
111:
112: } else {
113: throw new \LogicException();
114: }
115: }
116:
117:
118: public function indexFunction(Index $index, FunctionInfo $info): void
119: {
120: $index->function[$info->name->fullLower] = $info;
121: $index->files[$info->file ?? '']->function[$info->name->fullLower] = $info;
122: $index->namespace[$info->name->namespaceLower]->function[$info->name->shortLower] = $info;
123: }
124:
125:
126: public function postProcess(Index $index): void
127: {
128: // DAG
129: $this->indexDirectedAcyclicGraph($index);
130:
131: // instance of
132: foreach ([$index->class, $index->interface, $index->enum] as $infos) {
133: foreach ($infos as $info) {
134: $this->indexInstanceOf($index, $info);
135: }
136: }
137:
138: // exceptions
139: foreach ($index->namespace as $namespaceIndex) {
140: foreach ($namespaceIndex->class as $info) {
141: if ($info->isThrowable($index)) {
142: unset($namespaceIndex->class[$info->name->shortLower]);
143: $namespaceIndex->exception[$info->name->shortLower] = $info;
144: }
145: }
146: }
147:
148: // method overrides & implements
149: foreach ($index->classExtends[''] ?? [] as $info) {
150: $this->indexClassMethodOverrides($index, $info, [], []);
151: }
152:
153: foreach ($index->enum as $info) {
154: $this->indexClassMethodOverrides($index, $info, [], []);
155: }
156:
157: // sort
158: $this->sort($index);
159: }
160:
161:
162: protected function indexClass(ClassInfo $info, Index $index): void
163: {
164: $index->class[$info->name->fullLower] = $info;
165: $index->files[$info->file ?? '']->classLike[$info->name->fullLower] = $info;
166: $index->namespace[$info->name->namespaceLower]->class[$info->name->shortLower] = $info;
167: $index->classExtends[$info->extends ? $info->extends->fullLower : ''][$info->name->fullLower] = $info;
168:
169: foreach ($info->implements as $interfaceNameLower => $interfaceName) {
170: $index->classImplements[$interfaceNameLower][$info->name->fullLower] = $info;
171: }
172:
173: foreach ($info->uses as $traitNameLower => $traitName) {
174: $index->classUses[$traitNameLower][$info->name->fullLower] = $info;
175: }
176: }
177:
178:
179: protected function indexInterface(InterfaceInfo $info, Index $index): void
180: {
181: $index->interface[$info->name->fullLower] = $info;
182: $index->files[$info->file ?? '']->classLike[$info->name->fullLower] = $info;
183: $index->namespace[$info->name->namespaceLower]->interface[$info->name->shortLower] = $info;
184:
185: if ($info->extends) {
186: foreach ($info->extends as $interfaceNameLower => $interfaceName) {
187: $index->interfaceExtends[$interfaceNameLower][$info->name->fullLower] = $info;
188: }
189:
190: } else {
191: $index->interfaceExtends[''][$info->name->fullLower] = $info;
192: }
193: }
194:
195:
196: protected function indexTrait(TraitInfo $info, Index $index): void
197: {
198: $index->trait[$info->name->fullLower] = $info;
199: $index->files[$info->file ?? '']->classLike[$info->name->fullLower] = $info;
200: $index->namespace[$info->name->namespaceLower]->trait[$info->name->shortLower] = $info;
201: }
202:
203:
204: protected function indexEnum(EnumInfo $info, Index $index): void
205: {
206: $index->enum[$info->name->fullLower] = $info;
207: $index->files[$info->file ?? '']->classLike[$info->name->fullLower] = $info;
208: $index->namespace[$info->name->namespaceLower]->enum[$info->name->shortLower] = $info;
209:
210: foreach ($info->implements as $interfaceNameLower => $interfaceName) {
211: $index->enumImplements[$interfaceNameLower][$info->name->fullLower] = $info;
212: }
213: }
214:
215:
216: protected function indexMissing(MissingInfo $info, Index $index): void
217: {
218: // nothing to index
219: }
220:
221:
222: protected function indexDirectedAcyclicGraph(Index $index): void
223: {
224: $edgeGroups = [
225: 'class extends' => $index->classExtends,
226: 'class implements' => $index->classImplements,
227: 'class uses' => $index->classUses,
228: 'interface extends' => $index->interfaceExtends,
229: 'enum implements' => $index->enumImplements,
230: ];
231:
232: $dag = [];
233: foreach ($edgeGroups as $edgeGroup) {
234: foreach ($edgeGroup as $classLikeNameA => $classLikeGroup) {
235: foreach ($classLikeGroup as $classLikeNameB => $classLike) {
236: if (isset($dag[$classLikeNameA][$classLikeNameB])) {
237: $classLikeA = $index->classLike[$classLikeNameA];
238: $classLikeB = $index->classLike[$classLikeNameB];
239: $duplicateEdgeGroups = array_filter($edgeGroups, fn(array $edgeGroup) => isset($edgeGroup[$classLikeNameA][$classLikeNameB]));
240: $note = '(used both as \'' . implode('\' and \'', array_keys($duplicateEdgeGroups)) . '\')';
241: $path = "{$classLikeB->name->full} -> {$classLikeA->name->full}";
242: throw new \RuntimeException("Invalid directed acyclic graph because it contains duplicate edge {$note}:\n{$path}");
243: }
244:
245: $dag[$classLikeNameA][$classLikeNameB] = $classLike;
246: }
247: }
248: }
249:
250: $findCycle = static function (array $node, array $visited) use ($index, $dag, &$findCycle): void {
251: foreach ($node as $childKey => $_) {
252: if (isset($visited[$childKey])) {
253: $path = [...array_keys($visited), $childKey];
254: $path = array_map(fn(string $item) => $index->classLike[$item]->name->full, $path);
255: throw new \RuntimeException("Invalid directed acyclic graph because it contains cycle:\n" . implode(' -> ', $path));
256:
257: } else {
258: $findCycle($dag[$childKey] ?? [], $visited + [$childKey => true]);
259: }
260: }
261: };
262:
263: foreach ($dag as $nodeKey => $node) {
264: $findCycle($node, [$nodeKey => true]);
265: }
266:
267: $index->dag = $dag;
268: }
269:
270:
271: protected function indexInstanceOf(Index $index, ClassLikeInfo $info): void
272: {
273: if (isset($index->instanceOf[$info->name->fullLower])) {
274: return; // already computed
275: }
276:
277: $index->instanceOf[$info->name->fullLower] = [$info->name->fullLower => $info];
278: foreach ([$index->classExtends, $index->classImplements, $index->interfaceExtends, $index->enumImplements] as $edges) {
279: foreach ($edges[$info->name->fullLower] ?? [] as $childInfo) {
280: $this->indexInstanceOf($index, $childInfo);
281: $index->instanceOf[$info->name->fullLower] += $index->instanceOf[$childInfo->name->fullLower];
282: }
283: }
284: }
285:
286:
287: /**
288: * @param ClassLikeInfo[] $normal indexed by [methodName]
289: * @param ClassLikeInfo[] $abstract indexed by [methodName]
290: */
291: protected function indexClassMethodOverrides(Index $index, ClassInfo|EnumInfo $info, array $normal, array $abstract): void
292: {
293: $stack = array_keys($info->implements);
294: $stackIndex = count($stack);
295:
296: while ($stackIndex > 0) {
297: $interfaceNameLower = $stack[--$stackIndex];
298: $interface = $index->interface[$interfaceNameLower] ?? null;
299:
300: if ($interface !== null) {
301: foreach ($interface->methods as $method) {
302: $abstract[$method->nameLower] = $interface;
303: }
304:
305: foreach ($interface->extends as $extendLower => $extend) {
306: $stack[$stackIndex++] = $extendLower;
307: }
308: }
309: }
310:
311: foreach ($info->methods as $method) {
312: if ($method->private) {
313: continue;
314: }
315:
316: if (isset($normal[$method->nameLower])) {
317: $ancestor = $normal[$method->nameLower];
318: $index->methodOverrides[$info->name->fullLower][$method->nameLower][] = $ancestor;
319: $index->methodOverriddenBy[$ancestor->name->fullLower][$method->nameLower][] = $info;
320: }
321:
322: if (isset($abstract[$method->nameLower])) {
323: $ancestor = $abstract[$method->nameLower];
324: $index->methodImplements[$info->name->fullLower][$method->nameLower][] = $ancestor;
325: $index->methodImplementedBy[$ancestor->name->fullLower][$method->nameLower][] = $info;
326: }
327:
328: if ($method->abstract) {
329: $abstract[$method->nameLower] = $info;
330:
331: } else {
332: unset($abstract[$method->nameLower]);
333: $normal[$method->nameLower] = $info;
334: }
335: }
336:
337: foreach ($index->classExtends[$info->name->fullLower] ?? [] as $child) {
338: $this->indexClassMethodOverrides($index, $child, $normal, $abstract);
339: }
340: }
341:
342:
343: protected function sort(Index $index): void
344: {
345: ksort($index->files);
346: ksort($index->namespace);
347: ksort($index->classLike);
348: ksort($index->class);
349: ksort($index->interface);
350: ksort($index->trait);
351: ksort($index->enum);
352:
353: foreach ($index->classExtends as &$arr) {
354: ksort($arr);
355: }
356:
357: foreach ($index->classImplements as &$arr) {
358: ksort($arr);
359: }
360:
361: foreach ($index->classUses as &$arr) {
362: ksort($arr);
363: }
364:
365: foreach ($index->interfaceExtends as &$arr) {
366: ksort($arr);
367: }
368:
369: foreach ($index->enumImplements as &$arr) {
370: ksort($arr);
371: }
372:
373: foreach ($index->namespace as $namespaceIndex) {
374: ksort($namespaceIndex->class);
375: ksort($namespaceIndex->interface);
376: ksort($namespaceIndex->trait);
377: ksort($namespaceIndex->enum);
378: ksort($namespaceIndex->exception);
379: ksort($namespaceIndex->children);
380: }
381:
382: // move root namespace to end
383: if (isset($index->namespace[''])) {
384: $rootNamespace = $index->namespace[''];
385: unset($index->namespace[''], $rootNamespace->children['']);
386: $index->namespace[''] = $rootNamespace;
387: $rootNamespace->children[''] = $rootNamespace;
388: }
389: }
390: }
391: