PhpStan::trimLines()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
namespace exussum12\CoverageChecker\Loaders;
3
4
use Exception;
5
use exussum12\CoverageChecker\FileChecker;
6
use ReflectionFunction;
7
use ReflectionFunctionAbstract;
8
use ReflectionMethod;
9
10
/**
11
 * Class PhpStan
12
 * Used for parsing phpstan standard output
13
 * @package exussum12\CoverageChecker
14
 */
15
class PhpStan implements FileChecker
16
{
17
    protected $lineRegex = '/^\s+(?<lineNumber>[0-9]+)/';
18
19
    protected $file;
20
    protected $relatedRegex = '#(function|method) (?:(?P<class>.*?)::)?(?P<function>.*?)[ \(]#';
21
22
    /**
23
     * @var array
24
     */
25
    protected $invalidLines = [];
26
27
    /**
28
     * @param string $filename the path to the phpstan.txt file
29
     */
30
    public function __construct($filename)
31
    {
32
        $this->file = fopen($filename, 'r');
33
    }
34
35
    /**
36
     * {@inheritdoc}
37
     */
38
    public function parseLines(): array
39
    {
40
        $filename = '';
41
        $lineNumber = 0;
42
        while (($line = fgets($this->file)) !== false) {
0 ignored issues
show
Bug introduced by
It seems like $this->file can also be of type boolean; however, parameter $handle of fgets() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

42
        while (($line = fgets(/** @scrutinizer ignore-type */ $this->file)) !== false) {
Loading history...
43
            $filename = $this->checkForFilename($line, $filename);
44
            if ($lineNumber = $this->getLineNumber($line, $lineNumber)) {
45
                $error = $this->getMessage($line);
46
                if ($this->isExtendedMessage($line)) {
47
                    $this->appendError($filename, $lineNumber, $error);
48
                    continue;
49
                }
50
                $this->handleRelatedError($filename, $lineNumber, $error);
51
                $this->addError($filename, $lineNumber, $error);
52
            }
53
        }
54
55
        $this->trimLines();
56
57
        return array_keys($this->invalidLines);
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function getErrorsOnLine(string $file, int $lineNumber)
64
    {
65
        $errors = [];
66
        if (isset($this->invalidLines[$file][$lineNumber])) {
67
            $errors = $this->invalidLines[$file][$lineNumber];
68
        }
69
70
        return $errors;
71
    }
72
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function handleNotFoundFile()
78
    {
79
        return true;
80
    }
81
82
    /**
83
     * @param string $line
84
     * @param string $currentFile
85
     * @return string the currentFileName
86
     */
87
    protected function checkForFilename($line, $currentFile)
88
    {
89
        if (strpos($line, " Line ")) {
90
            return trim(str_replace('Line', '', $line));
91
        }
92
        return $currentFile;
93
    }
94
95
    protected function getLineNumber(string $line, int $currentLineNumber)
96
    {
97
        $matches = [];
98
        if (!preg_match($this->lineRegex, $line, $matches)) {
99
            if (preg_match('#^\s{3,}#', $line)) {
100
                return $currentLineNumber;
101
            }
102
103
            return false;
104
        }
105
106
        return (int) $matches['lineNumber'];
107
    }
108
109
    protected function getMessage($line)
110
    {
111
        return trim(preg_replace($this->lineRegex, '', $line));
112
    }
113
114
    protected function isExtendedMessage($line)
115
    {
116
        return preg_match($this->lineRegex, $line) === 0;
117
    }
118
119
    protected function trimLines()
120
    {
121
        array_walk_recursive($this->invalidLines, function (&$item) {
122
            if (is_string($item)) {
123
                $item = trim($item);
124
            }
125
        });
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public static function getDescription(): string
132
    {
133
        return 'Parses the text output of phpstan';
134
    }
135
136
    protected function handleRelatedError($filename, $line, $error)
137
    {
138
139
        $matches = [];
140
        if (preg_match($this->relatedRegex, $error, $matches)) {
141
            $error = sprintf(
142
                '%s (used %s line %d)',
143
                $error,
144
                $filename,
145
                $line
146
            );
147
148
            try {
149
                $reflection = $this->getReflector($matches);
150
                $filename = $reflection->getFileName();
151
                $currentLine = $reflection->getStartLine();
152
153
                while ($currentLine < $reflection->getEndLine()) {
154
                    $this->addError($filename, $currentLine++, $error);
155
                }
156
            } catch (Exception $exception) {
157
                // can't find any more info about this method, so just carry on
158
            }
159
        }
160
    }
161
162
    /**
163
     * @param string $filename
164
     * @param int $lineNumber
165
     * @param string $error
166
     */
167
    protected function addError($filename, $lineNumber, $error)
168
    {
169
        if (!isset($this->invalidLines[$filename][$lineNumber])) {
170
            $this->invalidLines[$filename][$lineNumber] = [];
171
        }
172
        $this->invalidLines[$filename][$lineNumber][] = $error;
173
    }
174
175
    protected function getReflector(array $matches): ReflectionFunctionAbstract
176
    {
177
        if ($matches['class']) {
178
            return $this->getClassReflector($matches);
179
        }
180
181
        return $this->getFunctionReflector($matches);
182
    }
183
184
    private function appendError(string $filename, int $lineNumber, string $error)
185
    {
186
        end($this->invalidLines[$filename][$lineNumber]);
187
        $key = key($this->invalidLines[$filename][$lineNumber]);
188
        $this->invalidLines[$filename][$lineNumber][$key] .= ' ' . $error;
189
    }
190
191
    protected function getClassReflector(array $matches): ReflectionMethod
192
    {
193
        if (!method_exists($matches['class'], $matches['function'])) {
194
            throw new Exception("Missing class function");
195
        }
196
        return new ReflectionMethod(
197
            $matches['class'],
198
            $matches['function']
199
        );
200
    }
201
202
    protected function getFunctionReflector(array $matches): ReflectionFunction
203
    {
204
        if (!function_exists($matches['function'])) {
205
            throw new Exception("Missing function reflector");
206
        }
207
        return new ReflectionFunction(
208
            $matches['function']
209
        );
210
    }
211
}
212