Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
15.94% covered (danger)
15.94%
11 / 69
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileCopyScanner
15.94% covered (danger)
15.94%
11 / 69
16.67% covered (danger)
16.67%
1 / 6
396.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 scanFiles
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
156
 isPackageExcluded
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 isNamespaceExcluded
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 isFilePathExcluded
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 preparePattern
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Loop over the discovered files and mark the file to be copied or not.
4 *
5 * ```
6 * "exclude_from_copy": {
7 *   "packages": [
8 *   ],
9 *   "namespaces": [
10 *   ],
11 *   "file_patterns": [
12 *   ]
13 * },
14 * ```
15 */
16
17namespace BrianHenryIE\Strauss\Pipeline;
18
19use BrianHenryIE\Strauss\Composer\ComposerPackage;
20use BrianHenryIE\Strauss\Config\FileCopyScannerConfigInterface;
21use BrianHenryIE\Strauss\Files\DiscoveredFiles;
22use BrianHenryIE\Strauss\Files\FileBase;
23use BrianHenryIE\Strauss\Files\FileWithDependency;
24use BrianHenryIE\Strauss\Helpers\Flysystem\FileSystem;
25use BrianHenryIE\Strauss\Types\DiscoveredSymbol;
26use Psr\Log\LoggerAwareTrait;
27use Psr\Log\LoggerInterface;
28use Psr\Log\NullLogger;
29
30class FileCopyScanner
31{
32    use LoggerAwareTrait;
33
34    protected FileCopyScannerConfigInterface $config;
35
36    protected FileSystem $filesystem;
37
38    public function __construct(
39        FileCopyScannerConfigInterface $config,
40        FileSystem $filesystem,
41        ?LoggerInterface $logger = null
42    ) {
43        $this->config = $config;
44        $this->filesystem = $filesystem;
45
46        $this->setLogger($logger ?? new NullLogger());
47    }
48
49    public function scanFiles(DiscoveredFiles $files): void
50    {
51        /** @var FileBase $file */
52        foreach ($files->getFiles() as $file) {
53            $copy = true;
54
55            if ($this->config->isTargetDirectoryVendor()) {
56                $this->logger->debug("The target directory is the same as the vendor directory."); // TODO: surely this should be outside the loop/class.
57                $copy = false;
58            }
59
60            if ($file instanceof FileWithDependency) {
61                if ($this->isPackageExcluded($file->getDependency())) {
62                    $copy = false;
63                    $this->logger->debug("File {sourcePath} will not be copied because {$file->getDependency()->getPackageName()} is excluded from copy.", [
64                        'sourcePath' => $file->getSourcePath(),
65                    ]);
66                }
67            }
68
69            if ($this->isNamespaceExcluded($file)) {
70                $copy = false;
71            }
72
73            if ($this->isFilePathExcluded($file)) {
74                $copy = false;
75            }
76
77//            if ($copy) {
78//                $this->logger->debug("Marking file {relativeFilePath} to be copied.", [
79//                    'relativeFilePath' => $this->filesystem->getRelativePath($this->config->getAbsoluteVendorDirectory(), $file->getSourcePath()),
80//                ]);
81//            }
82
83            $file->setDoCopy($copy);
84
85            if ($copy) {
86                $target = $file instanceof FileWithDependency
87                    ?  $this->config->getAbsoluteTargetDirectory() . '/' . $file->getDependency()->getRelativePath() . '/'. $file->getPackageRelativePath()
88                    : $file->getSourcePath();
89                $file->setTargetAbsolutePath(FileSystem::normalizeDirSeparator($target));
90            }
91
92            $shouldDelete = $this->config->isDeleteVendorFiles() && ! $this->filesystem->isSymlinked($file->getSourcePath());
93            $file->setDoDelete($shouldDelete);
94
95            // If a file isn't copied, don't unintentionally edit the source file.
96            if (!$file->isDoCopy() && !$this->config->isTargetDirectoryVendor()) {
97                $file->setDoPrefix(false);
98            }
99//            // If the file is marked not to copy, mark the symbol not to be renamed
100//            if (!$copy && !$this->config->isTargetDirectoryVendor()) {
101//                foreach ($file->getDiscoveredSymbols() as $symbol) {
102//                    // Only make this change if the symbol is only in one file (i.e. namespaces will be in many).
103//                    if (count($symbol->getSourceFiles()) === 1) {
104//                        $symbol->setDoRename(false);
105//                    }
106//                }
107//            }
108            // To make step-debugging easier.
109            unset($copy, $target, $shouldDelete);
110        };
111    }
112
113    protected function isPackageExcluded(ComposerPackage $package): bool
114    {
115        if (in_array(
116            $package->getPackageName(),
117            $this->config->getExcludePackagesFromCopy(),
118            true
119        )) {
120            return true;
121        }
122        return false;
123    }
124
125    protected function isNamespaceExcluded(FileBase $file): bool
126    {
127        if (!$file->isPhpFile()) {
128            return false;
129        }
130        /** @var string[] $namespaceStringsInFile */
131        $namespaceStringsInFile = array_map(
132            fn(DiscoveredSymbol $symbol): string => $symbol->getOriginalFqdnName(),
133            $file->getDiscoveredSymbols()->getNamespaces()->notGlobal()->toArray()
134        );
135        foreach ($this->config->getExcludeNamespacesFromCopy() as $excludedNamespaceString) {
136            $excludedNamespaceString = rtrim($excludedNamespaceString, '\\');
137
138            /** @var string[] $excludedNamespaceStringsInFile */
139            $excludedNamespaceStringsInFile = array_reduce(
140                $namespaceStringsInFile,
141                // TODO: use case insensitive check instead. People might write BrianHenryIE\API instead of BrianHenryIE\Api.
142                fn(array $carry, string $namespaceString) => str_starts_with($namespaceString, $excludedNamespaceString)
143                        ? array_merge($carry, [$namespaceString]) : $carry,
144                []
145            );
146            if (!empty($excludedNamespaceStringsInFile)) {
147                $this->logger->debug("File {sourcePath} will not be copied because namespace {$excludedNamespaceString} is excluded from copy.", [
148                    'sourcePath' => $file->getSourcePath(),
149                ]);
150                return true;
151            }
152        }
153        return false;
154    }
155
156    /**
157     * Compares the vendor relative path with `exclude_file_patterns` config.
158     *
159     * I.e. `my/package/src/file.php`.
160     *
161     * @param FileBase $file
162     */
163    protected function isFilePathExcluded(FileBase $file): bool
164    {
165        $path = $file->getVendorRelativePath();
166
167        foreach ($this->config->getExcludeFilePatternsFromCopy() as $pattern) {
168            $escapedPattern = $this->preparePattern($pattern);
169            if (1 === preg_match($escapedPattern, $path)) {
170                $this->logger->debug("File {path} will not be copied because it matches pattern {$pattern}.", [
171                    'path' => $path
172                ]);
173                return true;
174            }
175        }
176        return false;
177    }
178
179    private function preparePattern(string $pattern): string
180    {
181        $delimiter = '#';
182
183        if (substr($pattern, 0, 1) !== substr($pattern, - 1, 1)) {
184            $pattern = $delimiter . $pattern . $delimiter;
185        }
186
187        return $pattern;
188    }
189}