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 | } |