Parser::parseVersion()   B
last analyzed

Complexity

Conditions 7
Paths 7

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 7.0601

Importance

Changes 0
Metric Value
cc 7
nc 7
nop 0
dl 0
loc 50
ccs 25
cts 28
cp 0.8929
crap 7.0601
rs 8.1575
c 0
b 0
f 0
1
<?php
2
/* (c) Anton Medvedev <[email protected]>
3
 *
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 */
7
8
namespace Deployer\Support\Changelog;
9
10
class Parser
11
{
12
    /**
13
     * @var string[]
14
     */
15
    private $tokens;
16
17
    /**
18
     * @var string[]
19
     */
20
    private $span;
21
22
    /**
23
     * @var int
24
     */
25
    private $lineNumber = 0;
26
27
    /**
28
     * @var bool
29
     */
30
    private $strict;
31
32 2
    public function __construct(string $changelog, bool $strict = true)
33
    {
34 2
        $this->tokens = array_map('trim', explode("\n", $changelog));
35 2
        $this->strict = $strict;
36 2
    }
37
38 2
    private function current(): string
39
    {
40 2
        if (count($this->tokens) === 0) {
41 1
            return '';
42
        }
43 2
        return $this->tokens[0];
44
    }
45
46 2
    private function next(): string
47
    {
48 2
        if (count($this->tokens) === 0) {
49
            throw $this->error('Unexpected end of file');
50
        }
51
52 2
        $n = ++$this->lineNumber;
53 2
        $line = array_shift($this->tokens);
54
55 2
        $this->span[] = "    {$n}: $line";
56
57 2
        if (count($this->span) > 4) {
58 2
            array_shift($this->span);
59
        }
60
61 2
        return $line;
62
    }
63
64 2
    private function acceptEmptyLine()
65
    {
66 2
        if ($this->strict) {
67 1
            if ('' !== $this->next()) {
68 1
                throw $this->error('Expected an empty line');
69
            }
70
        } else {
71 1
            while (preg_match('/^\s*$/', $this->current()) && count($this->tokens) > 0) {
72 1
                $this->next();
73
            }
74
        }
75 2
    }
76
77 2
    private function acceptEof()
78
    {
79 2
        if (count($this->tokens) !== 0) {
80
            $this->next();
81
            throw $this->error('Expected EOF');
82
        }
83 2
    }
84
85 2
    private function matchVersion($line, &$m = null)
86
    {
87 2
        return preg_match('/^\#\# \s ( v\d+\.\d+\.\d+(-[\w\.]+)? | master )$/x', $line, $m);
88
    }
89
90
    private function error($message): ParseException
91
    {
92
        $c = count($this->span) - 1;
93
        $this->span[$c] = preg_replace('/^\s{4}/', ' -> ', $this->span[$c]);
94
95
        if (count($this->tokens) > 0) {
96
            $this->next();
97
        }
98
99
        return new ParseException($message, implode("\n", $this->span));
100
    }
101
102 2
    public function parse(): Changelog
103
    {
104 2
        $changelog = $this->parseTitle();
105
106 2
        $this->acceptEmptyLine();
107 2
        $this->acceptEmptyLine();
108
109 2
        while ($this->matchVersion($this->current())) {
110 2
            $version = $this->parseVersion();
111 2
            $changelog->addVersion($version);
112
        }
113
114 2
        $refs = $this->parseReferences();
115 2
        $changelog->setReferences($refs);
116
117 2
        $this->acceptEmptyLine();
118 2
        $this->acceptEof();
119
120 2
        return $changelog;
121
    }
122
123 2
    private function parseTitle(): Changelog
124
    {
125 2
        if (preg_match('/# (.+)/', $this->next(), $m)) {
126 2
            $c = new Changelog();
127 2
            $c->setTitle($m[1]);
128 2
            return $c;
129
        }
130
131
        throw $this->error('Expected title');
132
    }
133
134 2
    private function parseVersion(): Version
135
    {
136 2
        if ($this->matchVersion($this->next(), $m)) {
137 2
            $version = new Version();
138 2
            $version->setVersion($curr = $m[1]);
139
140 2
            $compareLink = $this->next();
141 2
            if (!preg_match('/^\[/', $compareLink)) {
142
                throw $this->error('Expected link to compare page with previous version');
143
            }
144
145 2
            $prev = 'v\d+\.\d+\.\d+(-[\d\w\.]+)?';
146
            $regexp = "/
147 2
                ^ \[($prev)\.\.\.$curr\]
148 2
                \(https\:\/\/github\.com\/deployphp\/deployer\/compare\/$prev\.\.\.$curr\) $
149 2
                /x";
150
151 2
            if (preg_match($regexp, $compareLink, $m)) {
152 2
                $version->setPrevious($m[1]);
153
            } else {
154
                throw $this->error('Error in compare link syntax');
155
            }
156
157 2
            $this->acceptEmptyLine();
158
159 2
            $sections = ['Added', 'Changed', 'Fixed', 'Removed'];
160 2
            $sectionsCount = count($sections);
161
162 2
            for ($i = 0; $i < $sectionsCount; $i++) {
163 2
                foreach ($sections as $key => $section) {
164 2
                    if (preg_match('/^\#\#\# \s ' . $section . ' $/x', $this->current())) {
165 2
                        $this->next();
166
167 2
                        $version->{"set$section"}($this->parseItems());
168 2
                        unset($sections[$key]);
169
170 2
                        $this->acceptEmptyLine();
171
172 2
                        break;
173
                    }
174
                }
175
            }
176
177 2
            $this->acceptEmptyLine();
178
179 2
            return $version;
180
        }
181
182
        throw $this->error('Expected version');
183
    }
184
185 2
    private function parseItems(): array
186
    {
187 2
        $items = [];
188 2
        while (preg_match('/^\- (.+) $/x', $this->current(), $m)) {
189 2
            $this->next();
190
191 2
            $item = new Item();
192 2
            $message = $m[1];
193 2
            $ref = '/\[ \#(\d+) \]/x';
194
195 2
            preg_match_all($ref, $message, $matches);
196 2
            foreach ($matches[1] as $m) {
197 2
                $item->addReference($m);
198
            }
199
200 2
            $message = trim(preg_replace($ref, '', $message));
201 2
            $item->setMessage($message);
202 2
            $items[] = $item;
203
        }
204 2
        return $items;
205
    }
206
207 2
    private function parseReferences(): array
208
    {
209 2
        $refs = [];
210 2
        while (preg_match('/^\[/', $this->current())) {
211 2
            if (preg_match('/^ \[\#(\d+)\]\: \s (https\:\/\/github\.com\/deployphp\/deployer\/(issues|pull)\/\d+)$/x', $this->next(), $m)) {
212 2
                $refs[$m[1]] = $m[2];
213
            } else {
214
                throw $this->error('Error parsing reference');
215
            }
216
        }
217 2
        return $refs;
218
    }
219
}
220