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: * @param string[] $files indexed by []
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: /** @var AnalyzeTask $task */
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: * @param array<ClassLikeInfo | FunctionInfo | ClassLikeReferenceInfo | ErrorInfo> $result
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: