Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
29.06% |
34 / 117 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
FileSymbolScanner | |
29.06% |
34 / 117 |
|
0.00% |
0 / 10 |
525.74 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
pad | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
add | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
findInFiles | |
72.22% |
13 / 18 |
|
0.00% |
0 / 1 |
5.54 | |||
find | |
60.00% |
21 / 35 |
|
0.00% |
0 / 1 |
18.74 | |||
splitByNamespace | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
addDiscoveredClassChange | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
addDiscoveredNamespaceChange | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getBuiltIns | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
loadBuiltIns | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * The purpose of this class is only to find changes that should be made. |
4 | * i.e. classes and namespaces to change. |
5 | * Those recorded are updated in a later step. |
6 | */ |
7 | |
8 | namespace BrianHenryIE\Strauss\Pipeline; |
9 | |
10 | use BrianHenryIE\Strauss\Config\FileSymbolScannerConfigInterface; |
11 | use BrianHenryIE\Strauss\Files\DiscoveredFiles; |
12 | use BrianHenryIE\Strauss\Files\File; |
13 | use BrianHenryIE\Strauss\Files\FileWithDependency; |
14 | use BrianHenryIE\Strauss\Helpers\FileSystem; |
15 | use BrianHenryIE\Strauss\Types\ClassSymbol; |
16 | use BrianHenryIE\Strauss\Types\ConstantSymbol; |
17 | use BrianHenryIE\Strauss\Types\DiscoveredSymbol; |
18 | use BrianHenryIE\Strauss\Types\DiscoveredSymbols; |
19 | use BrianHenryIE\Strauss\Types\FunctionSymbol; |
20 | use BrianHenryIE\Strauss\Types\InterfaceSymbol; |
21 | use BrianHenryIE\Strauss\Types\NamespaceSymbol; |
22 | use BrianHenryIE\Strauss\Types\TraitSymbol; |
23 | use PhpParser\Node; |
24 | use PhpParser\ParserFactory; |
25 | use PhpParser\PrettyPrinter\Standard; |
26 | use Psr\Log\LoggerAwareTrait; |
27 | use Psr\Log\LoggerInterface; |
28 | use Psr\Log\NullLogger; |
29 | use BrianHenryIE\SimplePhpParser\Model\PHPClass; |
30 | use BrianHenryIE\SimplePhpParser\Model\PHPConst; |
31 | use BrianHenryIE\SimplePhpParser\Model\PHPFunction; |
32 | use BrianHenryIE\SimplePhpParser\Parsers\PhpCodeParser; |
33 | |
34 | class FileSymbolScanner |
35 | { |
36 | use LoggerAwareTrait; |
37 | |
38 | /** @var string[] */ |
39 | protected array $excludeNamespacesFromPrefixing = array(); |
40 | |
41 | protected DiscoveredSymbols $discoveredSymbols; |
42 | |
43 | protected FileSystem $filesystem; |
44 | |
45 | protected FileSymbolScannerConfigInterface $config; |
46 | |
47 | /** @var string[] */ |
48 | protected array $builtIns = []; |
49 | |
50 | /** |
51 | * @var string[] |
52 | */ |
53 | protected array $loggedSymbols = []; |
54 | |
55 | /** |
56 | * FileScanner constructor. |
57 | */ |
58 | public function __construct( |
59 | FileSymbolScannerConfigInterface $config, |
60 | DiscoveredSymbols $discoveredSymbols, |
61 | FileSystem $filesystem, |
62 | ?LoggerInterface $logger = null |
63 | ) { |
64 | $this->discoveredSymbols = $discoveredSymbols; |
65 | $this->excludeNamespacesFromPrefixing = $config->getExcludeNamespacesFromPrefixing(); |
66 | |
67 | $this->config = $config; |
68 | |
69 | $this->filesystem = $filesystem; |
70 | $this->logger = $logger ?? new NullLogger(); |
71 | } |
72 | |
73 | |
74 | private static function pad(string $text, int $length = 25): string |
75 | { |
76 | /** @var int $padLength */ |
77 | static $padLength; |
78 | $padLength = max(isset($padLength) ? $padLength : 0, $length); |
79 | $padLength = max($padLength, strlen($text) + 1); |
80 | return str_pad($text, $padLength, ' ', STR_PAD_RIGHT); |
81 | } |
82 | |
83 | protected function add(DiscoveredSymbol $symbol): void |
84 | { |
85 | $this->discoveredSymbols->add($symbol); |
86 | |
87 | $level = in_array($symbol->getOriginalSymbol(), $this->loggedSymbols) ? 'debug' : 'info'; |
88 | $newText = in_array($symbol->getOriginalSymbol(), $this->loggedSymbols) ? '' : 'new '; |
89 | |
90 | $this->loggedSymbols[] = $symbol->getOriginalSymbol(); |
91 | |
92 | $this->logger->log( |
93 | $level, |
94 | sprintf( |
95 | "%s%s", |
96 | // The part up until the original symbol. I.e. the first "column" of the message. |
97 | self::pad(sprintf( |
98 | "Found %s%s:", |
99 | $newText, |
100 | // From `BrianHenryIE\Strauss\Types\TraitSymbol` -> `trait` |
101 | strtolower(str_replace('Symbol', '', array_reverse(explode('\\', get_class($symbol)))[0])), |
102 | )), |
103 | $symbol->getOriginalSymbol() |
104 | ) |
105 | ); |
106 | } |
107 | |
108 | /** |
109 | * @param DiscoveredFiles $files |
110 | */ |
111 | public function findInFiles(DiscoveredFiles $files): DiscoveredSymbols |
112 | { |
113 | foreach ($files->getFiles() as $file) { |
114 | if ($file instanceof FileWithDependency && !in_array($file->getDependency()->getPackageName(), array_keys($this->config->getPackagesToPrefix()))) { |
115 | $file->setDoPrefix(false); |
116 | continue; |
117 | } |
118 | |
119 | $relativeFilePath = $this->filesystem->getRelativePath( |
120 | $this->config->getProjectDirectory(), |
121 | $file->getSourcePath() |
122 | ); |
123 | |
124 | if (!$file->isPhpFile()) { |
125 | $file->setDoPrefix(false); |
126 | $this->logger->debug(self::pad("Skipping non-PHP file:"). $relativeFilePath); |
127 | continue; |
128 | } |
129 | |
130 | $this->logger->info(self::pad("Scanning file:") . $relativeFilePath); |
131 | $this->find( |
132 | $this->filesystem->read($file->getSourcePath()), |
133 | $file |
134 | ); |
135 | } |
136 | |
137 | return $this->discoveredSymbols; |
138 | } |
139 | |
140 | protected function find(string $contents, File $file): void |
141 | { |
142 | $namespaces = $this->splitByNamespace($contents); |
143 | |
144 | foreach ($namespaces as $namespaceName => $contents) { |
145 | $this->addDiscoveredNamespaceChange($namespaceName, $file); |
146 | |
147 | // Because we split the file by namespace, we need to add it back to avoid the case where `class A extends \A`. |
148 | $contents = trim($contents); |
149 | $contents = false === strpos($contents, '<?php') ? "<?php\n" . $contents : $contents; |
150 | if ($namespaceName !== '\\') { |
151 | $contents = preg_replace('#<\?php#', '<?php' . PHP_EOL . 'namespace ' . $namespaceName . ';' . PHP_EOL, $contents, 1); |
152 | } |
153 | |
154 | PhpCodeParser::$classExistsAutoload = false; |
155 | $phpCode = PhpCodeParser::getFromString($contents); |
156 | |
157 | /** @var PHPClass[] $phpClasses */ |
158 | $phpClasses = $phpCode->getClasses(); |
159 | foreach ($phpClasses as $fqdnClassname => $class) { |
160 | // Skip classes defined in other files. |
161 | // I tried to use the $class->file property but it was autoloading from Strauss so incorrectly setting |
162 | // the path, different to the file being scanned. |
163 | if (false !== strpos($contents, "use {$fqdnClassname};")) { |
164 | continue; |
165 | } |
166 | |
167 | $isAbstract = (bool) $class->is_abstract; |
168 | $extends = $class->parentClass; |
169 | $interfaces = $class->interfaces; |
170 | $this->addDiscoveredClassChange($fqdnClassname, $isAbstract, $file, $namespaceName, $extends, $interfaces); |
171 | } |
172 | |
173 | /** @var PHPFunction[] $phpFunctions */ |
174 | $phpFunctions = $phpCode->getFunctions(); |
175 | foreach ($phpFunctions as $functionName => $function) { |
176 | if (in_array($functionName, $this->getBuiltIns())) { |
177 | continue; |
178 | } |
179 | $functionSymbol = new FunctionSymbol($functionName, $file, $namespaceName); |
180 | $this->add($functionSymbol); |
181 | } |
182 | |
183 | /** @var PHPConst $phpConstants */ |
184 | $phpConstants = $phpCode->getConstants(); |
185 | foreach ($phpConstants as $constantName => $constant) { |
186 | $constantSymbol = new ConstantSymbol($constantName, $file, $namespaceName); |
187 | $this->add($constantSymbol); |
188 | } |
189 | |
190 | $phpInterfaces = $phpCode->getInterfaces(); |
191 | foreach ($phpInterfaces as $interfaceName => $interface) { |
192 | $interfaceSymbol = new InterfaceSymbol($interfaceName, $file, $namespaceName); |
193 | $this->add($interfaceSymbol); |
194 | } |
195 | |
196 | $phpTraits = $phpCode->getTraits(); |
197 | foreach ($phpTraits as $traitName => $trait) { |
198 | $traitSymbol = new TraitSymbol($traitName, $file, $namespaceName); |
199 | $this->add($traitSymbol); |
200 | } |
201 | } |
202 | } |
203 | |
204 | protected function splitByNamespace(string $contents):array |
205 | { |
206 | $result = []; |
207 | |
208 | $parser = (new ParserFactory())->createForNewestSupportedVersion(); |
209 | |
210 | $ast = $parser->parse(trim($contents)); |
211 | |
212 | foreach ($ast as $rootNode) { |
213 | if ($rootNode instanceof Node\Stmt\Namespace_) { |
214 | if (is_null($rootNode->name)) { |
215 | $result['\\'] = (new Standard())->prettyPrintFile($rootNode->stmts); |
216 | } else { |
217 | $result[$rootNode->name->name] = (new Standard())->prettyPrintFile($rootNode->stmts); |
218 | } |
219 | } |
220 | } |
221 | |
222 | // TODO: is this necessary? |
223 | if (empty($result)) { |
224 | $result['\\'] = $contents; |
225 | } |
226 | |
227 | return $result; |
228 | } |
229 | |
230 | protected function addDiscoveredClassChange( |
231 | string $fqdnClassname, |
232 | bool $isAbstract, |
233 | File $file, |
234 | $namespaceName, |
235 | $extends, |
236 | array $interfaces |
237 | ): void { |
238 | // TODO: This should be included but marked not to prefix. |
239 | if (in_array($fqdnClassname, $this->getBuiltIns())) { |
240 | return; |
241 | } |
242 | |
243 | $classSymbol = new ClassSymbol($fqdnClassname, $file, $isAbstract, $namespaceName, $extends, $interfaces); |
244 | $this->add($classSymbol); |
245 | } |
246 | |
247 | protected function addDiscoveredNamespaceChange(string $namespace, File $file): void |
248 | { |
249 | |
250 | foreach ($this->excludeNamespacesFromPrefixing as $excludeNamespace) { |
251 | if (0 === strpos($namespace, $excludeNamespace)) { |
252 | // TODO: Log. |
253 | return; |
254 | } |
255 | } |
256 | |
257 | $namespaceObj = $this->discoveredSymbols->getNamespace($namespace); |
258 | if ($namespaceObj) { |
259 | $namespaceObj->addSourceFile($file); |
260 | $file->addDiscoveredSymbol($namespaceObj); |
261 | return; |
262 | } else { |
263 | $namespaceObj = new NamespaceSymbol($namespace, $file); |
264 | } |
265 | |
266 | $this->add($namespaceObj); |
267 | } |
268 | |
269 | /** |
270 | * Get a list of PHP built-in classes etc. so they are not prefixed. |
271 | * |
272 | * Polyfilled classes were being prefixed, but the polyfills are only active when the PHP version is below X, |
273 | * so calls to those prefixed polyfilled classnames would fail on newer PHP versions. |
274 | * |
275 | * NB: This list is not exhaustive. Any unloaded PHP extensions are not included. |
276 | * |
277 | * @see https://github.com/BrianHenryIE/strauss/issues/79 |
278 | * |
279 | * ``` |
280 | * array_filter( |
281 | * get_declared_classes(), |
282 | * function(string $className): bool { |
283 | * $reflector = new \ReflectionClass($className); |
284 | * return empty($reflector->getFileName()); |
285 | * } |
286 | * ); |
287 | * ``` |
288 | * |
289 | * @return string[] |
290 | */ |
291 | protected function getBuiltIns(): array |
292 | { |
293 | if (empty($this->builtIns)) { |
294 | $this->loadBuiltIns(); |
295 | } |
296 | |
297 | return $this->builtIns; |
298 | } |
299 | |
300 | /** |
301 | * Load the file containing the built-in PHP classes etc. and flatten to a single array of strings and store. |
302 | */ |
303 | protected function loadBuiltIns(): void |
304 | { |
305 | $builtins = include __DIR__ . '/FileSymbol/builtinsymbols.php'; |
306 | |
307 | $flatArray = array(); |
308 | array_walk_recursive( |
309 | $builtins, |
310 | function ($array) use (&$flatArray) { |
311 | if (is_array($array)) { |
312 | $flatArray = array_merge($flatArray, array_values($array)); |
313 | } else { |
314 | $flatArray[] = $array; |
315 | } |
316 | } |
317 | ); |
318 | |
319 | $this->builtIns = $flatArray; |
320 | } |
321 | } |