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: * @param string[] $paths indexed by []
30: * @param string[] $include indexed by []
31: * @param string[] $exclude indexed by []
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: * @return string[] list of files, indexed by []
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: * @param string[] $files indexed by []
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: