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 Juliette
01:28
created

Plugin::getPhpExecCommand()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
cc 3
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 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
        $exitCode = $this->processExecutor->execute(
243
            sprintf(
244
                '%s ./bin/phpcs %s',
245
                $this->getPhpExecCommand(),
246
                implode(' ', $arguments)
247
            ),
248
            $configResult,
249
            $this->getPHPCodeSnifferInstallPath()
250
        );
251
252
        if ($exitCode === 0) {
253
            $this->io->write($configMessage);
254
        } else {
255
            $failMessage = sprintf(
256
                'Failed to set PHP CodeSniffer <info>%s</info> Config',
257
                self::PHPCS_CONFIG_KEY
258
            );
259
            $this->io->write($failMessage);
260
        }
261
262
        if ($this->io->isVerbose() && !empty($configResult)) {
263
            $this->io->write(sprintf('<info>%s</info>', $configResult));
264
        }
265
    }
266
267
    /**
268
     * Get the path to the current PHP version being used.
269
     *
270
     * Duplicate of the same in the EventDispatcher class in Composer itself.
271
     */
272
    protected function getPhpExecCommand()
273
    {
274
        $finder = new PhpExecutableFinder();
275
        $phpPath = $finder->find(false);
276
        if (!$phpPath) {
277
            throw new \RuntimeException('Failed to locate PHP binary to execute ' . $phpPath);
278
        }
279
        $phpArgs = $finder->findArguments();
280
        $phpArgs = $phpArgs ? ' ' . implode(' ', $phpArgs) : '';
281
282
        $command  = ProcessExecutor::escape($phpPath);
283
        $command .= $phpArgs;
284
        $command .= ' -d allow_url_fopen=' . ProcessExecutor::escape(ini_get('allow_url_fopen'));
285
        $command .= ' -d disable_functions=' . ProcessExecutor::escape(ini_get('disable_functions'));
286
        $command .= ' -d memory_limit=' . ProcessExecutor::escape(ini_get('memory_limit'));
287
288
        return $command;
289
    }
290
291
    /**
292
     * Iterate trough all known paths and check if they are still valid.
293
     *
294
     * If path does not exists, is not an directory or isn't readable, the path
295
     * is removed from the list.
296
     *
297
     * @return bool True if changes where made, false otherwise
298
     */
299
    private function cleanInstalledPaths()
300
    {
301
        $changes = false;
302
        foreach ($this->installedPaths as $key => $path) {
303
            // This might be a relative path as well
304
            $alternativePath = realpath($this->getPHPCodeSnifferInstallPath() . DIRECTORY_SEPARATOR . $path);
305
306
            if ((is_dir($path) === false || is_readable($path) === false) &&
307
                (is_dir($alternativePath) === false || is_readable($alternativePath) === false)
308
            ) {
309
                unset($this->installedPaths[$key]);
310
                $changes = true;
311
            }
312
        }
313
        return $changes;
314
    }
315
316
    /**
317
     * Check all installed packages (including the root package) against
318
     * the installed paths from PHP_CodeSniffer and add the missing ones.
319
     *
320
     * @return bool True if changes where made, false otherwise
321
     *
322
     * @throws \InvalidArgumentException
323
     * @throws \RuntimeException
324
     */
325
    private function updateInstalledPaths()
326
    {
327
        $changes = false;
328
329
        $searchPaths = array($this->cwd);
330
        $codingStandardPackages = $this->getPHPCodingStandardPackages();
331
        foreach ($codingStandardPackages as $package) {
332
            $installPath = $this->composer->getInstallationManager()->getInstallPath($package);
333
            if ($this->filesystem->isAbsolutePath($installPath) === false) {
334
                $installPath = $this->filesystem->normalizePath(
335
                    $this->cwd . DIRECTORY_SEPARATOR . $installPath
336
                );
337
            }
338
            $searchPaths[] = $installPath;
339
        }
340
341
        $finder = new Finder();
342
        $finder->files()
343
            ->depth('<= ' . $this->getMaxDepth())
344
            ->depth('>= ' . $this->getMinDepth())
345
            ->ignoreUnreadableDirs()
346
            ->ignoreVCS(true)
347
            ->in($searchPaths)
348
            ->name('ruleset.xml');
349
350
        // Process each found possible ruleset.
351
        foreach ($finder as $ruleset) {
352
            $standardsPath = $ruleset->getPath();
353
354
            // Pick the directory above the directory containing the standard, unless this is the project root.
355
            if ($standardsPath !== $this->cwd) {
356
                $standardsPath = dirname($standardsPath);
357
            }
358
359
            // Use relative paths for local project repositories.
360
            if ($this->isRunningGlobally() === false) {
361
                $standardsPath = $this->filesystem->findShortestPath(
362
                    $this->getPHPCodeSnifferInstallPath(),
363
                    $standardsPath,
364
                    true
365
                );
366
            }
367
368
            // De-duplicate and add when directory is not configured.
369
            if (in_array($standardsPath, $this->installedPaths, true) === false) {
370
                $this->installedPaths[] = $standardsPath;
371
                $changes = true;
372
            }
373
        }
374
375
        return $changes;
376
    }
377
378
    /**
379
     * Iterates through Composers' local repository looking for valid Coding
380
     * Standard packages.
381
     *
382
     * If the package is the RootPackage (the one the plugin is installed into),
383
     * the package is ignored for now since it needs a different install path logic.
384
     *
385
     * @return array Composer packages containing coding standard(s)
386
     */
387
    private function getPHPCodingStandardPackages()
388
    {
389
        $codingStandardPackages = array_filter(
390
            $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(),
391
            function (PackageInterface $package) {
392
                if ($package instanceof AliasPackage) {
393
                    return false;
394
                }
395
                return $package->getType() === Plugin::PACKAGE_TYPE;
396
            }
397
        );
398
399
        if (! $this->composer->getPackage() instanceof RootpackageInterface
400
            && $this->composer->getPackage()->getType() === self::PACKAGE_TYPE
401
        ) {
402
            $codingStandardPackages[] = $this->composer->getPackage();
403
        }
404
405
        return $codingStandardPackages;
406
    }
407
408
    /**
409
     * Searches for the installed PHP_CodeSniffer Composer package
410
     *
411
     * @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
412
     *
413
     * @return PackageInterface|null
414
     */
415
    private function getPHPCodeSnifferPackage($versionConstraint = null)
416
    {
417
        $packages = $this
418
            ->composer
419
            ->getRepositoryManager()
420
            ->getLocalRepository()
421
            ->findPackages(self::PACKAGE_NAME, $versionConstraint);
422
423
        return array_shift($packages);
424
    }
425
426
    /**
427
     * Returns the path to the PHP_CodeSniffer package installation location
428
     *
429
     * @return string
430
     */
431
    private function getPHPCodeSnifferInstallPath()
432
    {
433
        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...
434
    }
435
436
    /**
437
     * Simple check if PHP_CodeSniffer is installed.
438
     *
439
     * @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
440
     *
441
     * @return bool Whether PHP_CodeSniffer is installed
442
     */
443
    private function isPHPCodeSnifferInstalled($versionConstraint = null)
444
    {
445
        return ($this->getPHPCodeSnifferPackage($versionConstraint) !== null);
446
    }
447
448
    /**
449
     * Test if composer is running "global"
450
     * This check kinda dirty, but it is the "Composer Way"
451
     *
452
     * @return bool Whether Composer is running "globally"
453
     *
454
     * @throws \RuntimeException
455
     */
456
    private function isRunningGlobally()
457
    {
458
        return ($this->composer->getConfig()->get('home') === $this->cwd);
459
    }
460
461
    /**
462
     * Determines the maximum search depth when searching for Coding Standards.
463
     *
464
     * @return int
465
     *
466
     * @throws \InvalidArgumentException
467
     */
468
    private function getMaxDepth()
469
    {
470
        $maxDepth = 3;
471
472
        $extra = $this->composer->getPackage()->getExtra();
473
474
        if (array_key_exists(self::KEY_MAX_DEPTH, $extra)) {
475
            $maxDepth = $extra[self::KEY_MAX_DEPTH];
476
            $minDepth = $this->getMinDepth();
477
478
            if (is_int($maxDepth) === false     /* Must be an integer */
479
                || $maxDepth <= $minDepth       /* Larger than the minimum */
480
                || is_float($maxDepth) === true /* Within the boundaries of integer */
481
            ) {
482
                $message = vsprintf(
483
                    self::MESSAGE_ERROR_WRONG_MAX_DEPTH,
484
                    array(
485
                        'key' => self::KEY_MAX_DEPTH,
486
                        'min' => $minDepth,
487
                        'given' => var_export($maxDepth, true),
488
                    )
489
                );
490
491
                throw new \InvalidArgumentException($message);
492
            }
493
        }
494
495
        return $maxDepth;
496
    }
497
498
    /**
499
     * Returns the minimal search depth for Coding Standard packages.
500
     *
501
     * Usually this is 0, unless PHP_CodeSniffer >= 3 is used.
502
     *
503
     * @return int
504
     */
505
    private function getMinDepth()
506
    {
507
        if ($this->isPHPCodeSnifferInstalled('>= 3.0.0') !== true) {
508
            return 1;
509
        }
510
        return 0;
511
    }
512
}
513