1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace ApiGen; |
4: | |
5: | use ApiGen\Analyzer\AnalyzeResult; |
6: | use ApiGen\Analyzer\AnalyzeState; |
7: | use ApiGen\Analyzer\AnalyzeTask; |
8: | use ApiGen\Analyzer\AnalyzeTaskHandlerFactory; |
9: | use ApiGen\Info\ClassLikeInfo; |
10: | use ApiGen\Info\ClassLikeReferenceInfo; |
11: | use ApiGen\Info\ErrorInfo; |
12: | use ApiGen\Info\ErrorKind; |
13: | use ApiGen\Info\FunctionInfo; |
14: | use ApiGen\Info\MissingInfo; |
15: | use ApiGen\Info\NameInfo; |
16: | use ApiGen\Scheduler\SchedulerFactory; |
17: | use Symfony\Component\Console\Helper\ProgressBar; |
18: | |
19: | use function count; |
20: | use function implode; |
21: | |
22: | |
23: | class Analyzer |
24: | { |
25: | public function __construct( |
26: | protected SchedulerFactory $schedulerFactory, |
27: | protected Locator $locator, |
28: | ) { |
29: | } |
30: | |
31: | |
32: | |
33: | |
34: | |
35: | public function analyze(ProgressBar $progressBar, array $files): AnalyzeResult |
36: | { |
37: | $scheduler = $this->schedulerFactory->create(AnalyzeTaskHandlerFactory::class, context: null); |
38: | $state = new AnalyzeState($progressBar, $scheduler); |
39: | |
40: | foreach ($files as $file) { |
41: | $this->scheduleFile($state, $file, primary: true); |
42: | } |
43: | |
44: | |
45: | foreach ($scheduler->process() as $task => $result) { |
46: | $this->processTaskResult($result, $state); |
47: | $progressBar->setMessage($task->sourceFile); |
48: | $progressBar->advance(); |
49: | } |
50: | |
51: | foreach ($state->missing as $missing) { |
52: | $referencedBy = $state->classLikes[$missing->referencedBy->fullLower] ?? $state->functions[$missing->referencedBy->fullLower]; |
53: | |
54: | if ($referencedBy->primary) { |
55: | $state->errors[ErrorKind::MissingSymbol->name][] = $this->createMissingSymbolError($missing, $referencedBy); |
56: | } |
57: | } |
58: | |
59: | return new AnalyzeResult($state->classLikes + $state->missing, $state->functions, $state->errors); |
60: | } |
61: | |
62: | |
63: | protected function scheduleFile(AnalyzeState $state, string $file, bool $primary): void |
64: | { |
65: | $file = Helpers::realPath($file); |
66: | |
67: | if (isset($state->files[$file])) { |
68: | return; |
69: | } |
70: | |
71: | $state->files[$file] = true; |
72: | $state->progressBar->setMaxSteps(count($state->files)); |
73: | $state->scheduler->schedule(new AnalyzeTask($file, $primary)); |
74: | } |
75: | |
76: | |
77: | |
78: | |
79: | |
80: | protected function processTaskResult(array $result, AnalyzeState $state): void |
81: | { |
82: | foreach ($result as $info) { |
83: | match (true) { |
84: | $info instanceof ClassLikeReferenceInfo => $this->processClassLikeReference($state, $info), |
85: | $info instanceof ClassLikeInfo => $this->processClassLike($state, $info), |
86: | $info instanceof FunctionInfo => $this->processFunction($state, $info), |
87: | $info instanceof ErrorInfo => $this->processError($state, $info), |
88: | }; |
89: | } |
90: | } |
91: | |
92: | |
93: | protected function processClassLikeReference(AnalyzeState $state, ClassLikeReferenceInfo $info): void |
94: | { |
95: | if ($state->prevName !== null && !isset($state->classLikes[$info->fullLower]) && !isset($state->missing[$info->fullLower])) { |
96: | $name = new NameInfo($info->full, $info->fullLower); |
97: | $state->missing[$info->fullLower] = new MissingInfo($name, $state->prevName); |
98: | |
99: | if (($file = $this->locator->locate($info)) !== null) { |
100: | $this->scheduleFile($state, $file, primary: false); |
101: | } |
102: | } |
103: | } |
104: | |
105: | |
106: | protected function processClassLike(AnalyzeState $state, ClassLikeInfo $info): void |
107: | { |
108: | $existing = $state->classLikes[$info->name->fullLower] ?? null; |
109: | |
110: | if ($existing === null || ($info->primary && !$existing->primary)) { |
111: | unset($state->missing[$info->name->fullLower]); |
112: | $state->classLikes[$info->name->fullLower] = $info; |
113: | $state->prevName = $info->name; |
114: | |
115: | } elseif ($info->primary) { |
116: | $state->errors[ErrorKind::DuplicateSymbol->name][] = $this->createDuplicateSymbolError($info, $existing); |
117: | $state->prevName = null; |
118: | |
119: | } else { |
120: | $state->prevName = null; |
121: | } |
122: | } |
123: | |
124: | |
125: | protected function processFunction(AnalyzeState $state, FunctionInfo $info): void |
126: | { |
127: | $existing = $state->functions[$info->name->fullLower] ?? null; |
128: | |
129: | if ($existing === null || ($info->primary && !$existing->primary)) { |
130: | $state->functions[$info->name->fullLower] = $info; |
131: | $state->prevName = $info->name; |
132: | |
133: | } elseif ($info->primary) { |
134: | $state->errors[ErrorKind::DuplicateSymbol->name][] = $this->createDuplicateSymbolError($info, $existing); |
135: | $state->prevName = null; |
136: | |
137: | } else { |
138: | $state->prevName = null; |
139: | } |
140: | } |
141: | |
142: | |
143: | protected function processError(AnalyzeState $state, ErrorInfo $info): void |
144: | { |
145: | $state->errors[$info->kind->name][] = $info; |
146: | $state->prevName = null; |
147: | } |
148: | |
149: | |
150: | protected function createMissingSymbolError(MissingInfo $dependency, ClassLikeInfo | FunctionInfo $referencedBy): ErrorInfo |
151: | { |
152: | return new ErrorInfo(ErrorKind::MissingSymbol, implode("\n", [ |
153: | "Missing {$dependency->name->full}", |
154: | "referenced by {$referencedBy->name->full}", |
155: | ])); |
156: | } |
157: | |
158: | |
159: | protected function createDuplicateSymbolError(ClassLikeInfo | FunctionInfo $info, ClassLikeInfo | FunctionInfo $first): ErrorInfo |
160: | { |
161: | return new ErrorInfo(ErrorKind::DuplicateSymbol, implode("\n", [ |
162: | "Multiple definitions of {$info->name->full}.", |
163: | "The first definition was found in {$first->file} on line {$first->startLine}", |
164: | "and then another one was found in {$info->file} on line {$info->startLine}", |
165: | ])); |
166: | } |
167: | } |
168: | |