Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
29.06% covered (danger)
29.06%
34 / 117
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileSymbolScanner
29.06% covered (danger)
29.06%
34 / 117
0.00% covered (danger)
0.00%
0 / 10
525.74
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 pad
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 add
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 findInFiles
72.22% covered (warning)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
5.54
 find
60.00% covered (warning)
60.00%
21 / 35
0.00% covered (danger)
0.00%
0 / 1
18.74
 splitByNamespace
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 addDiscoveredClassChange
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 addDiscoveredNamespaceChange
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getBuiltIns
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 loadBuiltIns
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
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
8namespace BrianHenryIE\Strauss\Pipeline;
9
10use BrianHenryIE\Strauss\Config\FileSymbolScannerConfigInterface;
11use BrianHenryIE\Strauss\Files\DiscoveredFiles;
12use BrianHenryIE\Strauss\Files\File;
13use BrianHenryIE\Strauss\Files\FileWithDependency;
14use BrianHenryIE\Strauss\Helpers\FileSystem;
15use BrianHenryIE\Strauss\Types\ClassSymbol;
16use BrianHenryIE\Strauss\Types\ConstantSymbol;
17use BrianHenryIE\Strauss\Types\DiscoveredSymbol;
18use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
19use BrianHenryIE\Strauss\Types\FunctionSymbol;
20use BrianHenryIE\Strauss\Types\InterfaceSymbol;
21use BrianHenryIE\Strauss\Types\NamespaceSymbol;
22use BrianHenryIE\Strauss\Types\TraitSymbol;
23use PhpParser\Node;
24use PhpParser\ParserFactory;
25use PhpParser\PrettyPrinter\Standard;
26use Psr\Log\LoggerAwareTrait;
27use Psr\Log\LoggerInterface;
28use Psr\Log\NullLogger;
29use BrianHenryIE\SimplePhpParser\Model\PHPClass;
30use BrianHenryIE\SimplePhpParser\Model\PHPConst;
31use BrianHenryIE\SimplePhpParser\Model\PHPFunction;
32use BrianHenryIE\SimplePhpParser\Parsers\PhpCodeParser;
33
34class 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}