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