GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( d2042d...eaef5f )
by Franck
04:54
created

Plugin::cleanInstalledPaths()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.1111
c 0
b 0
f 0
cc 6
nc 3
nop 0
1
<?php
2
3
/**
4
 * This file is part of the Dealerdirect PHP_CodeSniffer Standards
5
 * Composer Installer Plugin package.
6
 *
7
 * @copyright 2016-2018 Dealerdirect B.V.
8
 * @license MIT
9
 */
10
11
namespace Dealerdirect\Composer\Plugin\Installers\PHPCodeSniffer;
12
13
use Composer\Composer;
14
use Composer\EventDispatcher\EventSubscriberInterface;
15
use Composer\IO\IOInterface;
16
use Composer\Package\AliasPackage;
17
use Composer\Package\PackageInterface;
18
use Composer\Package\RootpackageInterface;
19
use Composer\Plugin\PluginInterface;
20
use Composer\Script\Event;
21
use Composer\Script\ScriptEvents;
22
use Symfony\Component\Finder\Finder;
23
use Symfony\Component\Process\Exception\LogicException;
24
use Symfony\Component\Process\Exception\ProcessFailedException;
25
use Symfony\Component\Process\Exception\RuntimeException;
26
use Symfony\Component\Process\ProcessBuilder;
27
28
/**
29
 * PHP_CodeSniffer standard installation manager.
30
 *
31
 * @author Franck Nijhof <[email protected]>
32
 */
33
class Plugin implements PluginInterface, EventSubscriberInterface
34
{
35
36
    const KEY_MAX_DEPTH = 'phpcodesniffer-search-depth';
37
38
    const MESSAGE_ERROR_WRONG_MAX_DEPTH =
39
        'The value of "%s" (in the composer.json "extra".section) must be an integer larger then %d, %s given.';
40
    const MESSAGE_RUNNING_INSTALLER = 'Running PHPCodeSniffer Composer Installer';
41
    const MESSAGE_NOTHING_TO_INSTALL = 'Nothing to install or update';
42
    const MESSAGE_NOT_INSTALLED = 'PHPCodeSniffer is not installed';
43
44
    const PACKAGE_NAME = 'squizlabs/php_codesniffer';
45
    const PACKAGE_TYPE = 'phpcodesniffer-standard';
46
47
    const PHPCS_CONFIG_KEY = 'installed_paths';
48
49
    /**
50
     * @var Composer
51
     */
52
    private $composer;
53
54
    /**
55
     * @var IOInterface
56
     */
57
    private $io;
58
59
    /**
60
     * @var array
61
     */
62
    private $installedPaths;
63
64
    /**
65
     * @var ProcessBuilder
66
     */
67
    private $processBuilder;
68
69
    /**
70
     * Triggers the plugin's main functionality.
71
     *
72
     * Makes it possible to run the plugin as a custom command.
73
     *
74
     * @param Event $event
75
     *
76
     * @throws \InvalidArgumentException
77
     * @throws \RuntimeException
78
     * @throws LogicException
79
     * @throws ProcessFailedException
80
     * @throws RuntimeException
81
     */
82
    public static function run(Event $event)
83
    {
84
        $io = $event->getIO();
85
        $composer = $event->getComposer();
86
87
        $instance = new static();
88
89
        $instance->io = $io;
90
        $instance->composer = $composer;
91
        $instance->init();
92
        $instance->onDependenciesChangedEvent();
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     *
98
     * @throws \RuntimeException
99
     * @throws LogicException
100
     * @throws RuntimeException
101
     * @throws ProcessFailedException
102
     */
103
    public function activate(Composer $composer, IOInterface $io)
104
    {
105
        $this->composer = $composer;
106
        $this->io = $io;
107
108
        $this->init();
109
    }
110
111
    /**
112
     * Prepares the plugin so it's main functionality can be run.
113
     *
114
     * @throws \RuntimeException
115
     * @throws LogicException
116
     * @throws ProcessFailedException
117
     * @throws RuntimeException
118
     */
119
    private function init()
120
    {
121
        $this->installedPaths = array();
122
123
        $this->processBuilder = new ProcessBuilder();
124
        $this->processBuilder->setPrefix($this->composer->getConfig()->get('bin-dir') . DIRECTORY_SEPARATOR . 'phpcs');
125
    }
126
127
    /**
128
     * {@inheritDoc}
129
     */
130
    public static function getSubscribedEvents()
131
    {
132
        return array(
133
            ScriptEvents::POST_INSTALL_CMD => array(
134
                array('onDependenciesChangedEvent', 0),
135
            ),
136
            ScriptEvents::POST_UPDATE_CMD => array(
137
                array('onDependenciesChangedEvent', 0),
138
            ),
139
        );
140
    }
141
142
    /**
143
     * Entry point for post install and post update events.
144
     *
145
     * @throws \InvalidArgumentException
146
     * @throws RuntimeException
147
     * @throws LogicException
148
     * @throws ProcessFailedException
149
     */
150
    public function onDependenciesChangedEvent()
151
    {
152
        $io = $this->io;
153
        $isVerbose = $io->isVerbose();
154
155
        if ($isVerbose) {
156
            $io->write(sprintf('<info>%s</info>', self::MESSAGE_RUNNING_INSTALLER));
157
        }
158
159
        if ($this->isPHPCodeSnifferInstalled() === true) {
160
            $this->loadInstalledPaths();
161
            $installPathCleaned = $this->cleanInstalledPaths();
162
            $installPathUpdated = $this->updateInstalledPaths();
163
164
            if ($installPathCleaned === true || $installPathUpdated === true) {
165
                $this->saveInstalledPaths();
166
            } elseif ($isVerbose) {
167
                $io->write(sprintf('<info>%s</info>', self::MESSAGE_NOTHING_TO_INSTALL));
168
            }
169
        } elseif ($isVerbose) {
170
            $io->write(sprintf('<info>%s</info>', self::MESSAGE_NOT_INSTALLED));
171
        }
172
    }
173
174
    /**
175
     * Load all paths from PHP_CodeSniffer into an array.
176
     *
177
     * @throws RuntimeException
178
     * @throws LogicException
179
     * @throws ProcessFailedException
180
     */
181
    private function loadInstalledPaths()
182
    {
183
        if ($this->isPHPCodeSnifferInstalled() === true) {
184
            $output = $this->processBuilder
185
                ->setArguments(array('--config-show', self::PHPCS_CONFIG_KEY))
186
                ->getProcess()
187
                ->mustRun()
188
                ->getOutput();
189
190
            $phpcsInstalledPaths = str_replace(self::PHPCS_CONFIG_KEY . ': ', '', $output);
191
            $phpcsInstalledPaths = trim($phpcsInstalledPaths);
192
193
            if ($phpcsInstalledPaths !== '') {
194
                $this->installedPaths = explode(',', $phpcsInstalledPaths);
195
            }
196
        }
197
    }
198
199
    /**
200
     * Save all coding standard paths back into PHP_CodeSniffer
201
     *
202
     * @throws RuntimeException
203
     * @throws LogicException
204
     * @throws ProcessFailedException
205
     */
206
    private function saveInstalledPaths()
207
    {
208
        // Check if we found installed paths to set.
209
        if (count($this->installedPaths) !== 0) {
210
            $paths = implode(',', $this->installedPaths);
211
            $arguments = array('--config-set', self::PHPCS_CONFIG_KEY, $paths);
212
            $configMessage = sprintf(
213
                'PHP CodeSniffer Config <info>%s</info> <comment>set to</comment> <info>%s</info>',
214
                self::PHPCS_CONFIG_KEY,
215
                $paths
216
            );
217
        } else {
218
            // Delete the installed paths if none were found.
219
            $arguments = array('--config-delete', self::PHPCS_CONFIG_KEY);
220
            $configMessage = sprintf(
221
                'PHP CodeSniffer Config <info>%s</info> <comment>delete</comment>',
222
                self::PHPCS_CONFIG_KEY
223
            );
224
        }
225
226
        $this->io->write($configMessage);
227
228
        $configResult = $this->processBuilder
229
            ->setArguments($arguments)
230
            ->getProcess()
231
            ->mustRun()
232
            ->getOutput()
233
        ;
234
235
        if ($this->io->isVerbose() && !empty($configResult)) {
236
            $this->io->write(sprintf('<info>%s</info>', $configResult));
237
        }
238
    }
239
240
    /**
241
     * Iterate trough all known paths and check if they are still valid.
242
     *
243
     * If path does not exists, is not an directory or isn't readable, the path
244
     * is removed from the list.
245
     *
246
     * @return bool True if changes where made, false otherwise
247
     */
248
    private function cleanInstalledPaths()
249
    {
250
        $changes = false;
251
        foreach ($this->installedPaths as $key => $path) {
252
            // This might be a relative path as well
253
            $alternativePath = realpath($this->getPHPCodeSnifferInstallPath() . DIRECTORY_SEPARATOR . $path);
254
255
            if ((is_dir($path) === false || is_readable($path) === false) &&
256
                (is_dir($alternativePath) === false || is_readable($alternativePath) === false)
257
            ) {
258
                unset($this->installedPaths[$key]);
259
                $changes = true;
260
            }
261
        }
262
        return $changes;
263
    }
264
265
    /**
266
     * Check all installed packages (including the root package) against
267
     * the installed paths from PHP_CodeSniffer and add the missing ones.
268
     *
269
     * @return bool True if changes where made, false otherwise
270
     *
271
     * @throws \InvalidArgumentException
272
     * @throws \RuntimeException
273
     */
274
    private function updateInstalledPaths()
275
    {
276
        $changes = false;
277
278
        $searchPaths = array(getcwd());
279
        $codingStandardPackages = $this->getPHPCodingStandardPackages();
280
        foreach ($codingStandardPackages as $package) {
281
            $searchPaths[] = $this->composer->getInstallationManager()->getInstallPath($package);
282
        }
283
284
        $finder = new Finder();
285
        $finder->files()
286
            ->ignoreUnreadableDirs()
287
            ->ignoreVCS(true)
288
            ->depth('<= ' . $this->getMaxDepth())
289
            ->depth('>= ' . $this->getMinDepth())
290
            ->name('ruleset.xml')
291
            ->in($searchPaths);
292
293
        // Process each found possible ruleset.
294
        foreach ($finder as $ruleset) {
295
            $standardsPath = $ruleset->getPath();
296
297
            // Pick the directory above the directory containing the standard, unless this is the project root.
298
            if ($standardsPath !== getcwd()) {
299
                $standardsPath = dirname($standardsPath);
300
            }
301
302
            // Use relative paths for local project repositories.
303
            if ($this->isRunningGlobally() === false) {
304
                $standardsPath = $this->getRelativePath($standardsPath);
305
            }
306
307
            // De-duplicate and add when directory is not configured.
308
            if (in_array($standardsPath, $this->installedPaths, true) === false) {
309
                $this->installedPaths[] = $standardsPath;
310
                $changes = true;
311
            }
312
        }
313
314
        return $changes;
315
    }
316
317
    /**
318
     * Iterates through Composers' local repository looking for valid Coding
319
     * Standard packages.
320
     *
321
     * If the package is the RootPackage (the one the plugin is installed into),
322
     * the package is ignored for now since it needs a different install path logic.
323
     *
324
     * @return array Composer packages containing coding standard(s)
325
     */
326
    private function getPHPCodingStandardPackages()
327
    {
328
        $codingStandardPackages = array_filter(
329
            $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(),
330
            function (PackageInterface $package) {
331
                if ($package instanceof AliasPackage) {
332
                    return false;
333
                }
334
                return $package->getType() === Plugin::PACKAGE_TYPE;
335
            }
336
        );
337
338
        if (! $this->composer->getPackage() instanceof RootpackageInterface
339
            && $this->composer->getPackage()->getType() === self::PACKAGE_TYPE
340
        ) {
341
            $codingStandardPackages[] = $this->composer->getPackage();
342
        }
343
344
        return $codingStandardPackages;
345
    }
346
347
    /**
348
     * Searches for the installed PHP_CodeSniffer Composer package
349
     *
350
     * @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
351
     *
352
     * @return PackageInterface|null
353
     */
354
    private function getPHPCodeSnifferPackage($versionConstraint = null)
355
    {
356
        $packages = $this
357
            ->composer
358
            ->getRepositoryManager()
359
            ->getLocalRepository()
360
            ->findPackages(self::PACKAGE_NAME, $versionConstraint);
361
362
        return array_shift($packages);
363
    }
364
365
    /**
366
     * Returns the path to the PHP_CodeSniffer package installation location
367
     *
368
     * @return string
369
     */
370
    private function getPHPCodeSnifferInstallPath()
371
    {
372
        return $this->composer->getInstallationManager()->getInstallPath($this->getPHPCodeSnifferPackage());
0 ignored issues
show
Bug introduced by
It seems like $this->getPHPCodeSnifferPackage() can be null; however, getInstallPath() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
373
    }
374
375
    /**
376
     * Simple check if PHP_CodeSniffer is installed.
377
     *
378
     * @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
379
     *
380
     * @return bool Whether PHP_CodeSniffer is installed
381
     */
382
    private function isPHPCodeSnifferInstalled($versionConstraint = null)
383
    {
384
        return ($this->getPHPCodeSnifferPackage($versionConstraint) !== null);
385
    }
386
387
    /**
388
     * Test if composer is running "global"
389
     * This check kinda dirty, but it is the "Composer Way"
390
     *
391
     * @return bool Whether Composer is running "globally"
392
     *
393
     * @throws \RuntimeException
394
     */
395
    private function isRunningGlobally()
396
    {
397
        return ($this->composer->getConfig()->get('home') === getcwd());
398
    }
399
400
    /**
401
     * Returns the relative path to PHP_CodeSniffer from any other absolute path
402
     *
403
     * @param string $to Absolute path
404
     *
405
     * @return string Relative path
406
     */
407
    private function getRelativePath($to)
408
    {
409
        $from = $this->getPHPCodeSnifferInstallPath();
410
411
        // Some compatibility fixes for Windows paths
412
        $from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
413
        $to = is_dir($to) ? rtrim($to, '\/') . '/' : $to;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $to. This often makes code more readable.
Loading history...
414
        $from = str_replace('\\', '/', $from);
415
        $to = str_replace('\\', '/', $to);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $to. This often makes code more readable.
Loading history...
416
417
        $from = explode('/', $from);
418
        $to = explode('/', $to);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $to. This often makes code more readable.
Loading history...
419
        $relPath = $to;
420
421
        foreach ($from as $depth => $dir) {
422
            // Find first non-matching dir
423
            if ($dir === $to[$depth]) {
424
                // Ignore this directory
425
                array_shift($relPath);
426
            } else {
427
                // Get number of remaining dirs to $from
428
                $remaining = count($from) - $depth;
429
                if ($remaining > 1) {
430
                    // Add traversals up to first matching dir
431
                    $padLength = (count($relPath) + $remaining - 1) * -1;
432
                    $relPath = array_pad($relPath, $padLength, '..');
433
                    break;
434
                } else {
435
                    $relPath[0] = './' . $relPath[0];
436
                }
437
            }
438
        }
439
        return implode('/', $relPath);
440
    }
441
442
    /**
443
     * Determines the maximum search depth when searching for Coding Standards.
444
     *
445
     * @return int
446
     *
447
     * @throws \InvalidArgumentException
448
     */
449
    private function getMaxDepth()
450
    {
451
        $maxDepth = 3;
452
453
        $extra = $this->composer->getPackage()->getExtra();
454
455
        if (array_key_exists(self::KEY_MAX_DEPTH, $extra)) {
456
            $maxDepth = $extra[self::KEY_MAX_DEPTH];
457
            $minDepth = $this->getMinDepth();
458
459
            if (is_int($maxDepth) === false     /* Must be an integer */
460
                || $maxDepth <= $minDepth       /* Larger than the minimum */
461
                || is_float($maxDepth) === true /* Within the boundaries of integer */
462
            ) {
463
                $message = vsprintf(
464
                    self::MESSAGE_ERROR_WRONG_MAX_DEPTH,
465
                    array(
466
                        'key' => self::KEY_MAX_DEPTH,
467
                        'min' => $minDepth,
468
                        'given' => var_export($maxDepth, true),
469
                    )
470
                );
471
472
                throw new \InvalidArgumentException($message);
473
            }
474
        }
475
476
        return $maxDepth;
477
    }
478
479
    /**
480
     * Returns the minimal search depth for Coding Standard packages.
481
     *
482
     * Usually this is 0, unless PHP_CodeSniffer >= 3 is used.
483
     *
484
     * @return int
485
     */
486
    private function getMinDepth()
487
    {
488
        if ($this->isPHPCodeSnifferInstalled('>= 3.0.0') !== true) {
489
            return 1;
490
        }
491
        return 0;
492
    }
493
}
494