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.

Plugin::saveInstalledPaths()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

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