Failed Conditions
Pull Request — master (#1990)
by Mischa
01:34
created

coalib.results.Diff   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 249
Duplicated Lines 0 %
Metric Value
dl 0
loc 249
rs 8.3673
wmc 45

16 Methods

Rating   Name   Duplication   Size   Complexity  
A Diff.from_clang_fixit() 0 22 1
C Diff.modified() 0 26 7
A Diff.change_line() 0 11 2
D Diff.from_string_arrays() 0 39 8
A Diff.original() 0 6 1
A Diff.affected_code() 0 9 2
A Diff.range() 0 14 1
A Diff.__json__() 0 6 1
A Diff.__len__() 0 2 1
A Diff.unified_diff() 0 9 1
A Diff.delete_line() 0 7 1
A Diff.add_lines() 0 18 3
A Diff._get_change() 0 7 3
B Diff.__add__() 0 20 6
A Diff.__eq__() 0 3 1
B Diff.split_diff() 0 20 5

How to fix   Complexity   

Complex Class

Complex classes like coalib.results.Diff 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.

1
import copy
2
import difflib
3
4
from coalib.results.LineDiff import LineDiff, ConflictError
5
from coalib.results.SourceRange import SourceRange
6
7
8
class Diff:
9
    """
10
    A Diff result represents a difference for one file.
11
    """
12
13
    def __init__(self, file_list):
14
        """
15
        Creates an empty diff for the given file.
16
17
        :param file_list: The original (unmodified) file as a list of its
18
                          lines.
19
        """
20
        self._changes = {}
21
        self._file = file_list
22
23
    @classmethod
24
    def from_string_arrays(cls, file_array_1, file_array_2):
25
        """
26
        Creates a Diff object from two arrays containing strings.
27
28
        If this Diff is applied to the original array, the second array will be
29
        created.
30
31
        :param file_array_1: Original array
32
        :param file_array_2: Array to compare
33
        """
34
        result = cls(file_array_1)
35
36
        matcher = difflib.SequenceMatcher(None, file_array_1, file_array_2)
37
        # We use this because its faster (generator) and doesnt yield as much
38
        # useless information as get_opcodes.
39
        for change_group in matcher.get_grouped_opcodes(1):
40
            for (tag,
41
                 a_index_1,
42
                 a_index_2,
43
                 b_index_1,
44
                 b_index_2) in change_group:
45
                if tag == "delete":
46
                    for index in range(a_index_1+1, a_index_2+1):
47
                        result.delete_line(index)
48
                elif tag == "insert":
49
                    # We add after line, they add before, so dont add 1 here
50
                    result.add_lines(a_index_1,
51
                                     file_array_2[b_index_1:b_index_2])
52
                elif tag == "replace":
53
                    result.change_line(a_index_1+1,
54
                                       file_array_1[a_index_1],
55
                                       file_array_2[b_index_1])
56
                    result.add_lines(a_index_1+1,
57
                                     file_array_2[b_index_1+1:b_index_2])
58
                    for index in range(a_index_1+2, a_index_2+1):
59
                        result.delete_line(index)
60
61
        return result
62
63
    @classmethod
64
    def from_clang_fixit(cls, fixit, file):
65
        """
66
        Creates a Diff object from a given clang fixit and the file contents.
67
68
        :param fixit: A cindex.Fixit object.
69
        :param file:  A list of lines in the file to apply the fixit to.
70
        :return:      The corresponding Diff object.
71
        """
72
73
        oldvalue = '\n'.join(file[fixit.range.start.line-1:
74
                                  fixit.range.end.line])
75
        endindex = fixit.range.end.column - len(file[fixit.range.end.line-1])-1
76
77
        newvalue = (oldvalue[:fixit.range.start.column-1] +
78
                    fixit.value +
79
                    oldvalue[endindex:])
80
        new_file = (file[:fixit.range.start.line-1] +
81
                    newvalue.splitlines(True) +
82
                    file[fixit.range.end.line:])
83
84
        return cls.from_string_arrays(file, new_file)
85
86
    def _get_change(self, line_nr, min_line=1):
87
        if not isinstance(line_nr, int):
88
            raise TypeError("line_nr needs to be an integer.")
89
        if line_nr < min_line:
90
            raise ValueError("The given line number is not allowed.")
91
92
        return self._changes.get(line_nr, LineDiff())
93
94
    def __len__(self):
95
        return len(self._changes)
96
97
    @property
98
    def original(self):
99
        """
100
        Retrieves the original file.
101
        """
102
        return self._file
103
104
    @property
105
    def modified(self):
106
        """
107
        Calculates the modified file, after applying the Diff to the original.
108
        """
109
        result = []
110
        current_line = 0
111
112
        # Note that line_nr counts from _1_ although 0 is possible when
113
        # inserting lines before everything
114
        for line_nr in sorted(self._changes):
115
            result.extend(self._file[current_line:max(line_nr-1, 0)])
116
            linediff = self._changes[line_nr]
117
            if not linediff.delete and not linediff.change and line_nr > 0:
118
                result.append(self._file[line_nr-1])
119
            elif linediff.change:
120
                result.append(linediff.change[1])
121
122
            if linediff.add_after:
123
                result.extend(linediff.add_after)
124
125
            current_line = line_nr
126
127
        result.extend(self._file[current_line:])
128
129
        return result
130
131
    @property
132
    def unified_diff(self):
133
        """
134
        Generates a unified diff corresponding to this patch.
135
136
        Note that the unified diff is not deterministic and thus not suitable
137
        for equality comparison.
138
        """
139
        return ''.join(difflib.unified_diff(self.original, self.modified))
140
141
    def __json__(self):
142
        """
143
        Override JSON export, using the unified diff is the easiest thing for
144
        the users.
145
        """
146
        return self.unified_diff
147
148
    def affected_code(self, filename):
149
        """
150
        Creates a list of SourceRange objects which point to the related code.
151
        Changes on continuous lines will be put into one SourceRange.
152
153
        :param filename: The filename to associate the SourceRange's to.
154
        :return:         A list of all related SourceRange objects.
155
        """
156
        return list(diff.range(filename) for diff in self.split_diff())
157
158
    def split_diff(self):
159
        """
160
        Splits this diff into small pieces, such that several continuously
161
        altered lines are still together in one diff. All subdiffs will be
162
        yielded.
163
        """
164
        diffs = []
165
166
        last_line = -1
167
        this_diff = Diff(self._file)
168
        for line in sorted(self._changes.keys()):
169
            if line != last_line + 1 and len(this_diff._changes) > 0:
170
                yield this_diff
171
                this_diff = Diff(self._file)
172
173
            last_line = line
174
            this_diff._changes[line] = self._changes[line]
175
176
        if len(this_diff._changes) > 0:
177
            yield this_diff
178
179
    def range(self, filename):
180
        """
181
        Calculates a SourceRange spanning over the whole Diff. If something is
182
        added after the 0th line (i.e. before the first line) the first line
183
        will be included in the SourceRange.
184
185
        :param filename: The filename to associate the SourceRange with.
186
        :return:         A SourceRange object.
187
        """
188
        start = min(self._changes.keys())
189
        end = max(self._changes.keys())
190
        return SourceRange.from_values(filename,
191
                                       start_line=max(1, start),
192
                                       end_line=max(1, end))
193
194
    def __add__(self, other):
195
        """
196
        Adds another diff to this one. Will throw an exception if this is not
197
        possible. (This will *not* be done in place.)
198
        """
199
        if not isinstance(other, Diff):
200
            raise TypeError("Only diffs can be added to a diff.")
201
202
        result = copy.deepcopy(self)
203
204
        for line_nr in other._changes:
205
            change = other._changes[line_nr]
206
            if change.delete is True:
207
                result.delete_line(line_nr)
208
            if change.add_after is not False:
209
                result.add_lines(line_nr, change.add_after)
210
            if change.change is not False:
211
                result.change_line(line_nr, change.change[0], change.change[1])
212
213
        return result
214
215
    def delete_line(self, line_nr):
216
        """
217
        Mark the given line nr as deleted. The first line is line number 1.
218
        """
219
        linediff = self._get_change(line_nr)
220
        linediff.delete = True
221
        self._changes[line_nr] = linediff
222
223
    def add_lines(self, line_nr_before, lines):
224
        """
225
        Adds lines after the given line number.
226
227
        :param line_nr_before: Line number of the line before the additions.
228
                               Use 0 for insert lines before everything.
229
        :param lines:          A list of lines to add.
230
        """
231
        if lines == []:
232
            return  # No action
233
234
        linediff = self._get_change(line_nr_before, min_line=0)
235
        if linediff.add_after is not False:
236
            raise ConflictError("Cannot add lines after the given line since "
237
                                "there are already lines.")
238
239
        linediff.add_after = lines
240
        self._changes[line_nr_before] = linediff
241
242
    def change_line(self, line_nr, original_line, replacement):
243
        """
244
        Changes the given line with the given line number. The replacement will
245
        be there instead.
246
        """
247
        linediff = self._get_change(line_nr)
248
        if linediff.change is not False:
249
            raise ConflictError("An already changed line cannot be changed.")
250
251
        linediff.change = (original_line, replacement)
252
        self._changes[line_nr] = linediff
253
254
    def __eq__(self, other):
255
        return ((self._file == other._file) and
256
                (self.modified == other.modified))
257