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 ( 221b57...2250c5 )
by Franck
12s
created

Plugin   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 394
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 19
Bugs 2 Features 0
Metric Value
wmc 43
c 19
b 2
f 0
lcom 1
cbo 11
dl 0
loc 394
rs 8.3157

15 Methods

Rating   Name   Duplication   Size   Complexity  
A run() 0 12 1
A init() 0 9 1
C onDependenciesChangedEvent() 0 22 7
B saveInstalledPaths() 0 33 4
B cleanInstalledPaths() 0 16 6
B updateInstalledPaths() 0 35 5
A activate() 0 7 1
A getSubscribedEvents() 0 11 1
A loadInstalledPaths() 0 17 3
A getPHPCodingStandardPackages() 0 20 4
A getPHPCodeSnifferPackage() 0 10 1
A getPHPCodeSnifferInstallPath() 0 4 1
A isPHPCodeSnifferInstalled() 0 4 1
A isRunningGlobally() 0 4 1
B getRelativePath() 0 34 6

How to fix   Complexity   

Complex Class

Complex classes like Plugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Plugin, and based on these observations, apply Extract Interface, too.

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