Passed
Push — master ( bbf990...4412c7 )
by Simon
02:59
created

Collector   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 489
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 66
eloc 184
dl 0
loc 489
c 4
b 0
f 0
rs 3.12

18 Methods

Rating   Name   Duplication   Size   Complexity  
A collectable() 0 7 3
A realpath() 0 16 4
A parse() 0 7 2
A __construct() 0 32 3
B _coverage() 0 32 11
A addFile() 0 16 6
A _processTree() 0 4 2
A _processMetrics() 0 26 5
A add() 0 9 3
A start() 0 8 2
A metrics() 0 16 2
A _processNode() 0 19 6
A export() 0 13 5
A _lineMetric() 0 8 2
A stop() 0 17 4
A base() 0 3 1
A _methodMetrics() 0 13 4
A driver() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Collector 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.

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 Collector, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Kahlan\Reporter\Coverage;
3
4
use Kahlan\Dir\Dir;
5
use Kahlan\Jit\ClassLoader;
6
7
class Collector
8
{
9
    /**
10
     * Stack of active collectors.
11
     *
12
     * @var array
13
     */
14
    protected static $_collectors = [];
15
16
    /**
17
     * Class dependencies.
18
     *
19
     * @var array
20
     */
21
    protected $_classes = [
22
        'parser' => 'Kahlan\Jit\Parser',
23
    ];
24
25
    /**
26
     * The driver instance which will log the coverage data.
27
     *
28
     * @var object
29
     */
30
    protected $_driver = null;
31
32
    /**
33
     * The path(s) which contain the code source files.
34
     *
35
     * @var array
36
     */
37
    protected $_paths = [];
38
39
    /**
40
     * The base path.
41
     *
42
     * @var string
43
     */
44
    protected $_base = '';
45
46
    /**
47
     * Some prefix to remove to get the real file path.
48
     *
49
     * @var string
50
     */
51
    protected $_prefix = '';
52
53
    /**
54
     * Indicate if the filesystem has volumes or not.
55
     *
56
     * @var boolean
57
     */
58
    protected $_hasVolume = false;
59
60
    /**
61
     * The files presents in `Collector::_paths`.
62
     *
63
     * @var array
64
     */
65
    protected $_files = [];
66
67
    /**
68
     * The coverage data.
69
     *
70
     * @var array
71
     */
72
    protected $_coverage = [];
73
74
    /**
75
     * The metrics.
76
     *
77
     * @var array
78
     */
79
    protected $_metrics = [];
80
81
    /**
82
     * Cache all parsed files
83
     *
84
     * @var array
85
     */
86
    protected $_tree = [];
87
88
    /**
89
     * Temps cache of processed lines
90
     *
91
     * @var array
92
     */
93
    protected $_processed = [];
94
95
    /**
96
     * The Constructor.
97
     *
98
     * @param array $config Possible options values are:
99
     *                    - `'driver'` _object_: the driver instance which will log the coverage data.
100
     *                    - `'path'`   _array_ : the path(s) which contain the code source files.
101
     *                    - `'base'`   _string_: the base path of the repo (default: `getcwd`).
102
     *                    - `'prefix'` _string_: some prefix to remove to get the real file path.
103
     */
104
    public function __construct($config = [])
105
    {
106
        $defaults = [
107
            'driver'         => null,
108
            'path'           => [],
109
            'include'        => '*.php',
110
            'exclude'        => [],
111
            'type'           => 'file',
112
            'skipDots'       => true,
113
            'leavesOnly'     => false,
114
            'followSymlinks' => true,
115
            'recursive'      => true,
116
            'base'           => getcwd(),
117
            'hasVolume'      => stripos(PHP_OS, 'WIN') === 0
118
        ];
119
        $config += $defaults;
120
121
        if ($loader = ClassLoader::instance()) {
122
            $config += ['prefix' => rtrim($loader->cachePath(), DS)];
0 ignored issues
show
Bug introduced by
The constant Kahlan\Reporter\Coverage\DS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
123
        } else {
124
            $config += ['prefix' => ''];
125
        }
126
127
        $this->_driver = $config['driver'];
128
        $this->_paths  = (array) $config['path'];
129
        $this->_base   = $config['base'];
130
        $this->_prefix = $config['prefix'];
131
        $this->_hasVolume = $config['hasVolume'];
132
133
        $files = Dir::scan($this->_paths, $config);
134
        foreach ($files as $file) {
135
            $this->_coverage[realpath($file)] = [];
136
        }
137
    }
138
139
    /**
140
     * Gets the used driver.
141
     *
142
     * @return object
143
     */
144
    public function driver()
145
    {
146
        return $this->_driver;
147
    }
148
149
    /**
150
     * Gets the base path used to compute relative paths.
151
     *
152
     * @return string
153
     */
154
    public function base()
155
    {
156
        return rtrim($this->_base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
157
    }
158
159
    /**
160
     * Starts collecting coverage data.
161
     *
162
     * @return boolean
163
     */
164
    public function start()
165
    {
166
        if ($collector = end(static::$_collectors)) {
167
            $collector->add($collector->_driver->stop());
168
        }
169
        static::$_collectors[] = $this;
170
        $this->_driver->start();
171
        return true;
172
    }
173
174
    /**
175
     * Stops collecting coverage data.
176
     *
177
     * @return boolean
178
     */
179
    public function stop($mergeToParent = true)
180
    {
181
        $collector = end(static::$_collectors);
182
        if ($collector !== $this) {
183
            return false;
184
        }
185
        array_pop(static::$_collectors);
186
        $collected = $this->_driver->stop();
187
        $this->add($collected);
188
189
        $collector = end(static::$_collectors);
190
        if (!$collector) {
191
            return true;
192
        }
193
        $collector->add($mergeToParent ? $collected : []);
194
        $collector->_driver->start();
195
        return true;
196
    }
197
198
    /**
199
     * Adds some coverage data to the collector.
200
     *
201
     * @param  array $coverage Some coverage data.
202
     * @return array           The current coverage data.
203
     */
204
    public function add($coverage)
205
    {
206
        if (!$coverage) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $coverage of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
207
            return;
208
        }
209
        foreach ($coverage as $file => $data) {
210
            $this->addFile($file, $data);
211
        }
212
        return $this->_coverage;
213
    }
214
215
    /**
216
     * Adds some coverage data to the collector.
217
     *
218
     * @param  string $file     A file path.
219
     * @param  array  $coverage Some coverage related to the file path.
220
     */
221
    public function addFile($file, $coverage)
222
    {
223
        $file = $this->realpath($file);
224
        if (!$this->collectable($file)) {
225
            return;
226
        }
227
        $nbLines = count(file($file));
228
229
        foreach ($coverage as $line => $value) {
230
            if ($line === 0 || $line >= $nbLines) {
231
                continue; // Because Xdebug bugs...
232
            }
233
            if (!isset($this->_coverage[$file][$line])) {
234
                $this->_coverage[$file][$line] = $value;
235
            } else {
236
                $this->_coverage[$file][$line] += $value;
237
            }
238
        }
239
    }
240
241
    /**
242
     * Helper for `Collector::addFile()`.
243
     *
244
     * @param  string $file     A file path.
245
     * @param  array  $coverage Some coverage related to the file path.
246
     */
247
    protected function _coverage($file, $coverage)
248
    {
249
        $result = [];
250
        $root = $this->parse($file);
251
        foreach ($root->lines['content'] as $num => $content) {
252
            $coverable = null;
253
            foreach ($content['nodes'] as $node) {
254
                if ($node->coverable && $node->lines['stop'] === $num) {
255
                    $coverable = $node;
256
                    break;
257
                }
258
            }
259
            if (!$coverable) {
260
                continue;
261
            }
262
            if (isset($coverage[$num])) {
263
                $result[$num] = $coverage[$num];
264
            } elseif (isset($coverable->lines['begin'])) {
265
                for ($i = $coverable->lines['begin']; $i <= $num; $i++) {
266
                    if (isset($coverage[$i])) {
267
                        $result[$num] = $coverage[$i];
268
                        break;
269
                    }
270
                }
271
                if (!isset($result[$num])) {
272
                    $result[$num] = 0;
273
                }
274
            } else {
275
                $result[$num] = 0;
276
            }
277
        }
278
        return $result;
279
    }
280
281
    /**
282
     * Checks if a filename is collectable.
283
     *
284
     * @param   string  $file A file path.
285
     * @return  boolean
286
     */
287
    public function collectable($file)
288
    {
289
        $file = $this->realpath($file);
290
        if (preg_match("/eval\(\)'d code$/", $file) || !isset($this->_coverage[$file])) {
291
            return false;
292
        }
293
        return true;
294
    }
295
296
    /**
297
     * Gets the real path in the original src directory.
298
     *
299
     * @param  string $file A file path or cached file path.
300
     * @return string       The original file path.
301
     */
302
    public function realpath($file)
303
    {
304
        $prefix = preg_quote($this->_prefix, '~');
305
        $file = preg_replace("~^{$prefix}~", '', $file);
306
        if (!$this->_hasVolume) {
307
            return $file;
308
        }
309
        if (preg_match('~^[A-Z]+:~', $file)) {
310
            return $file;
311
        }
312
        $file = ltrim($file, DS);
0 ignored issues
show
Bug introduced by
The constant Kahlan\Reporter\Coverage\DS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
313
        $pos = strpos($file, DS);
314
        if ($pos !== false) {
315
            $file = substr_replace($file, ':' . DS, $pos, 1);
316
        }
317
        return $file;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $file also could return the type array which is incompatible with the documented return type string.
Loading history...
318
    }
319
320
    /**
321
     * Exports coverage data.
322
     *
323
     * @return array The coverage data.
324
     */
325
    public function export($file = null)
326
    {
327
        if ($file) {
328
            return isset($this->_coverage[$file]) ? $this->_coverage($file, $this->_coverage[$file]) : [];
329
        }
330
        $result = [];
331
        $base = preg_quote($this->base(), '~');
332
        foreach ($this->_coverage as $file => $rawCoverage) {
333
            if ($coverage = $this->_coverage($file, $rawCoverage)) {
334
                $result[preg_replace("~^{$base}~", '', $file)] = $coverage;
335
            }
336
        }
337
        return $result;
338
    }
339
340
    /**
341
     * Gets the collected metrics from coverage data.
342
     *
343
     * @return Metrics The collected metrics.
344
     */
345
    public function metrics()
346
    {
347
        $this->_metrics = new Metrics();
0 ignored issues
show
Documentation Bug introduced by
It seems like new Kahlan\Reporter\Coverage\Metrics() of type Kahlan\Reporter\Coverage\Metrics is incompatible with the declared type array of property $_metrics.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
348
        foreach ($this->_coverage as $file => $rawCoverage) {
349
            $root = $this->parse($file);
350
            $coverage = $this->export($file);
351
            $this->_processed = [
352
                'loc'      => -1,
353
                'nlloc'    => -1,
354
                'lloc'     => -1,
355
                'cloc'     => -1,
356
                'coverage' => -1
357
            ];
358
            $this->_processTree($file, $root->tree, $coverage);
359
        }
360
        return $this->_metrics;
361
    }
362
363
    /**
364
     * Helper for `Collector::metrics()`.
365
     *
366
     * @param  string  $file     The processed file.
367
     * @param  object  $nodes    The nodes to collect metrics on.
368
     * @param  array   $coverage The coverage data.
369
     * @param  string  $path     The naming of the processed node.
370
     */
371
    protected function _processTree($file, $nodes, $coverage, $path = '')
372
    {
373
        foreach ($nodes as $node) {
374
            $this->_processNode($file, $node, $coverage, $path);
375
        }
376
    }
377
378
    /**
379
     * Helper for `Collector::metrics()`.
380
     *
381
     * @param  string  $file     The processed file.
382
     * @param  object  $node     The node to collect metrics on.
383
     * @param  array   $coverage The coverage data.
384
     * @param  string  $path     The naming of the processed node.
385
     */
386
    protected function _processNode($file, $node, $coverage, $path)
387
    {
388
        if ($node->type === 'namespace') {
389
            $path = "{$path}" . $node->name . '\\';
390
            $this->_processTree($file, $node->tree, $coverage, $path);
391
        } elseif ($node->hasMethods) {
392
            if ($node->type === 'interface') {
393
                return;
394
            }
395
            $path = "{$path}" . $node->name;
396
            $this->_processTree($file, $node->tree, $coverage, $path);
397
        } elseif ($node->type === 'function') {
398
            $prefix = $node->isMethod ? "{$path}::" : "{$path}";
399
            $path = $prefix . $node->name . '()';
400
        } else {
401
            $this->_processTree($file, $node->tree, $coverage, '');
402
        }
403
        $metrics = $this->_processMetrics($file, $node, $coverage);
404
        $this->_metrics->add($path, $metrics);
405
    }
406
407
    /**
408
     * Helper for `Collector::metrics()`.
409
     *
410
     * @param  string  $file     The processed file.
411
     * @param  object  $node     The node to collect metrics on.
412
     * @param  array   $coverage The coverage data.
413
     * @return array             The collected metrics.
414
     */
415
    protected function _processMetrics($file, $node, $coverage)
416
    {
417
        $metrics = [
418
            'loc'      => 0,
419
            'nlloc'    => 0,
420
            'lloc'     => 0,
421
            'cloc'     => 0,
422
            'coverage' => 0
423
        ];
424
        if (!$coverage) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $coverage of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
425
            return $metrics;
426
        }
427
        for ($index = $node->lines['start']; $index <= $node->lines['stop']; $index++) {
428
            $metrics['loc'] = $this->_lineMetric('loc', $index, $metrics['loc']);
429
            if (!isset($coverage[$index])) {
430
                $metrics['nlloc'] = $this->_lineMetric('nlloc', $index, $metrics['nlloc']);
431
                continue;
432
            }
433
            $metrics['lloc'] = $this->_lineMetric('lloc', $index, $metrics['lloc']);
434
            if ($coverage[$index]) {
435
                $metrics['cloc'] = $this->_lineMetric('cloc', $index, $metrics['cloc']);
436
                $metrics['coverage'] = $this->_lineMetric('coverage', $index, $metrics['coverage'], $coverage[$index]);
437
            }
438
        }
439
        $metrics['files'][$file] = $file;
440
        return $this->_methodMetrics($node, $metrics);
441
    }
442
443
    /**
444
     * Helper for `Collector::metrics()`.
445
     *
446
     * @param  string  $type      The metric type.
447
     * @param  integer $index     The line index.
448
     * @param  integer $value     The value to update.
449
     * @param  integer $increment The increment to perform if the line has not already been processed.
450
     * @return integer            The metric value.
451
     */
452
    protected function _lineMetric($type, $index, $value, $increment = 1)
453
    {
454
        if ($this->_processed[$type] >= $index) {
455
            return $value;
456
        }
457
        $this->_processed[$type] = $index;
458
        $value += $increment;
459
        return $value;
460
    }
461
462
    /**
463
     * Helper for `Collector::metrics()`.
464
     *
465
     * @param  object  $node    The node to collect metrics on.
466
     * @param  array   $metrics The metrics of the node.
467
     * @return array            The updated metrics.
468
     */
469
    protected function _methodMetrics($node, $metrics)
470
    {
471
        if ($node->type !== 'function' || $node->isClosure) {
472
            return $metrics;
473
        }
474
        $metrics['methods'] = 1;
475
        if ($metrics['cloc']) {
476
            $metrics['cmethods'] = 1;
477
        }
478
479
        $metrics['line']['start'] = $node->lines['start'];
480
        $metrics['line']['stop'] = $node->lines['stop'];
481
        return $metrics;
482
    }
483
484
    /**
485
     * Retruns & cache the tree structure of a file.
486
     *
487
     * @param string $file the file path to use for building the tree structure.
488
     */
489
    public function parse($file)
490
    {
491
        if (isset($this->_tree[$file])) {
492
            return $this->_tree[$file];
493
        }
494
        $parser = $this->_classes['parser'];
495
        return $this->_tree[$file] = $parser::parse(file_get_contents($file), ['lines' => true]);
496
    }
497
}
498