Passed
Pull Request — 4 (#10150)
by Maxime
06:26
created

ManifestFileFinder::findModuleCILib()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 3
dl 0
loc 16
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
use SilverStripe\Assets\FileFinder;
6
7
/**
8
 * An extension to the default file finder with some extra filters to faciliate
9
 * autoload and template manifest generation:
10
 *   - Only modules with _config.php files are scanned.
11
 *   - If a _manifest_exclude file is present inside a directory it is ignored.
12
 *   - Assets and module language directories are ignored.
13
 *   - Module tests directories are skipped if the ignore_tests option is not
14
 *     set to false.
15
 */
16
class ManifestFileFinder extends FileFinder
17
{
18
19
    const CONFIG_FILE = '_config.php';
20
    const CONFIG_DIR = '_config';
21
    const EXCLUDE_FILE = '_manifest_exclude';
22
    const LANG_DIR = 'lang';
23
    const TESTS_DIR = 'tests';
24
    const VENDOR_DIR = 'vendor';
25
26
    /**
27
     * @deprecated 4.4.0:5.0.0 Use global `RESOURCES_DIR` instead.
28
     */
29
    const RESOURCES_DIR = RESOURCES_DIR;
30
31
    protected static $default_options = [
32
        'include_themes' => false,
33
        'ignore_tests' => true,
34
        'min_depth' => 1,
35
        'ignore_dirs' => ['node_modules'],
36
        'ignore_ci_library' => []
37
    ];
38
39
    public function acceptDir($basename, $pathname, $depth)
40
    {
41
        // Skip if ignored
42
        if ($this->isInsideIgnored($basename, $pathname, $depth)) {
43
            return false;
44
        }
45
46
        // Keep searching inside vendor
47
        $inVendor = $this->isInsideVendor($basename, $pathname, $depth);
48
        if ($inVendor) {
49
            // Skip nested vendor folders (e.g. vendor/silverstripe/framework/vendor)
50
            if ($depth == 4 && basename($pathname) === self::VENDOR_DIR) {
51
                return false;
52
            }
53
54
            // Keep searching if we could have a subdir module
55
            if ($depth < 3) {
56
                return true;
57
            }
58
59
            // Stop searching if we are in a non-module library
60
            $libraryPath = $this->upLevels($pathname, $depth - 3);
61
            $libraryBase = basename($libraryPath);
62
            if (!$this->isDirectoryModule($libraryBase, $libraryPath, 3)) {
63
                return false;
64
            }
65
        }
66
67
        // Include themes
68
        if ($this->getOption('include_themes') && $this->isInsideThemes($basename, $pathname, $depth)) {
69
            return true;
70
        }
71
72
        // Skip if not in module
73
        if (!$this->isInsideModule($basename, $pathname, $depth)) {
74
            return false;
75
        }
76
77
        // Skip if test dir inside vendor module with unexpected CI library
78
        if ($depth > 3 && $basename === self::TESTS_DIR && $ignoreCILib = $this->getOption('ignore_ci_library')) {
79
            $ciLib = $this->findModuleCILib($basename, $pathname, $depth);
80
            if (in_array($ciLib, $ignoreCILib)) {
81
                return false;
82
            }
83
        }
84
85
        return parent::acceptDir($basename, $pathname, $depth);
86
    }
87
88
    /**
89
     * Check if the given dir is, or is inside the vendor folder
90
     *
91
     * @param string $basename
92
     * @param string $pathname
93
     * @param int $depth
94
     * @return bool
95
     */
96
    public function isInsideVendor($basename, $pathname, $depth)
97
    {
98
        $base = basename($this->upLevels($pathname, $depth - 1));
99
        return $base === self::VENDOR_DIR;
100
    }
101
102
    /**
103
     * Check if the given dir is, or is inside the themes folder
104
     *
105
     * @param string $basename
106
     * @param string $pathname
107
     * @param int $depth
108
     * @return bool
109
     */
110
    public function isInsideThemes($basename, $pathname, $depth)
111
    {
112
        $base = basename($this->upLevels($pathname, $depth - 1));
113
        return $base === THEMES_DIR;
114
    }
115
116
    /**
117
     * Check if this folder or any parent is ignored
118
     *
119
     * @param string $basename
120
     * @param string $pathname
121
     * @param int $depth
122
     * @return bool
123
     */
124
    public function isInsideIgnored($basename, $pathname, $depth)
125
    {
126
        return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
127
            return $this->isDirectoryIgnored($basename, $pathname, $depth);
128
        });
129
    }
130
131
    /**
132
     * Check if this folder is inside any module
133
     *
134
     * @param string $basename
135
     * @param string $pathname
136
     * @param int $depth
137
     * @return bool
138
     */
139
    public function isInsideModule($basename, $pathname, $depth)
140
    {
141
        return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
142
            return $this->isDirectoryModule($basename, $pathname, $depth);
143
        });
144
    }
145
146
    /**
147
     * Check if any parents match the given callback
148
     *
149
     * @param string $basename
150
     * @param string $pathname
151
     * @param int $depth
152
     * @param callable $callback
153
     * @return bool
154
     */
155
    protected function anyParents($basename, $pathname, $depth, $callback)
156
    {
157
        // Check all ignored dir up the path
158
        while ($depth >= 0) {
159
            $ignored = $callback($basename, $pathname, $depth);
160
            if ($ignored) {
161
                return true;
162
            }
163
            $pathname = dirname($pathname);
164
            $basename = basename($pathname);
165
            $depth--;
166
        }
167
        return false;
168
    }
169
170
    /**
171
     * Check if the given dir is a module root (not a subdir)
172
     *
173
     * @param string $basename
174
     * @param string $pathname
175
     * @param string $depth
176
     * @return bool
177
     */
178
    public function isDirectoryModule($basename, $pathname, $depth)
179
    {
180
        // Depth can either be 0, 1, or 3 (if and only if inside vendor)
181
        $inVendor = $this->isInsideVendor($basename, $pathname, $depth);
0 ignored issues
show
Bug introduced by
$depth of type string is incompatible with the type integer expected by parameter $depth of SilverStripe\Core\Manife...inder::isInsideVendor(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

181
        $inVendor = $this->isInsideVendor($basename, $pathname, /** @scrutinizer ignore-type */ $depth);
Loading history...
182
        if ($depth > 0 && $depth !== ($inVendor ? 3 : 1)) {
183
            return false;
184
        }
185
186
        // True if config file exists
187
        if (file_exists($pathname . '/' . self::CONFIG_FILE)) {
188
            return true;
189
        }
190
191
        // True if config dir exists
192
        if (file_exists($pathname . '/' . self::CONFIG_DIR)) {
193
            return true;
194
        }
195
196
        return false;
197
    }
198
199
    /**
200
     * Get a parent path the given levels above
201
     *
202
     * @param string $pathname
203
     * @param int $depth Number of parents to rise
204
     * @return string
205
     */
206
    protected function upLevels($pathname, $depth)
207
    {
208
        if ($depth < 0) {
209
            return null;
210
        }
211
        while ($depth--) {
212
            $pathname = dirname($pathname);
213
        }
214
        return $pathname;
215
    }
216
217
    /**
218
     * Get all ignored directories
219
     *
220
     * @return array
221
     */
222
    protected function getIgnoredDirs()
223
    {
224
        $ignored = [self::LANG_DIR, 'node_modules'];
225
        if ($this->getOption('ignore_tests')) {
226
            $ignored[] = self::TESTS_DIR;
227
        }
228
        return $ignored;
229
    }
230
231
    /**
232
     * Check if the given directory is ignored
233
     * @param string $basename
234
     * @param string $pathname
235
     * @param string $depth
236
     * @return bool
237
     */
238
    public function isDirectoryIgnored($basename, $pathname, $depth)
239
    {
240
        // Don't ignore root
241
        if ($depth === 0) {
0 ignored issues
show
introduced by
The condition $depth === 0 is always false.
Loading history...
242
            return false;
243
        }
244
245
        // Check if manifest-ignored is present
246
        if (file_exists($pathname . '/' . self::EXCLUDE_FILE)) {
247
            return true;
248
        }
249
250
        // Check if directory name is ignored
251
        $ignored = $this->getIgnoredDirs();
252
        if (in_array($basename, $ignored)) {
253
            return true;
254
        }
255
256
        // Ignore these dirs in the root only
257
        if ($depth === 1 && in_array($basename, [ASSETS_DIR, RESOURCES_DIR])) {
0 ignored issues
show
introduced by
The condition $depth === 1 is always false.
Loading history...
258
            return true;
259
        }
260
261
        return false;
262
    }
263
264
    private function findModuleCILib(string $basename, string $pathname, int $depth): string
265
    {
266
        if ($depth < 1) {
267
            return Module::CI_PHPUNIT_UNKNOWN;
268
        }
269
270
        $newBasename = basename($pathname);
271
        $newPathname = dirname($pathname);
272
        $newDepth = $depth - 1;
273
274
        if ($this->isDirectoryModule($newBasename, $newPathname, $newDepth)) {
275
            $module = new Module($newPathname, $this->upLevels($newPathname, $newDepth));
276
            return $module->getCILibrary();
277
        }
278
279
        return $this->findModuleCILib($newBasename, $newPathname, $newDepth);
280
    }
281
}
282