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