Completed
Push — master ( 751a3d...b7ca33 )
by Sebastian
17:18 queued 15:56
created

FullDiffList::parseCodeLine()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 9.9
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * This file is part of SebastianFeldmann\Git.
4
 *
5
 * (c) Sebastian Feldmann <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace SebastianFeldmann\Git\Command\Diff\Compare;
11
12
use SebastianFeldmann\Cli\Command\OutputFormatter;
13
use SebastianFeldmann\Git\Diff\Change;
14
use SebastianFeldmann\Git\Diff\File;
15
use SebastianFeldmann\Git\Diff\Line;
16
17
/**
18
 * FullDiffList output formatter.
19
 *
20
 * Returns a list of SebastianFeldmann\Git\Diff\File objects. Each containing
21
 * the list of changes that happened in that file.
22
 *
23
 * @author  Sebastian Feldmann <[email protected]>
24
 * @link    https://github.com/sebastianfeldmann/git
25
 * @since   Class available since Release 1.2.0
26
 */
27
class FullDiffList implements OutputFormatter
28
{
29
    /**
30
     * Available line types of git diff output.
31
     */
32
    const LINE_TYPE_START      = 'Start';
33
    const LINE_TYPE_HEADER     = 'Header';
34
    const LINE_TYPE_SIMILARITY = 'HeaderSimilarity';
35
    const LINE_TYPE_OP         = 'HeaderOp';
36
    const LINE_TYPE_INDEX      = 'HeaderIndex';
37
    const LINE_TYPE_FORMAT     = 'HeaderFormat';
38
    const LINE_TYPE_POSITION   = 'ChangePosition';
39
    const LINE_TYPE_CODE       = 'ChangeCode';
40
41
    /**
42
     * Search and parse strategy.
43
     *
44
     * Define possible follow up lines for each line type to minimize search effort.
45
     *
46
     * @var array
47
     */
48
    private static $lineTypesToCheck = [
49
        self::LINE_TYPE_START => [
50
            self::LINE_TYPE_HEADER
51
        ],
52
        self::LINE_TYPE_HEADER => [
53
            self::LINE_TYPE_SIMILARITY,
54
            self::LINE_TYPE_OP,
55
            self::LINE_TYPE_INDEX,
56
        ],
57
        self::LINE_TYPE_SIMILARITY => [
58
            self::LINE_TYPE_OP,
59
            self::LINE_TYPE_INDEX,
60
        ],
61
        self::LINE_TYPE_OP => [
62
            self::LINE_TYPE_OP,
63
            self::LINE_TYPE_INDEX
64
        ],
65
        self::LINE_TYPE_INDEX => [
66
            self::LINE_TYPE_FORMAT
67
        ],
68
        self::LINE_TYPE_FORMAT => [
69
            self::LINE_TYPE_FORMAT,
70
            self::LINE_TYPE_POSITION
71
        ],
72
        self::LINE_TYPE_POSITION => [
73
            self::LINE_TYPE_CODE
74
        ],
75
        self::LINE_TYPE_CODE => [
76
            self::LINE_TYPE_HEADER,
77
            self::LINE_TYPE_POSITION,
78
            self::LINE_TYPE_CODE
79
        ]
80
    ];
81
82
    /**
83
     * Maps git diff output to file operations.
84
     *
85
     * @var array
86
     */
87
    private static $opsMap = [
88
        'old'     => File::OP_MODIFIED,
89
        'new'     => File::OP_CREATED,
90
        'deleted' => File::OP_DELETED,
91
        'rename'  => File::OP_RENAMED,
92
        'copy'    => File::OP_COPIED,
93
    ];
94
95
    /**
96
     * List of diff File objects.
97
     *
98
     * @var \SebastianFeldmann\Git\Diff\File[]
99
     */
100
    private $files = [];
101
102
    /**
103
     * The currently processed file.
104
     *
105
     * @var \SebastianFeldmann\Git\Diff\File
106
     */
107
    private $currentFile;
108
109
    /**
110
     * The file name of the currently processed file.
111
     *
112
     * @var string
113
     */
114
    private $currentFileName;
115
116
    /**
117
     * The change position of the currently processed file.
118
     *
119
     * @var string
120
     */
121
    private $currentPosition;
122
123
    /**
124
     * The operation of the currently processed file.
125
     *
126
     * @var string
127
     */
128
    private $currentOperation;
129
130
    /**
131
     * List of collected changes.
132
     *
133
     * @var \SebastianFeldmann\Git\Diff\Change[]
134
     */
135
    private $currentChanges = [];
136
137
    /**
138
     * Format the output.
139
     *
140
     * @param  array $output
141
     * @return iterable
142
     */
143 7
    public function format(array $output)
144
    {
145 7
        $previousLineType = self::LINE_TYPE_START;
146
        // for each line of the output
147 7
        for ($i = 0, $length = count($output); $i < $length; $i++) {
148 7
            $line = $output[$i];
149
            // depending on the previous line type
150
            // check for all possible following line types and handle it
151 7
            foreach (self::$lineTypesToCheck[$previousLineType] as $typeToCheck) {
152 7
                $call = 'is' . $typeToCheck . 'Line';
153
                // if the line type could be matched
154 7
                if ($this->$call($line)) {
155
                    // remember the line type
156 7
                    $previousLineType = $typeToCheck;
157 7
                    break;
158
                }
159
            }
160
        }
161 7
        $this->appendCollectedFileAndChanges();
162
163 7
        return $this->files;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->files; (SebastianFeldmann\Git\Diff\File[]) is incompatible with the return type declared by the interface SebastianFeldmann\Cli\Co...OutputFormatter::format of type SebastianFeldmann\Cli\Command\iterable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
164
    }
165
166
    /**
167
     * Is the given line a diff header line.
168
     *
169
     * diff --git a/some/file b/some/file
170
     *
171
     * @param  string $line
172
     * @return bool
173
     */
174 7 View Code Duplication
    private function isHeaderLine(string $line): bool
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...
175
    {
176 7
        $matches = [];
177 7
        if (preg_match('#^diff --git [a|b|c|i|w|o]/(.*) [a|b|c|i|w|o]/(.*)#', $line, $matches)) {
178 7
            $this->appendCollectedFileAndChanges();
179 7
            $this->currentOperation = File::OP_MODIFIED;
180 7
            $this->currentFileName  = $matches[2];
181 7
            return true;
182
        }
183 7
        return false;
184
    }
185
186
    /**
187
     * Is the given line a diff header similarity line.
188
     *
189
     * similarity index 96%
190
     *
191
     * @param  string $line
192
     * @return bool
193
     */
194 7
    private function isHeaderSimilarityLine(string $line): bool
195
    {
196 7
        $matches = [];
197 7
        return (bool)preg_match('#^(similarity|dissimilarity) index [0-9]+%$#', $line, $matches);
198
    }
199
200
    /**
201
     * Is the given line a diff header operation line.
202
     *
203
     * new file mode 100644
204
     * delete file
205
     * rename from some/file
206
     * rename to some/other/file
207
     *
208
     * @param  string $line
209
     * @return bool
210
     */
211 7
    private function isHeaderOpLine(string $line): bool
212
    {
213 7
        $matches = [];
214 7
        if (preg_match('#^(old|new|deleted|rename|copy) (file mode|from|to) (.+)#', $line, $matches)) {
215 5
            $this->currentOperation = self::$opsMap[$matches[1]];
216 5
            return true;
217
        }
218 7
        return false;
219
    }
220
221
    /**
222
     * Is the given line a diff header index line.
223
     *
224
     * index f7fc435..7b5bd26 100644
225
     *
226
     * @param  string $line
227
     * @return bool
228
     */
229 7 View Code Duplication
    private function isHeaderIndexLine(string $line): bool
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...
230
    {
231 7
        $matches = [];
232 7
        if (preg_match('#^index\s([a-z0-9]+)\.\.([a-z0-9]+)(.*)$#i', $line, $matches)) {
233 7
            $this->currentFile = new File($this->currentFileName, $this->currentOperation);
234 7
            return true;
235
        }
236 1
        return false;
237
    }
238
239
    /**
240
     * Is the given line a diff header format line.
241
     *
242
     * --- a/some/file
243
     * +++ b/some/file
244
     *
245
     * @param  string $line
246
     * @return bool
247
     */
248 7
    private function isHeaderFormatLine(string $line): bool
249
    {
250 7
        $matches = [];
251 7
        return (bool)preg_match('#^[\-\+]{3} [a|b|c|i|w|o]?/.*#', $line, $matches);
252
    }
253
254
    /**
255
     * Is the given line a diff change position line.
256
     *
257
     * @@ -4,3 +4,10 @@ some file hint
258
     *
259
     * @param  string $line
260
     * @return bool
261
     */
262 7
    private function isChangePositionLine(string $line): bool
263
    {
264 7
        $matches = [];
265 7
        if (preg_match('#^@@ (\-[0-9,]{3,} \+[0-9,]{3,}) @@ ?(.*)$#', $line, $matches)) {
266 7
            $this->currentPosition                        = $matches[1];
267 7
            $this->currentChanges[$this->currentPosition] = new Change($matches[1], $matches[2]);
268 7
            return true;
269
        }
270 7
        return false;
271
    }
272
273
    /**
274
     * In our case we treat every line as code line if no other line type matched before.
275
     *
276
     * @param  string $line
277
     * @return bool
278
     */
279 7
    private function isChangeCodeLine(string $line): bool
280
    {
281 7
        $line = $this->parseCodeLine($line);
282 7
        $this->currentChanges[$this->currentPosition]->addLine($line);
283 7
        return true;
284
    }
285
286
    /**
287
     * Determines the line type and cleans up the line.
288
     *
289
     * @param  string $line
290
     * @return \SebastianFeldmann\Git\Diff\Line
291
     */
292 7
    private function parseCodeLine(string $line): Line
293
    {
294 7
        if (strlen($line) == 0) {
295 1
            return new Line(Line::EXISTED, '');
296
        }
297
298 7
        $firstChar = $line[0];
299 7
        $cleanLine = rtrim(substr($line, 1));
300
301 7
        return new Line(Line::$opsMap[$firstChar], $cleanLine);
302
    }
303
304
    /**
305
     * Add all collected changes to its file.
306
     */
307 7
    private function appendCollectedFileAndChanges()
308
    {
309 7
        if (!empty($this->currentFile)) {
310 7
            foreach ($this->currentChanges as $change) {
311 7
                $this->currentFile->addChange($change);
312
            }
313 7
            $this->files[] = $this->currentFile;
314
        }
315 7
        $this->currentChanges = [];
316 7
    }
317
}
318