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 (#71)
by Franck
50:54 queued 18:56
created

Plugin::getRelativePath()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

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