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 ( b3ce18...d9d33d )
by Franck
11s
created

Plugin::isPHPCodeSnifferInstalled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
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-2017 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
    const MESSAGE_RUNNING_INSTALLER = 'Running PHPCodeSniffer Composer Installer';
36
    const MESSAGE_NOTHING_TO_INSTALL = 'Nothing to install or update';
37
    const MESSAGE_NOT_INSTALLED = 'PHPCodeSniffer is not installed';
38
39
    const PACKAGE_NAME = 'squizlabs/php_codesniffer';
40
    const PACKAGE_TYPE = 'phpcodesniffer-standard';
41
42
    const PHPCS_CONFIG_KEY = 'installed_paths';
43
44
    /**
45
     * @var Composer
46
     */
47
    private $composer;
48
49
    /**
50
     * @var IOInterface
51
     */
52
    private $io;
53
54
    /**
55
     * @var array
56
     */
57
    private $installedPaths;
58
59
    /**
60
     * @var ProcessBuilder
61
     */
62
    private $processBuilder;
63
64
    /**
65
     * Triggers the plugin's main functionality.
66
     *
67
     * Makes it possible to run the plugin as a custom command.
68
     *
69
     * @param Event $event
70
     *
71
     * @throws \InvalidArgumentException
72
     * @throws \RuntimeException
73
     * @throws LogicException
74
     * @throws ProcessFailedException
75
     * @throws RuntimeException
76
     */
77
    public static function run(Event $event)
78
    {
79
        $io = $event->getIO();
80
        $composer = $event->getComposer();
81
82
        $instance = new static();
83
84
        $instance->io = $io;
85
        $instance->composer = $composer;
86
        $instance->init();
87
        $instance->onDependenciesChangedEvent();
88
    }
89
90
    /**
91
     * {@inheritDoc}
92
     *
93
     * @throws \RuntimeException
94
     * @throws LogicException
95
     * @throws RuntimeException
96
     * @throws ProcessFailedException
97
     */
98
    public function activate(Composer $composer, IOInterface $io)
99
    {
100
        $this->composer = $composer;
101
        $this->io = $io;
102
103
        $this->init();
104
    }
105
106
    /**
107
     * Prepares the plugin so it's main functionality can be run.
108
     *
109
     * @throws \RuntimeException
110
     * @throws LogicException
111
     * @throws ProcessFailedException
112
     * @throws RuntimeException
113
     */
114
    private function init()
115
    {
116
        $this->installedPaths = [];
117
118
        $this->processBuilder = new ProcessBuilder();
119
        $this->processBuilder->setPrefix($this->composer->getConfig()->get('bin-dir') . DIRECTORY_SEPARATOR . 'phpcs');
120
121
        $this->loadInstalledPaths();
122
    }
123
124
    /**
125
     * {@inheritDoc}
126
     */
127
    public static function getSubscribedEvents()
128
    {
129
        return [
130
            ScriptEvents::POST_INSTALL_CMD => [
131
                ['onDependenciesChangedEvent', 0],
132
            ],
133
            ScriptEvents::POST_UPDATE_CMD => [
134
                ['onDependenciesChangedEvent', 0],
135
            ],
136
        ];
137
    }
138
139
    /**
140
     * Entry point for post install and post update events.
141
     *
142
     * @throws \InvalidArgumentException
143
     * @throws RuntimeException
144
     * @throws LogicException
145
     * @throws ProcessFailedException
146
     */
147
    public function onDependenciesChangedEvent()
148
    {
149
        $io = $this->io;
150
        $isVerbose = $io->isVerbose();
151
152
        if ($isVerbose) {
153
            $io->write(sprintf('<info>%s</info>', self::MESSAGE_RUNNING_INSTALLER));
154
        }
155
156
        if ($this->isPHPCodeSnifferInstalled() === true) {
157
            $installPathCleaned = $this->cleanInstalledPaths();
158
            $installPathUpdated = $this->updateInstalledPaths();
159
160
            if ($installPathCleaned === true || $installPathUpdated === true) {
161
                $this->saveInstalledPaths();
162
            } elseif ($isVerbose) {
163
                $io->write(sprintf('<info>%s</info>', self::MESSAGE_NOTHING_TO_INSTALL));
164
            }
165
        } elseif ($isVerbose) {
166
            $io->write(sprintf('<info>%s</info>', self::MESSAGE_NOT_INSTALLED));
167
        }
168
    }
169
170
    /**
171
     * Load all paths from PHP_CodeSniffer into an array.
172
     *
173
     * @throws RuntimeException
174
     * @throws LogicException
175
     * @throws ProcessFailedException
176
     */
177
    private function loadInstalledPaths()
178
    {
179
        if ($this->isPHPCodeSnifferInstalled() === true) {
180
            $output = $this->processBuilder
181
                ->setArguments(['--config-show', self::PHPCS_CONFIG_KEY])
182
                ->getProcess()
183
                ->mustRun()
184
                ->getOutput();
185
186
            $phpcsInstalledPaths = str_replace(self::PHPCS_CONFIG_KEY . ': ', '', $output);
187
            $phpcsInstalledPaths = trim($phpcsInstalledPaths);
188
189
            if ($phpcsInstalledPaths !== '') {
190
                $this->installedPaths = explode(',', $phpcsInstalledPaths);
191
            }
192
        }
193
    }
194
195
    /**
196
     * Save all coding standard paths back into PHP_CodeSniffer
197
     *
198
     * @throws RuntimeException
199
     * @throws LogicException
200
     * @throws ProcessFailedException
201
     */
202
    private function saveInstalledPaths()
203
    {
204
        // Check if we found installed paths to set.
205
        if (count($this->installedPaths) !== 0) {
206
            $paths = implode(',', $this->installedPaths);
207
            $arguments = ['--config-set', self::PHPCS_CONFIG_KEY, $paths];
208
            $configMessage = sprintf(
209
                'PHP CodeSniffer Config <info>%s</info> <comment>set to</comment> <info>%s</info>',
210
                self::PHPCS_CONFIG_KEY,
211
                $paths
212
            );
213
        } else {
214
            // Delete the installed paths if none were found.
215
            $arguments = ['--config-delete', self::PHPCS_CONFIG_KEY];
216
            $configMessage = sprintf(
217
                'PHP CodeSniffer Config <info>%s</info> <comment>delete</comment>',
218
                self::PHPCS_CONFIG_KEY
219
            );
220
        }
221
222
        $this->io->write($configMessage);
223
224
        $configResult = $this->processBuilder
225
            ->setArguments($arguments)
226
            ->getProcess()
227
            ->mustRun()
228
            ->getOutput()
229
        ;
230
231
        if ($this->io->isVerbose() && !empty($configResult)) {
232
            $this->io->write(sprintf('<info>%s</info>', $configResult));
233
        }
234
    }
235
236
    /**
237
     * Iterate trough all known paths and check if they are still valid.
238
     *
239
     * If path does not exists, is not an directory or isn't readable, the path
240
     * is removed from the list.
241
     *
242
     * @return bool True if changes where made, false otherwise
243
     */
244
    private function cleanInstalledPaths()
245
    {
246
        $changes = false;
247
        foreach ($this->installedPaths as $key => $path) {
248
            // This might be a relative path as well
249
            $alternativePath = realpath($this->getPHPCodeSnifferInstallPath() . DIRECTORY_SEPARATOR . $path);
250
251
            if ((is_dir($path) === false || is_readable($path) === false) &&
252
                (is_dir($alternativePath) === false || is_readable($alternativePath) === false)
253
            ) {
254
                unset($this->installedPaths[$key]);
255
                $changes = true;
256
            }
257
        }
258
        return $changes;
259
    }
260
261
    /**
262
     * Check all installed packages (including the root package) against
263
     * the installed paths from PHP_CodeSniffer and add the missing ones.
264
     *
265
     * @return bool True if changes where made, false otherwise
266
     *
267
     * @throws \InvalidArgumentException
268
     * @throws \RuntimeException
269
     */
270
    private function updateInstalledPaths()
271
    {
272
        $changes = false;
273
274
        $searchPaths = [getcwd()];
275
        $codingStandardPackages = $this->getPHPCodingStandardPackages();
276
        foreach ($codingStandardPackages as $package) {
277
            $searchPaths[] = $this->composer->getInstallationManager()->getInstallPath($package);
278
        }
279
280
        $finder = new Finder();
281
        $finder->files()
282
            ->ignoreUnreadableDirs()
283
            ->ignoreVCS(true)
284
            ->depth('< 4')
285
            ->name('ruleset.xml')
286
            ->in($searchPaths);
287
288
        // Only version 3.x and higher has support for having coding standard in the root of the directory.
289
        $allowCodingStandardsInRoot = $this->isPHPCodeSnifferInstalled('>= 3.0.0');
290
291
        if ($allowCodingStandardsInRoot !== true) {
292
            $finder->depth('>= 1');
293
        }
294
295
        foreach ($finder as $ruleset) {
296
            $standardsPath = $ruleset->getPath();
297
298
            if ($allowCodingStandardsInRoot === false) {
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
            if (in_array($standardsPath, $this->installedPaths, true) === false) {
308
                $this->installedPaths[] = $standardsPath;
309
                $changes = true;
310
            }
311
        }
312
313
        return $changes;
314
    }
315
316
    /**
317
     * Iterates through Composers' local repository looking for valid Coding
318
     * Standard packages.
319
     *
320
     * If the package is the RootPackage (the one the plugin is installed into),
321
     * the package is ignored for now since it needs a different install path logic.
322
     *
323
     * @return array Composer packages containing coding standard(s)
324
     */
325
    private function getPHPCodingStandardPackages()
326
    {
327
        $codingStandardPackages = array_filter(
328
            $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(),
329
            function (PackageInterface $package) {
330
                if ($package instanceof AliasPackage) {
331
                    return false;
332
                }
333
                return $package->getType() === Plugin::PACKAGE_TYPE;
334
            }
335
        );
336
337
        if (! $this->composer->getPackage() instanceof RootpackageInterface
338
            && $this->composer->getPackage()->getType() === self::PACKAGE_TYPE
339
        ) {
340
            $codingStandardPackages[] = $this->composer->getPackage();
341
        }
342
343
        return $codingStandardPackages;
344
    }
345
346
    /**
347
     * Searches for the installed PHP_CodeSniffer Composer package
348
     *
349
     * @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
350
     *
351
     * @return PackageInterface|null
352
     */
353
    private function getPHPCodeSnifferPackage($versionConstraint = null)
354
    {
355
        $packages = $this
356
            ->composer
357
            ->getRepositoryManager()
358
            ->getLocalRepository()
359
            ->findPackages(self::PACKAGE_NAME, $versionConstraint);
360
361
        return array_shift($packages);
362
    }
363
364
    /**
365
     * Returns the path to the PHP_CodeSniffer package installation location
366
     *
367
     * @return string
368
     */
369
    private function getPHPCodeSnifferInstallPath()
370
    {
371
        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...
372
    }
373
374
    /**
375
     * Simple check if PHP_CodeSniffer is installed.
376
     *
377
     * @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
378
     *
379
     * @return bool Whether PHP_CodeSniffer is installed
380
     */
381
    private function isPHPCodeSnifferInstalled($versionConstraint = null)
382
    {
383
        return ($this->getPHPCodeSnifferPackage($versionConstraint) !== null);
384
    }
385
386
    /**
387
     * Test if composer is running "global"
388
     * This check kinda dirty, but it is the "Composer Way"
389
     *
390
     * @return bool Whether Composer is running "globally"
391
     *
392
     * @throws \RuntimeException
393
     */
394
    private function isRunningGlobally()
395
    {
396
        return ($this->composer->getConfig()->get('home') === getcwd());
397
    }
398
399
    /**
400
     * Returns the relative path to PHP_CodeSniffer from any other absolute path
401
     *
402
     * @param string $to Absolute path
403
     *
404
     * @return string Relative path
405
     */
406
    private function getRelativePath($to)
407
    {
408
        $from = $this->getPHPCodeSnifferInstallPath();
409
410
        // Some compatibility fixes for Windows paths
411
        $from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
412
        $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...
413
        $from = str_replace('\\', '/', $from);
414
        $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...
415
416
        $from = explode('/', $from);
417
        $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...
418
        $relPath = $to;
419
420
        foreach ($from as $depth => $dir) {
421
            // Find first non-matching dir
422
            if ($dir === $to[$depth]) {
423
                // Ignore this directory
424
                array_shift($relPath);
425
            } else {
426
                // Get number of remaining dirs to $from
427
                $remaining = count($from) - $depth;
428
                if ($remaining > 1) {
429
                    // Add traversals up to first matching dir
430
                    $padLength = (count($relPath) + $remaining - 1) * -1;
431
                    $relPath = array_pad($relPath, $padLength, '..');
432
                    break;
433
                } else {
434
                    $relPath[0] = './' . $relPath[0];
435
                }
436
            }
437
        }
438
        return implode('/', $relPath);
439
    }
440
}
441