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
Pull Request — master (#80)
by Ben
01:14
created

Plugin::saveInstalledPaths()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 48
rs 8.8234
c 0
b 0
f 0
cc 5
nc 8
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 Composer\Util\Filesystem;
23
use Composer\Util\ProcessExecutor;
24
use Symfony\Component\Finder\Finder;
25
use Symfony\Component\Process\Exception\LogicException;
26
use Symfony\Component\Process\Exception\ProcessFailedException;
27
use Symfony\Component\Process\Exception\RuntimeException;
28
use Symfony\Component\Process\PhpExecutableFinder;
29
30
/**
31
 * PHP_CodeSniffer standard installation manager.
32
 *
33
 * @author Franck Nijhof <[email protected]>
34
 */
35
class Plugin implements PluginInterface, EventSubscriberInterface
36
{
37
38
    const KEY_MAX_DEPTH = 'phpcodesniffer-search-depth';
39
40
    const MESSAGE_ERROR_WRONG_MAX_DEPTH =
41
        'The value of "%s" (in the composer.json "extra".section) must be an integer larger then %d, %s given.';
42
    const MESSAGE_NOT_INSTALLED = 'PHPCodeSniffer is not installed';
43
    const MESSAGE_NOTHING_TO_INSTALL = 'Nothing to install or update';
44
    const MESSAGE_RUNNING_INSTALLER = 'Running PHPCodeSniffer Composer Installer';
45
46
    const PACKAGE_NAME = 'squizlabs/php_codesniffer';
47
    const PACKAGE_TYPE = 'phpcodesniffer-standard';
48
49
    const PHPCS_CONFIG_KEY = 'installed_paths';
50
51
    /**
52
     * @var Composer
53
     */
54
    private $composer;
55
56
    /**
57
     * @var string
58
     */
59
    private $cwd;
60
61
    /**
62
     * @var Filesystem
63
     */
64
    private $filesystem;
65
66
    /**
67
     * @var array
68
     */
69
    private $installedPaths;
70
71
    /**
72
     * @var IOInterface
73
     */
74
    private $io;
75
76
    /**
77
     * @var ProcessExecutor
78
     */
79
    private $processExecutor;
80
81
    /**
82
     * Triggers the plugin's main functionality.
83
     *
84
     * Makes it possible to run the plugin as a custom command.
85
     *
86
     * @param Event $event
87
     *
88
     * @throws \InvalidArgumentException
89
     * @throws \RuntimeException
90
     * @throws LogicException
91
     * @throws ProcessFailedException
92
     * @throws RuntimeException
93
     */
94
    public static function run(Event $event)
95
    {
96
        $io = $event->getIO();
97
        $composer = $event->getComposer();
98
99
        $instance = new static();
100
101
        $instance->io = $io;
102
        $instance->composer = $composer;
103
        $instance->init();
104
        $instance->onDependenciesChangedEvent();
105
    }
106
107
    /**
108
     * {@inheritDoc}
109
     *
110
     * @throws \RuntimeException
111
     * @throws LogicException
112
     * @throws ProcessFailedException
113
     * @throws RuntimeException
114
     */
115
    public function activate(Composer $composer, IOInterface $io)
116
    {
117
        $this->composer = $composer;
118
        $this->io = $io;
119
120
        $this->init();
121
    }
122
123
    /**
124
     * Prepares the plugin so it's main functionality can be run.
125
     *
126
     * @throws \RuntimeException
127
     * @throws LogicException
128
     * @throws ProcessFailedException
129
     * @throws RuntimeException
130
     */
131
    private function init()
132
    {
133
        $this->cwd = getcwd();
134
        $this->installedPaths = array();
135
136
        $this->processExecutor = new ProcessExecutor($this->io);
137
        $this->filesystem = new Filesystem($this->processExecutor);
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143
    public static function getSubscribedEvents()
144
    {
145
        return array(
146
            ScriptEvents::POST_INSTALL_CMD => array(
147
                array('onDependenciesChangedEvent', 0),
148
            ),
149
            ScriptEvents::POST_UPDATE_CMD => array(
150
                array('onDependenciesChangedEvent', 0),
151
            ),
152
        );
153
    }
154
155
    /**
156
     * Entry point for post install and post update events.
157
     *
158
     * @throws \InvalidArgumentException
159
     * @throws LogicException
160
     * @throws ProcessFailedException
161
     * @throws RuntimeException
162
     */
163
    public function onDependenciesChangedEvent()
164
    {
165
        $io = $this->io;
166
        $isVerbose = $io->isVerbose();
167
168
        if ($isVerbose) {
169
            $io->write(sprintf('<info>%s</info>', self::MESSAGE_RUNNING_INSTALLER));
170
        }
171
172
        if ($this->isPHPCodeSnifferInstalled() === true) {
173
            $this->loadInstalledPaths();
174
            $installPathCleaned = $this->cleanInstalledPaths();
175
            $installPathUpdated = $this->updateInstalledPaths();
176
177
            if ($installPathCleaned === true || $installPathUpdated === true) {
178
                $this->saveInstalledPaths();
179
            } elseif ($isVerbose) {
180
                $io->write(sprintf('<info>%s</info>', self::MESSAGE_NOTHING_TO_INSTALL));
181
            }
182
        } elseif ($isVerbose) {
183
            $io->write(sprintf('<info>%s</info>', self::MESSAGE_NOT_INSTALLED));
184
        }
185
    }
186
187
    /**
188
     * Load all paths from PHP_CodeSniffer into an array.
189
     *
190
     * @throws LogicException
191
     * @throws ProcessFailedException
192
     * @throws RuntimeException
193
     */
194
    private function loadInstalledPaths()
195
    {
196
        if ($this->isPHPCodeSnifferInstalled() === true) {
197
            $this->processExecutor->execute(
198
                sprintf(
199
                    'phpcs --config-show %s',
200
                    self::PHPCS_CONFIG_KEY
201
                ),
202
                $output,
203
                $this->composer->getConfig()->get('bin-dir')
204
            );
205
206
            $phpcsInstalledPaths = str_replace(self::PHPCS_CONFIG_KEY . ': ', '', $output);
207
            $phpcsInstalledPaths = trim($phpcsInstalledPaths);
208
209
            if ($phpcsInstalledPaths !== '') {
210
                $this->installedPaths = explode(',', $phpcsInstalledPaths);
211
            }
212
        }
213
    }
214
215
    /**
216
     * Save all coding standard paths back into PHP_CodeSniffer
217
     *
218
     * @throws LogicException
219
     * @throws ProcessFailedException
220
     * @throws RuntimeException
221
     */
222
    private function saveInstalledPaths()
223
    {
224
        // Check if we found installed paths to set.
225
        if (count($this->installedPaths) !== 0) {
226
            $paths = implode(',', $this->installedPaths);
227
            $arguments = array('--config-set', self::PHPCS_CONFIG_KEY, $paths);
228
            $configMessage = sprintf(
229
                'PHP CodeSniffer Config <info>%s</info> <comment>set to</comment> <info>%s</info>',
230
                self::PHPCS_CONFIG_KEY,
231
                $paths
232
            );
233
        } else {
234
            // Delete the installed paths if none were found.
235
            $arguments = array('--config-delete', self::PHPCS_CONFIG_KEY);
236
            $configMessage = sprintf(
237
                'PHP CodeSniffer Config <info>%s</info> <comment>delete</comment>',
238
                self::PHPCS_CONFIG_KEY
239
            );
240
        }
241
242
        // Prepare message in case of failure
243
        $failMessage = sprintf(
244
            'Failed to set PHP CodeSniffer <info>%s</info> Config',
245
            self::PHPCS_CONFIG_KEY
246
        );
247
248
        // Okay, lets rock! 🤘
249
        $command = vsprintf('%s ./bin/phpcs %s', [
250
            'php executable' => $this->getPhpExecCommand(),
251
            'arguments' => implode(' ', $arguments)
252
        ]);
253
254
        $exitCode = $this->processExecutor->execute(
255
            $command,
256
            $configResult,
257
            $this->getPHPCodeSnifferInstallPath()
258
        );
259
260
        if ($exitCode === 0) {
261
            $this->io->write($configMessage);
262
        } else {
263
            $this->io->write($failMessage);
264
        }
265
266
        if ($this->io->isVerbose() && !empty($configResult)) {
267
            $this->io->write(sprintf('<info>%s</info>', $configResult));
268
        }
269
    }
270
271
    /**
272
     * Get the path to the current PHP version being used.
273
     *
274
     * Duplicate of the same in the EventDispatcher class in Composer itself.
275
     */
276
    protected function getPhpExecCommand()
277
    {
278
        $finder = new PhpExecutableFinder();
279
        
280
        $phpPath = $finder->find(false);
281
        
282
        if ( ! $phpPath) {
283
            throw new \RuntimeException('Failed to locate PHP binary to execute ' . $phpPath);
284
        }
285
286
        $phpArgs = $finder->findArguments();
287
        $phpArgs = $phpArgs
288
            ? ' ' . implode(' ', $phpArgs)
289
            : ''
290
        ;
291
292
        $command  = ProcessExecutor::escape($phpPath) .
293
            $phpArgs .
294
            ' -d allow_url_fopen=' . ProcessExecutor::escape(ini_get('allow_url_fopen')) .
295
            ' -d disable_functions=' . ProcessExecutor::escape(ini_get('disable_functions')) .
296
            ' -d memory_limit=' . ProcessExecutor::escape(ini_get('memory_limit'))
297
        ;
298
299
        return $command;
300
    }
301
302
    /**
303
     * Iterate trough all known paths and check if they are still valid.
304
     *
305
     * If path does not exists, is not an directory or isn't readable, the path
306
     * is removed from the list.
307
     *
308
     * @return bool True if changes where made, false otherwise
309
     */
310
    private function cleanInstalledPaths()
311
    {
312
        $changes = false;
313
        foreach ($this->installedPaths as $key => $path) {
314
            // This might be a relative path as well
315
            $alternativePath = realpath($this->getPHPCodeSnifferInstallPath() . DIRECTORY_SEPARATOR . $path);
316
317
            if ((is_dir($path) === false || is_readable($path) === false) &&
318
                (is_dir($alternativePath) === false || is_readable($alternativePath) === false)
319
            ) {
320
                unset($this->installedPaths[$key]);
321
                $changes = true;
322
            }
323
        }
324
        return $changes;
325
    }
326
327
    /**
328
     * Check all installed packages (including the root package) against
329
     * the installed paths from PHP_CodeSniffer and add the missing ones.
330
     *
331
     * @return bool True if changes where made, false otherwise
332
     *
333
     * @throws \InvalidArgumentException
334
     * @throws \RuntimeException
335
     */
336
    private function updateInstalledPaths()
337
    {
338
        $changes = false;
339
340
        $searchPaths = array($this->cwd);
341
        $codingStandardPackages = $this->getPHPCodingStandardPackages();
342
        foreach ($codingStandardPackages as $package) {
343
            $installPath = $this->composer->getInstallationManager()->getInstallPath($package);
344
            if ($this->filesystem->isAbsolutePath($installPath) === false) {
345
                $installPath = $this->filesystem->normalizePath(
346
                    $this->cwd . DIRECTORY_SEPARATOR . $installPath
347
                );
348
            }
349
            $searchPaths[] = $installPath;
350
        }
351
352
        $finder = new Finder();
353
        $finder->files()
354
            ->depth('<= ' . $this->getMaxDepth())
355
            ->depth('>= ' . $this->getMinDepth())
356
            ->ignoreUnreadableDirs()
357
            ->ignoreVCS(true)
358
            ->in($searchPaths)
359
            ->name('ruleset.xml');
360
361
        // Process each found possible ruleset.
362
        foreach ($finder as $ruleset) {
363
            $standardsPath = $ruleset->getPath();
364
365
            // Pick the directory above the directory containing the standard, unless this is the project root.
366
            if ($standardsPath !== $this->cwd) {
367
                $standardsPath = dirname($standardsPath);
368
            }
369
370
            // Use relative paths for local project repositories.
371
            if ($this->isRunningGlobally() === false) {
372
                $standardsPath = $this->filesystem->findShortestPath(
373
                    $this->getPHPCodeSnifferInstallPath(),
374
                    $standardsPath,
375
                    true
376
                );
377
            }
378
379
            // De-duplicate and add when directory is not configured.
380
            if (in_array($standardsPath, $this->installedPaths, true) === false) {
381
                $this->installedPaths[] = $standardsPath;
382
                $changes = true;
383
            }
384
        }
385
386
        return $changes;
387
    }
388
389
    /**
390
     * Iterates through Composers' local repository looking for valid Coding
391
     * Standard packages.
392
     *
393
     * If the package is the RootPackage (the one the plugin is installed into),
394
     * the package is ignored for now since it needs a different install path logic.
395
     *
396
     * @return array Composer packages containing coding standard(s)
397
     */
398
    private function getPHPCodingStandardPackages()
399
    {
400
        $codingStandardPackages = array_filter(
401
            $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(),
402
            function (PackageInterface $package) {
403
                if ($package instanceof AliasPackage) {
404
                    return false;
405
                }
406
                return $package->getType() === Plugin::PACKAGE_TYPE;
407
            }
408
        );
409
410
        if (! $this->composer->getPackage() instanceof RootpackageInterface
411
            && $this->composer->getPackage()->getType() === self::PACKAGE_TYPE
412
        ) {
413
            $codingStandardPackages[] = $this->composer->getPackage();
414
        }
415
416
        return $codingStandardPackages;
417
    }
418
419
    /**
420
     * Searches for the installed PHP_CodeSniffer Composer package
421
     *
422
     * @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
423
     *
424
     * @return PackageInterface|null
425
     */
426
    private function getPHPCodeSnifferPackage($versionConstraint = null)
427
    {
428
        $packages = $this
429
            ->composer
430
            ->getRepositoryManager()
431
            ->getLocalRepository()
432
            ->findPackages(self::PACKAGE_NAME, $versionConstraint);
433
434
        return array_shift($packages);
435
    }
436
437
    /**
438
     * Returns the path to the PHP_CodeSniffer package installation location
439
     *
440
     * @return string
441
     */
442
    private function getPHPCodeSnifferInstallPath()
443
    {
444
        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...
445
    }
446
447
    /**
448
     * Simple check if PHP_CodeSniffer is installed.
449
     *
450
     * @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
451
     *
452
     * @return bool Whether PHP_CodeSniffer is installed
453
     */
454
    private function isPHPCodeSnifferInstalled($versionConstraint = null)
455
    {
456
        return ($this->getPHPCodeSnifferPackage($versionConstraint) !== null);
457
    }
458
459
    /**
460
     * Test if composer is running "global"
461
     * This check kinda dirty, but it is the "Composer Way"
462
     *
463
     * @return bool Whether Composer is running "globally"
464
     *
465
     * @throws \RuntimeException
466
     */
467
    private function isRunningGlobally()
468
    {
469
        return ($this->composer->getConfig()->get('home') === $this->cwd);
470
    }
471
472
    /**
473
     * Determines the maximum search depth when searching for Coding Standards.
474
     *
475
     * @return int
476
     *
477
     * @throws \InvalidArgumentException
478
     */
479
    private function getMaxDepth()
480
    {
481
        $maxDepth = 3;
482
483
        $extra = $this->composer->getPackage()->getExtra();
484
485
        if (array_key_exists(self::KEY_MAX_DEPTH, $extra)) {
486
            $maxDepth = $extra[self::KEY_MAX_DEPTH];
487
            $minDepth = $this->getMinDepth();
488
489
            if (is_int($maxDepth) === false     /* Must be an integer */
490
                || $maxDepth <= $minDepth       /* Larger than the minimum */
491
                || is_float($maxDepth) === true /* Within the boundaries of integer */
492
            ) {
493
                $message = vsprintf(
494
                    self::MESSAGE_ERROR_WRONG_MAX_DEPTH,
495
                    array(
496
                        'key' => self::KEY_MAX_DEPTH,
497
                        'min' => $minDepth,
498
                        'given' => var_export($maxDepth, true),
499
                    )
500
                );
501
502
                throw new \InvalidArgumentException($message);
503
            }
504
        }
505
506
        return $maxDepth;
507
    }
508
509
    /**
510
     * Returns the minimal search depth for Coding Standard packages.
511
     *
512
     * Usually this is 0, unless PHP_CodeSniffer >= 3 is used.
513
     *
514
     * @return int
515
     */
516
    private function getMinDepth()
517
    {
518
        if ($this->isPHPCodeSnifferInstalled('>= 3.0.0') !== true) {
519
            return 1;
520
        }
521
        return 0;
522
    }
523
}
524