Completed
Push — master ( 9604dc...5404e5 )
by James
03:26
created

Processor::process()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 0
cts 13
cp 0
rs 8.439
c 0
b 0
f 0
cc 5
eloc 13
nc 4
nop 1
crap 30
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Copyright (c) 1998-2017 Browser Capabilities Project
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License as
10
 * published by the Free Software Foundation, either version 3 of the
11
 * License, or (at your option) any later version.
12
 *
13
 * Refer to the LICENSE file distributed with this package.
14
 *
15
 * @category   Browscap
16
 * @copyright  1998-2017 Browser Capabilities Project
17
 * @license    MIT
18
 */
19
20
namespace Browscap\Coverage;
21
22
use Seld\JsonLint\Lexer;
23
24
/**
25
 * Class Processor
26
 *
27
 * @category   Browscap
28
 * @author     Jay Klehr <[email protected]>
29
 */
30
final class Processor implements ProcessorInterface
31
{
32
    /**@+
33
     * The codes representing different JSON elements
34
     *
35
     * These come from the Seld\JsonLint\JsonParser class. The values are returned by the lexer when
36
     * the lex() method is called.
37
     *
38
     * @var int
39
     */
40
    const JSON_OBJECT_START = 17;
41
    const JSON_OBJECT_END   = 18;
42
    const JSON_ARRAY_START  = 23;
43
    const JSON_ARRAY_END    = 24;
44
    const JSON_EOF          = 1;
45
    const JSON_STRING       = 4;
46
    const JSON_COLON        = 21;
47
    /**@-*/
48
49
    /**
50
     * @var string
51
     */
52
    private $resourceDir;
53
54
    /**
55
     * The pattern ids encountered during the test run. These are compared against the JSON file structure to determine
56
     * if the statement/function/branch is covered.
57
     *
58
     * @var string[]
59
     */
60
    private $coveredIds = [];
61
62
    /**
63
     * This is the full coverage array that gets output in the write method.  For each file an entry in the array
64
     * is added.  Each entry contains the elements required for Istanbul compatible coverage reporters.
65
     *
66
     * @var array
67
     */
68
    private $coverage = [];
69
70
    /**
71
     * An incrementing integer for every "function" (child match) encountered in all processed files. This is used
72
     * to name the anonymous functions in the coverage report.
73
     *
74
     * @var int
75
     */
76
    private $funcCount = 0;
77
78
    /**
79
     * A storage variable for the lines of a file while processing that file, used for determining column
80
     * position of a statement/function/branch
81
     *
82
     * @var string[]
83
     */
84
    private $fileLines = [];
85
86
    /**
87
     * A storage variable of the pattern ids covered by tests for a specific file (set when processing of that
88
     * file begins)
89
     *
90
     * @var string[]
91
     */
92
    private $fileCoveredIds = [];
93
94
    /**
95
     * A temporary storage for coverage information for a specific file that is later merged into the main $coverage
96
     * property after the file is done processing.
97
     *
98
     * @var array
99
     */
100
    private $fileCoverage = [];
101
102
    /**
103
     * Create a new Coverage Processor for the specified directory
104
     *
105
     * @param string $resourceDir
106
     */
107 10
    public function __construct(string $resourceDir)
108
    {
109 10
        $this->resourceDir = $resourceDir;
110 10
    }
111
112
    /**
113
     * Process the directory of JSON files using the collected pattern ids
114
     *
115
     * @param string[] $coveredIds
116
     *
117
     * @return void
118
     */
119
    public function process(array $coveredIds)
120
    {
121
        $this->setCoveredPatternIds($coveredIds);
122
123
        $iterator = new \RecursiveDirectoryIterator($this->resourceDir);
124
125
        foreach (new \RecursiveIteratorIterator($iterator) as $file) {
126
            /** @var $file \SplFileInfo */
127
            if (!$file->isFile() || $file->getExtension() !== 'json') {
128
                continue;
129
            }
130
131
            $patternFileName = substr($file->getPathname(), strpos($file->getPathname(), 'resources/'));
132
133
            if (!isset($this->coveredIds[$patternFileName])) {
134
                $this->coveredIds[$patternFileName] = [];
135
            }
136
137
            $this->coverage[$patternFileName] = $this->processFile(
138
                $patternFileName,
139
                file_get_contents($file->getPathname()),
140
                $this->coveredIds[$patternFileName]
0 ignored issues
show
Documentation introduced by
$this->coveredIds[$patternFileName] is of type string|array, but the function expects a array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
141
            );
142
        }
143
    }
144
145
    /**
146
     * Write the coverage data in JSON format to specified filename
147
     *
148
     * @param string $fileName
149
     *
150
     * @return void
151
     */
152
    public function write(string $fileName)
153
    {
154
        file_put_contents(
155
            $fileName,
156
            // str_replace here is to convert empty arrays into empty JS objects, which is expected by
157
            // codecov.io. Owner of the service said he was going to patch it, haven't tested since
158
            // Note: Can't use JSON_FORCE_OBJECT here as we **do** want arrays for the 'b' structure
159
            // which FORCE_OBJECT turns into objects, breaking at least the Istanbul coverage reporter
160
            str_replace('[]', '{}', json_encode($this->coverage, JSON_UNESCAPED_SLASHES))
161
        );
162
    }
163
164
    /**
165
     * Stores passed in pattern ids, grouping them by file first
166
     *
167
     * @param string[] $coveredIds
168
     *
169
     * @return void
170
     */
171 1
    public function setCoveredPatternIds(array $coveredIds)
172
    {
173 1
        $this->coveredIds = $this->groupIdsByFile($coveredIds);
174 1
    }
175
176
    /**
177
     * Returns the grouped pattern ids previously set
178
     *
179
     * @return array
180
     */
181 1
    public function getCoveredPatternIds() : array
182
    {
183 1
        return $this->coveredIds;
184
    }
185
186
    /**
187
     * Process an individual file for coverage data using covered ids
188
     *
189
     * @param string   $file
190
     * @param string   $contents
191
     * @param string[] $coveredIds
192
     *
193
     * @return array
194
     */
195 9
    public function processFile(string $file, string $contents, array $coveredIds) : array
196
    {
197
        // These keynames are expected by Istanbul compatible coverage reporters
198
        // the format is outlined here: https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md
199 9
        $this->fileCoverage = [
200
            // path to file this coverage information is for (i.e. resources/user-agents/browsers/chrome/chrome.json)
201 9
            'path' => $file,
202
            // location information for the statements that should be covered
203
            'statementMap' => [],
204
            // location information for the functions that should be covered
205
            'fnMap' => [],
206
            // location information for the different branch structures that should be covered
207
            'branchMap' => [],
208
            // coverage counts for the statements from statementMap
209
            's' => [],
210
            // coverage counts for the branches from branchMap (in array form)
211
            'b' => [],
212
            // coverage counts for the functions from fnMap
213
            'f' => [],
214
        ];
215
216 9
        $this->fileLines      = explode("\n", $contents);
217 9
        $this->fileCoveredIds = $coveredIds;
218
219 9
        $lexer = new Lexer();
220 9
        $lexer->setInput($contents);
221
222 9
        $this->handleJsonRoot($lexer);
223
224
        // This re-indexes the arrays to be 1 based instead of 0, which will make them be JSON objects rather
225
        // than arrays, which is how they're expected to be in the coverage JSON file
226 9
        $this->fileCoverage['fnMap']        = array_filter(array_merge([0], $this->fileCoverage['fnMap']));
227 9
        $this->fileCoverage['statementMap'] = array_filter(array_merge([0], $this->fileCoverage['statementMap']));
228 9
        $this->fileCoverage['branchMap']    = array_filter(array_merge([0], $this->fileCoverage['branchMap']));
229
230
        // Can't use the same method for the b/s/f sections since they can (and should) contain a 0 value, which
231
        // array_filter would remove
232 9
        array_unshift($this->fileCoverage['b'], '');
233 9
        unset($this->fileCoverage['b'][0]);
234 9
        array_unshift($this->fileCoverage['f'], '');
235 9
        unset($this->fileCoverage['f'][0]);
236 9
        array_unshift($this->fileCoverage['s'], '');
237 9
        unset($this->fileCoverage['s'][0]);
238
239 9
        return $this->fileCoverage;
240
    }
241
242
    /**
243
     * Builds the location object for the current position in the JSON file
244
     *
245
     * @param Lexer  $lexer
246
     * @param bool   $end
247
     * @param string $content
248
     *
249
     * @return array
250
     */
251 9
    private function getLocationCoordinates(Lexer $lexer, bool $end = false, string $content = '') : array
252
    {
253 9
        $lineNumber  = $lexer->yylineno;
254 9
        $lineContent = $this->fileLines[$lineNumber];
255
256 9
        if ($content === '') {
257 9
            $content = $lexer->yytext;
258
        }
259
260 9
        $position = strpos($lineContent, $content);
261
262 9
        if ($end === true) {
263 9
            $position += strlen($content);
264
        }
265
266 9
        return ['line' => $lineNumber + 1, 'column' => $position];
267
    }
268
269
    /**
270
     * JSON file processing entry point
271
     *
272
     * Hands execution off to applicable method when certain tokens are encountered
273
     * (in this case, Division is the only one), returns to caller when EOF is reached
274
     *
275
     * @param Lexer $lexer
276
     *
277
     * @return void
278
     */
279 9
    private function handleJsonRoot(Lexer $lexer)
280
    {
281
        do {
282 9
            $code = $lexer->lex();
283
284 9
            if ($code === self::JSON_OBJECT_START) {
285 9
                $code = $this->handleJsonDivision($lexer);
286
            }
287 9
        } while ($code !== self::JSON_EOF);
288 9
    }
289
290
    /**
291
     * Processes the Division block (which is the root JSON object essentially)
292
     *
293
     * Lexes the main division object and hands execution off to relevant methods for further
294
     * processing. Returns the next token code returned from the lexer.
295
     *
296
     * @param Lexer $lexer
297
     *
298
     * @return int
299
     */
300 9 View Code Duplication
    private function handleJsonDivision(Lexer $lexer) : int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
301
    {
302 9
        $enterUaGroup = false;
303
304
        do {
305 9
            $code = $lexer->lex();
306
307 9
            if ($code === self::JSON_STRING && $lexer->yytext === 'userAgents') {
308 9
                $enterUaGroup = true;
309 9
            } elseif ($code === self::JSON_ARRAY_START && $enterUaGroup === true) {
310 9
                $code         = $this->handleUseragentGroup($lexer);
311 9
                $enterUaGroup = false;
312 9
            } elseif ($code === self::JSON_OBJECT_START) {
313
                $code = $this->ignoreObjectBlock($lexer);
314
            }
315 9
        } while ($code !== self::JSON_OBJECT_END);
316
317 9
        return $lexer->lex();
318
    }
319
320
    /**
321
     * Processes the userAgents array
322
     *
323
     * @param Lexer $lexer
324
     *
325
     * @return int
326
     */
327 9 View Code Duplication
    private function handleUseragentGroup(Lexer $lexer) : int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
328
    {
329 9
        $useragentPosition = 0;
330
331
        do {
332 9
            $code = $lexer->lex();
333
334 9
            if ($code === self::JSON_OBJECT_START) {
335 9
                $code = $this->handleUseragentBlock($lexer, $useragentPosition);
336 9
                ++$useragentPosition;
337
            }
338 9
        } while ($code !== self::JSON_ARRAY_END);
339
340 9
        return $lexer->lex();
341
    }
342
343
    /**
344
     * Processes each userAgent object in the userAgents array
345
     *
346
     * @param Lexer $lexer
347
     * @param int   $useragentPosition
348
     *
349
     * @return int
350
     */
351 9 View Code Duplication
    private function handleUseragentBlock(Lexer $lexer, int $useragentPosition) : int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
352
    {
353 9
        $enterChildGroup = false;
354
355
        do {
356 9
            $code = $lexer->lex();
357
358 9
            if ($code === self::JSON_STRING && $lexer->yytext === 'children') {
359 9
                $enterChildGroup = true;
360 9
            } elseif ($code === self::JSON_ARRAY_START && $enterChildGroup === true) {
361 9
                $code            = $this->handleChildrenGroup($lexer, $useragentPosition);
362 9
                $enterChildGroup = false;
363 9
            } elseif ($code === self::JSON_OBJECT_START) {
364 9
                $code = $this->ignoreObjectBlock($lexer);
365
            }
366 9
        } while ($code !== self::JSON_OBJECT_END);
367
368 9
        return $lexer->lex();
369
    }
370
371
    /**
372
     * Processes the children array of a userAgent object
373
     *
374
     * @param Lexer $lexer
375
     * @param int   $useragentPosition
376
     *
377
     * @return int
378
     */
379 9 View Code Duplication
    private function handleChildrenGroup(Lexer $lexer, int $useragentPosition) : int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
380
    {
381 9
        $childPosition = 0;
382
383
        do {
384 9
            $code = $lexer->lex();
385
386 9
            if ($code === self::JSON_OBJECT_START) {
387 9
                $code = $this->handleChildBlock($lexer, $useragentPosition, $childPosition);
388 9
                ++$childPosition;
389
            }
390 9
        } while ($code !== self::JSON_ARRAY_END);
391
392 9
        return $lexer->lex();
393
    }
394
395
    /**
396
     * Processes each child object in the children array
397
     *
398
     * @param Lexer $lexer
399
     * @param int   $useragentPosition
400
     * @param int   $childPosition
401
     *
402
     * @return int
403
     */
404 9
    private function handleChildBlock(Lexer $lexer, int $useragentPosition, int $childPosition) : int
405
    {
406 9
        $enterPlatforms = false;
407 9
        $enterDevices   = false;
408 9
        $collectMatch   = false;
409
410 9
        $functionStart       = $this->getLocationCoordinates($lexer);
411 9
        $functionDeclaration = [];
412
413
        do {
414 9
            $code = $lexer->lex();
415
416
            switch ($code) {
417 9
                case self::JSON_STRING:
418 9
                    if ($lexer->yytext === 'platforms') {
419 9
                        $enterPlatforms = true;
420 9
                    } elseif ($lexer->yytext === 'devices') {
421 4
                        $enterDevices = true;
422 9
                    } elseif ($lexer->yytext === 'match') {
423 9
                        $collectMatch = true;
424 9
                    } elseif ($collectMatch === true) {
425 9
                        $collectMatch        = false;
426 9
                        $match               = $lexer->yytext;
427
                        $functionDeclaration = [
428 9
                            'start' => $this->getLocationCoordinates($lexer, false, '"' . $match . '"'),
429 9
                            'end' => $this->getLocationCoordinates($lexer, true, '"' . $match . '"'),
430
                        ];
431
                    }
432 9
                    break;
433 9
                case self::JSON_OBJECT_START:
434 4
                    if ($enterDevices === true) {
435 4
                        $code         = $this->handleDeviceBlock($lexer, $useragentPosition, $childPosition);
436 4
                        $enterDevices = false;
437
                    } else {
438 4
                        $code = $this->ignoreObjectBlock($lexer);
439
                    }
440 4
                    break;
441 9
                case self::JSON_ARRAY_START:
442 9
                    if ($enterPlatforms === true) {
443 9
                        $code           = $this->handlePlatformBlock($lexer, $useragentPosition, $childPosition);
444 9
                        $enterPlatforms = false;
445
                    }
446 9
                    break;
447
            }
448 9
        } while ($code !== self::JSON_OBJECT_END);
449
450 9
        $functionEnd = $this->getLocationCoordinates($lexer, true);
451
452 9
        $functionCoverage = $this->getCoverageCount(
453 9
            sprintf('u%d::c%d::d::p', $useragentPosition, $childPosition),
454 9
            $this->fileCoveredIds
455
        );
456
457 9
        $this->collectFunction($functionStart, $functionEnd, $functionDeclaration, $functionCoverage);
458
459 9
        return $lexer->lex();
460
    }
461
462
    /**
463
     * Process the "devices" has in the child object
464
     *
465
     * @param Lexer $lexer
466
     * @param int   $useragentPosition
467
     * @param int   $childPosition
468
     *
469
     * @return int
470
     */
471 4
    private function handleDeviceBlock(Lexer $lexer, int $useragentPosition, int $childPosition) : int
472
    {
473 4
        $capturedKey = false;
474 4
        $sawColon    = false;
475
476 4
        $branchStart     = $this->getLocationCoordinates($lexer);
477 4
        $branchLocations = [];
478 4
        $branchCoverage  = [];
479
480
        do {
481 4
            $code = $lexer->lex();
482
483 4
            if ($code === self::JSON_STRING && $capturedKey === false) {
484 4
                $branchLocations[] = [
485 4
                    'start' => $this->getLocationCoordinates($lexer, false, '"' . $lexer->yytext . '"'),
486 4
                    'end' => $this->getLocationCoordinates($lexer, true, '"' . $lexer->yytext . '"'),
487
                ];
488 4
                $branchCoverage[] = $this->getCoverageCount(
489 4
                    sprintf('u%d::c%d::d%s::p', $useragentPosition, $childPosition, $lexer->yytext),
490 4
                    $this->fileCoveredIds
491
                );
492 4
                $capturedKey = true;
493 4
            } elseif ($code === self::JSON_COLON) {
494 4
                $sawColon = true;
495 4
            } elseif ($code === self::JSON_STRING && $sawColon === true) {
496 4
                $capturedKey = false;
497
            }
498 4
        } while ($code !== self::JSON_OBJECT_END);
499
500 4
        $branchEnd = $this->getLocationCoordinates($lexer, true);
501
502 4
        $this->collectBranch($branchStart, $branchEnd, $branchLocations, $branchCoverage);
503
504 4
        return $lexer->lex();
505
    }
506
507
    /**
508
     * Processes the "platforms" hash in the child object
509
     *
510
     * @param Lexer $lexer
511
     * @param int   $useragentPosition
512
     * @param int   $childPosition
513
     *
514
     * @return int
515
     */
516 9
    private function handlePlatformBlock(Lexer $lexer, int $useragentPosition, int $childPosition) : int
517
    {
518 9
        $branchStart     = $this->getLocationCoordinates($lexer);
519 9
        $branchLocations = [];
520 9
        $branchCoverage  = [];
521
522
        do {
523 9
            $code = $lexer->lex();
524
525 9
            if ($code === self::JSON_STRING) {
526 9
                $branchLocations[] = [
527 9
                    'start' => $this->getLocationCoordinates($lexer, false, '"' . $lexer->yytext . '"'),
528 9
                    'end' => $this->getLocationCoordinates($lexer, true, '"' . $lexer->yytext . '"'),
529
                ];
530 9
                $branchCoverage[] = $this->getCoverageCount(
531 9
                    sprintf('u%d::c%d::d::p%s', $useragentPosition, $childPosition, $lexer->yytext),
532 9
                    $this->fileCoveredIds
533
                );
534
            }
535 9
        } while ($code !== self::JSON_ARRAY_END);
536
537 9
        $branchEnd = $this->getLocationCoordinates($lexer, true);
538
539 9
        $this->collectBranch($branchStart, $branchEnd, $branchLocations, $branchCoverage);
540
541 9
        return $lexer->lex();
542
    }
543
544
    /**
545
     * Processes JSON object block that isn't needed for coverage data
546
     *
547
     * @param Lexer $lexer
548
     *
549
     * @return int
550
     */
551 9
    private function ignoreObjectBlock(Lexer $lexer) : int
552
    {
553
        do {
554 9
            $code = $lexer->lex();
555
556
            // recursively ignore nested objects
557 9
            if ($code === self::JSON_OBJECT_START) {
558
                $this->ignoreObjectBlock($lexer);
559
            }
560 9
        } while ($code !== self::JSON_OBJECT_END);
561
562 9
        return $lexer->lex();
563
    }
564
565
    /**
566
     * Collects and stores a function's location information as well as any passed in coverage counts
567
     *
568
     * @param array $start
569
     * @param array $end
570
     * @param array $declaration
571
     * @param int   $coverage
572
     *
573
     * @return void
574
     */
575 9
    private function collectFunction(array $start, array $end, array $declaration, int $coverage = 0)
576
    {
577 9
        $this->fileCoverage['fnMap'][] = [
578 9
            'name' => '(anonymous_' . $this->funcCount . ')',
579 9
            'decl' => $declaration,
580 9
            'loc' => ['start' => $start, 'end' => $end],
581
        ];
582
583 9
        $this->fileCoverage['f'][] = $coverage;
584
585
        // Collect statements as well, one for entire function, one just for function declaration
586 9
        $this->collectStatement($start, $end, $coverage);
587 9
        $this->collectStatement($declaration['start'], $declaration['end'], $coverage);
588
589 9
        ++$this->funcCount;
590 9
    }
591
592
    /**
593
     * Collects and stores a branch's location information as well as any coverage counts
594
     *
595
     * @param array $start
596
     * @param array $end
597
     * @param array $locations
598
     * @param int[] $coverage
599
     *
600
     * @return void
601
     */
602 9
    private function collectBranch(array $start, array $end, array $locations, array $coverage = [])
603
    {
604 9
        $this->fileCoverage['branchMap'][] = [
605 9
            'type' => 'switch',
606 9
            'locations' => $locations,
607 9
            'loc' => ['start' => $start, 'end' => $end],
608
        ];
609
610 9
        $this->fileCoverage['b'][] = $coverage;
611
612
        // Collect statements as well (entire branch is a statement, each location is a statement)
613 9
        $this->collectStatement($start, $end, array_sum($coverage));
614
615 9
        for ($i = 0, $count = count($locations); $i < $count; ++$i) {
616 9
            $this->collectStatement($locations[$i]['start'], $locations[$i]['end'], $coverage[$i]);
617
        }
618 9
    }
619
620
    /**
621
     * Collects and stores a statement's location information as well as any coverage counts
622
     *
623
     * @param array $start
624
     * @param array $end
625
     * @param int   $coverage
626
     *
627
     * @return void
628
     */
629 9
    private function collectStatement(array $start, array $end, int $coverage = 0)
630
    {
631 9
        $this->fileCoverage['statementMap'][] = [
632 9
            'start' => $start,
633 9
            'end' => $end,
634
        ];
635
636 9
        $this->fileCoverage['s'][] = $coverage;
637 9
    }
638
639
    /**
640
     * Groups pattern ids by their filename prefix
641
     *
642
     * @param string[] $ids
643
     *
644
     * @return array
645
     */
646 1
    private function groupIdsByFile(array $ids) : array
647
    {
648 1
        $covered = [];
649
650 1
        foreach ($ids as $id) {
651 1
            $file = substr($id, 0, strpos($id, '::'));
652
653 1
            if (!isset($covered[$file])) {
654 1
                $covered[$file] = [];
655
            }
656
657 1
            $covered[$file][] = substr($id, strpos($id, '::') + 2);
658
        }
659
660 1
        return $covered;
661
    }
662
663
    /**
664
     * Counts number of times given generated pattern id is covered by patterns ids collected during tests
665
     *
666
     * @param string   $id
667
     * @param string[] $covered
668
     *
669
     * @return int
670
     */
671 9
    private function getCoverageCount(string $id, array $covered) : int
672
    {
673 9
        $id                  = str_replace('\/', '/', $id);
674 9
        list($u, $c, $d, $p) = explode('::', $id);
675
676 9
        $u = preg_quote(substr($u, 1), '/');
677 9
        $c = preg_quote(substr($c, 1), '/');
678 9
        $p = preg_quote(substr($p, 1), '/');
679 9
        $d = preg_quote(substr($d, 1), '/');
680
681 9
        $count = 0;
682
683 9
        if (strlen($p) === 0) {
684 9
            $p = '.*?';
685
        }
686 9
        if (strlen($d) === 0) {
687 9
            $d = '.*?';
688
        }
689
690 9
        $regex = sprintf('/^u%d::c%d::d%s::p%s$/', $u, $c, $d, $p);
691
692 9
        foreach ($covered as $patternId) {
693 5
            if (preg_match($regex, $patternId)) {
694 5
                ++$count;
695
            }
696
        }
697
698 9
        return $count;
699
    }
700
}
701