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: | |
129: | $this->indexDirectedAcyclicGraph($index); |
130: | |
131: | |
132: | foreach ([$index->class, $index->interface, $index->enum] as $infos) { |
133: | foreach ($infos as $info) { |
134: | $this->indexInstanceOf($index, $info); |
135: | } |
136: | } |
137: | |
138: | |
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: | |
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: | |
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: | |
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; |
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: | |
289: | |
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: | |
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: | |