1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace ApiGen; |
4: | |
5: | use ApiGen\Analyzer\AnalyzeResult; |
6: | use ApiGen\Index\Index; |
7: | use ApiGen\Info\ErrorKind; |
8: | use Nette\Utils\Finder; |
9: | use Symfony\Component\Console\Helper\ProgressBar; |
10: | use Symfony\Component\Console\Style\OutputStyle; |
11: | |
12: | use function array_column; |
13: | use function array_slice; |
14: | use function count; |
15: | use function hrtime; |
16: | use function implode; |
17: | use function is_dir; |
18: | use function is_file; |
19: | use function memory_get_peak_usage; |
20: | use function memory_reset_peak_usage; |
21: | use function sprintf; |
22: | |
23: | use const PHP_VERSION_ID; |
24: | |
25: | |
26: | class ApiGen |
27: | { |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | public function __construct( |
34: | protected OutputStyle $output, |
35: | protected Analyzer $analyzer, |
36: | protected Indexer $indexer, |
37: | protected Renderer $renderer, |
38: | protected array $paths, |
39: | protected array $include, |
40: | protected array $exclude, |
41: | ) { |
42: | } |
43: | |
44: | |
45: | public function generate(): bool |
46: | { |
47: | $files = $this->findFiles(); |
48: | |
49: | PHP_VERSION_ID >= 80200 && memory_reset_peak_usage(); |
50: | $analyzeTime = -hrtime(true); |
51: | $analyzeResult = $this->analyze($files); |
52: | $analyzeTime += hrtime(true); |
53: | $analyzeMemory = memory_get_peak_usage(); |
54: | |
55: | PHP_VERSION_ID >= 80200 && memory_reset_peak_usage(); |
56: | $indexTime = -hrtime(true); |
57: | $index = $this->index($analyzeResult); |
58: | $indexTime += hrtime(true); |
59: | $indexMemory = memory_get_peak_usage(); |
60: | |
61: | PHP_VERSION_ID >= 80200 && memory_reset_peak_usage(); |
62: | $renderTime = -hrtime(true); |
63: | $this->render($index); |
64: | $renderTime += hrtime(true); |
65: | $renderMemory = memory_get_peak_usage(); |
66: | |
67: | $this->performance($analyzeTime, $analyzeMemory, $indexTime, $indexMemory, $renderTime, $renderMemory); |
68: | return $this->finish($analyzeResult); |
69: | } |
70: | |
71: | |
72: | |
73: | |
74: | |
75: | protected function findFiles(): array |
76: | { |
77: | $files = []; |
78: | $dirs = []; |
79: | |
80: | foreach ($this->paths as $path) { |
81: | if (is_file($path)) { |
82: | $files[] = $path; |
83: | |
84: | } elseif (is_dir($path)) { |
85: | $dirs[] = $path; |
86: | |
87: | } else { |
88: | $this->output->error(sprintf('Path "%s" does not exist.', $path)); |
89: | } |
90: | } |
91: | |
92: | if (count($dirs) > 0) { |
93: | $finder = Finder::findFiles($this->include) |
94: | ->exclude($this->exclude) |
95: | ->from($dirs); |
96: | |
97: | foreach ($finder as $file => $_) { |
98: | $files[] = $file; |
99: | } |
100: | } |
101: | |
102: | if (count($files) === 0) { |
103: | throw new \RuntimeException('No source files found.'); |
104: | |
105: | } elseif ($this->output->isDebug()) { |
106: | $this->output->text('<info>Matching source files:</info>'); |
107: | $this->output->newLine(); |
108: | $this->output->listing($files); |
109: | |
110: | } elseif ($this->output->isVerbose()) { |
111: | $this->output->text(sprintf('Found %d source files.', count($files))); |
112: | $this->output->newLine(); |
113: | } |
114: | |
115: | return $files; |
116: | } |
117: | |
118: | |
119: | |
120: | |
121: | |
122: | protected function analyze(array $files): AnalyzeResult |
123: | { |
124: | $progressBar = $this->createProgressBar('Analyzing'); |
125: | $result = $this->analyzer->analyze($progressBar, $files); |
126: | |
127: | if ($progressBar->getMaxSteps() === $progressBar->getProgress()) { |
128: | $progressBar->setMessage('done'); |
129: | $progressBar->finish(); |
130: | } |
131: | |
132: | $this->output->newLine(2); |
133: | |
134: | return $result; |
135: | } |
136: | |
137: | |
138: | protected function index(AnalyzeResult $analyzeResult): Index |
139: | { |
140: | $index = new Index(); |
141: | |
142: | foreach ($analyzeResult->classLike as $info) { |
143: | $this->indexer->indexFile($index, $info->file, $info->primary); |
144: | $this->indexer->indexNamespace($index, $info->name->namespace, $info->name->namespaceLower, $info->primary, $info->isDeprecated()); |
145: | $this->indexer->indexClassLike($index, $info); |
146: | } |
147: | |
148: | foreach ($analyzeResult->function as $info) { |
149: | $this->indexer->indexFile($index, $info->file, $info->primary); |
150: | $this->indexer->indexNamespace($index, $info->name->namespace, $info->name->namespaceLower, $info->primary, $info->isDeprecated()); |
151: | $this->indexer->indexFunction($index, $info); |
152: | } |
153: | |
154: | $this->indexer->postProcess($index); |
155: | return $index; |
156: | } |
157: | |
158: | |
159: | protected function render(Index $index): void |
160: | { |
161: | $progressBar = $this->createProgressBar('Rendering'); |
162: | $this->renderer->render($progressBar, $index); |
163: | |
164: | if ($progressBar->getMaxSteps() === $progressBar->getProgress()) { |
165: | $progressBar->setMessage('done'); |
166: | $progressBar->finish(); |
167: | } |
168: | |
169: | $this->output->newLine(2); |
170: | } |
171: | |
172: | |
173: | protected function createProgressBar(string $label): ProgressBar |
174: | { |
175: | $progressBar = $this->output->createProgressBar(); |
176: | $progressBar->setFormat(" <fg=green>$label</> %current%/%max% %bar% %percent:3s%% %message%"); |
177: | $progressBar->setBarCharacter("\u{2588}"); |
178: | $progressBar->setProgressCharacter('_'); |
179: | $progressBar->setEmptyBarCharacter('_'); |
180: | |
181: | return $progressBar; |
182: | } |
183: | |
184: | |
185: | protected function performance(float $analyzeTime, int $analyzeMemory, float $indexTime, int $indexMemory, float $renderTime, int $renderMemory): void |
186: | { |
187: | if ($this->output->isVeryVerbose()) { |
188: | $lines = [ |
189: | 'Analyze time' => sprintf('%6.0f ms', $analyzeTime / 1e6), |
190: | 'Index time' => sprintf('%6.0f ms', $indexTime / 1e6), |
191: | 'Render time' => sprintf('%6.0f ms', $renderTime / 1e6), |
192: | '' => '', |
193: | 'Analyze peak memory' => sprintf('%6.0f MB', $analyzeMemory / 1e6), |
194: | 'Index peak memory' => sprintf('%6.0f MB', $indexMemory / 1e6), |
195: | 'Render peak memory' => sprintf('%6.0f MB', $renderMemory / 1e6), |
196: | ]; |
197: | |
198: | foreach ($lines as $label => $value) { |
199: | $this->output->text(sprintf('<info>%-20s</info> %s', $label, $value)); |
200: | } |
201: | } |
202: | } |
203: | |
204: | |
205: | protected function finish(AnalyzeResult $analyzeResult): bool |
206: | { |
207: | if (count($analyzeResult->error) === 0) { |
208: | $this->output->success('Finished OK'); |
209: | return true; |
210: | } |
211: | |
212: | $hasError = false; |
213: | foreach ($analyzeResult->error as $errorKind => $errorGroup) { |
214: | $errorLines = array_column($errorGroup, 'message'); |
215: | |
216: | if (!$this->output->isVerbose() && count($errorLines) > 5) { |
217: | $errorLines = array_slice($errorLines, 0, 5); |
218: | $errorLines[] = '...'; |
219: | $errorLines[] = sprintf('and %d more (use --verbose to show all)', count($errorGroup) - 5); |
220: | } |
221: | |
222: | if ($errorKind === ErrorKind::InternalError->name) { |
223: | $hasError = true; |
224: | $this->output->error(implode("\n\n", $errorLines)); |
225: | |
226: | } else { |
227: | $this->output->warning(implode("\n\n", $errorLines)); |
228: | } |
229: | } |
230: | |
231: | if ($hasError) { |
232: | $this->output->error('Finished with errors'); |
233: | return false; |
234: | } |
235: | |
236: | $this->output->success('Finished with warnings'); |
237: | return true; |
238: | } |
239: | } |
240: | |