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